Skip to content

Commit 75c5089

Browse files
chore: create a custom warnings.warn wrapper
Create a custom `warnings.warn` wrapper that will walk the stack trace to find the first frame outside of the `gitlab/` path to print the warning against. This will make it easier for users to find where in their code the error is generated from
1 parent 4cb7d92 commit 75c5089

File tree

6 files changed

+95
-25
lines changed

6 files changed

+95
-25
lines changed

docs/api-usage.rst

+13-5
Original file line numberDiff line numberDiff line change
@@ -93,20 +93,26 @@ Examples:
9393
.. code-block:: python
9494
9595
# list all the projects
96-
projects = gl.projects.list()
96+
projects = gl.projects.list(as_list=False)
9797
for project in projects:
9898
print(project)
9999
100100
# get the group with id == 2
101101
group = gl.groups.get(2)
102-
for project in group.projects.list():
102+
for project in group.projects.list(as_list=False):
103103
print(project)
104104
105105
# create a new user
106106
user_data = {'email': 'jen@foo.com', 'username': 'jen', 'name': 'Jen'}
107107
user = gl.users.create(user_data)
108108
print(user)
109109
110+
.. warning::
111+
Calling `list()` without any arguments will by default not return the complete list
112+
of items. Use either the `all=True` or `as_list=False` parameters to get all the
113+
items when using listing methods. See the :ref:`pagination` section for more
114+
information.
115+
110116
You can list the mandatory and optional attributes for object creation and
111117
update with the manager's ``get_create_attrs()`` and ``get_update_attrs()``
112118
methods. They return 2 tuples, the first one is the list of mandatory
@@ -133,7 +139,7 @@ Some objects also provide managers to access related GitLab resources:
133139
134140
# list the issues for a project
135141
project = gl.projects.get(1)
136-
issues = project.issues.list()
142+
issues = project.issues.list(all=True)
137143
138144
python-gitlab allows to send any data to the GitLab server when making queries.
139145
In case of invalid or missing arguments python-gitlab will raise an exception
@@ -150,9 +156,9 @@ conflict with python or python-gitlab when using them as kwargs:
150156

151157
.. code-block:: python
152158
153-
gl.user_activities.list(from='2019-01-01') ## invalid
159+
gl.user_activities.list(from='2019-01-01', as_list=False) ## invalid
154160
155-
gl.user_activities.list(query_parameters={'from': '2019-01-01'}) # OK
161+
gl.user_activities.list(query_parameters={'from': '2019-01-01'}, as_list=False) # OK
156162
157163
Gitlab Objects
158164
==============
@@ -222,6 +228,8 @@ a project (the previous example used 2 API calls):
222228
project = gl.projects.get(1, lazy=True) # no API call
223229
project.star() # API call
224230
231+
.. _pagination:
232+
225233
Pagination
226234
==========
227235

gitlab/__init__.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from typing import Any
2121

2222
import gitlab.config # noqa: F401
23+
from gitlab import utils as _utils
2324
from gitlab._version import ( # noqa: F401
2425
__author__,
2526
__copyright__,
@@ -40,11 +41,13 @@
4041
def __getattr__(name: str) -> Any:
4142
# Deprecate direct access to constants without namespace
4243
if name in gitlab.const._DEPRECATED:
43-
warnings.warn(
44-
f"\nDirect access to 'gitlab.{name}' is deprecated and will be "
45-
f"removed in a future major python-gitlab release. Please "
46-
f"use 'gitlab.const.{name}' instead.",
47-
DeprecationWarning,
44+
_utils.warn(
45+
message=(
46+
f"\nDirect access to 'gitlab.{name}' is deprecated and will be "
47+
f"removed in a future major python-gitlab release. Please "
48+
f"use 'gitlab.const.{name}' instead."
49+
),
50+
category=DeprecationWarning,
4851
)
4952
return getattr(gitlab.const, name)
5053
raise AttributeError(f"module {__name__} has no attribute {name}")

gitlab/utils.py

+37-1
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
# You should have received a copy of the GNU Lesser General Public License
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

18+
import pathlib
19+
import traceback
1820
import urllib.parse
19-
from typing import Any, Callable, Dict, Optional, Union
21+
import warnings
22+
from typing import Any, Callable, Dict, Optional, Type, Union
2023

2124
import requests
2225

@@ -90,3 +93,36 @@ def __new__( # type: ignore
9093

9194
def remove_none_from_dict(data: Dict[str, Any]) -> Dict[str, Any]:
9295
return {k: v for k, v in data.items() if v is not None}
96+
97+
98+
def warn(
99+
message: str,
100+
*,
101+
category: Optional[Type] = None,
102+
source: Optional[Any] = None,
103+
) -> None:
104+
"""This `warnings.warn` wrapper function attempts to show the location causing the
105+
warning in the user code that called the library.
106+
107+
It does this by walking up the stack trace to find the first frame located outside
108+
the `gitlab/` directory. This is helpful to users as it shows them their code that
109+
is causing the warning.
110+
"""
111+
# Get `stacklevel` for user code so we indicate where issue is in
112+
# their code.
113+
pg_dir = pathlib.Path(__file__).parent.resolve()
114+
stack = traceback.extract_stack()
115+
stacklevel = 1
116+
warning_from = ""
117+
for stacklevel, frame in enumerate(reversed(stack), start=1):
118+
if stacklevel == 2:
119+
warning_from = f" (python-gitlab: {frame.filename}:{frame.lineno})"
120+
frame_dir = str(pathlib.Path(frame.filename).parent.resolve())
121+
if not frame_dir.startswith(str(pg_dir)):
122+
break
123+
warnings.warn(
124+
message=message + warning_from,
125+
category=category,
126+
stacklevel=stacklevel,
127+
source=source,
128+
)

gitlab/v4/objects/artifacts.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
GitLab API:
33
https://docs.gitlab.com/ee/api/job_artifacts.html
44
"""
5-
import warnings
65
from typing import Any, Callable, Optional, TYPE_CHECKING
76

87
import requests
@@ -34,10 +33,12 @@ def __call__(
3433
*args: Any,
3534
**kwargs: Any,
3635
) -> Optional[bytes]:
37-
warnings.warn(
38-
"The project.artifacts() method is deprecated and will be "
39-
"removed in a future version. Use project.artifacts.download() instead.\n",
40-
DeprecationWarning,
36+
utils.warn(
37+
message=(
38+
"The project.artifacts() method is deprecated and will be removed in a "
39+
"future version. Use project.artifacts.download() instead.\n"
40+
),
41+
category=DeprecationWarning,
4142
)
4243
return self.download(
4344
*args,

gitlab/v4/objects/projects.py

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import warnings
21
from typing import Any, Callable, cast, Dict, List, Optional, TYPE_CHECKING, Union
32

43
import requests
@@ -548,10 +547,12 @@ def transfer(self, to_namespace: Union[int, str], **kwargs: Any) -> None:
548547

549548
@cli.register_custom_action("Project", ("to_namespace",))
550549
def transfer_project(self, *args: Any, **kwargs: Any) -> None:
551-
warnings.warn(
552-
"The project.transfer_project() method is deprecated and will be "
553-
"removed in a future version. Use project.transfer() instead.",
554-
DeprecationWarning,
550+
utils.warn(
551+
message=(
552+
"The project.transfer_project() method is deprecated and will be "
553+
"removed in a future version. Use project.transfer() instead."
554+
),
555+
category=DeprecationWarning,
555556
)
556557
return self.transfer(*args, **kwargs)
557558

@@ -562,10 +563,12 @@ def artifact(
562563
*args: Any,
563564
**kwargs: Any,
564565
) -> Optional[bytes]:
565-
warnings.warn(
566-
"The project.artifact() method is deprecated and will be "
567-
"removed in a future version. Use project.artifacts.raw() instead.",
568-
DeprecationWarning,
566+
utils.warn(
567+
message=(
568+
"The project.artifact() method is deprecated and will be "
569+
"removed in a future version. Use project.artifacts.raw() instead."
570+
),
571+
category=DeprecationWarning,
569572
)
570573
return self.artifacts.raw(*args, **kwargs)
571574

tests/unit/test_utils.py

+19
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

1818
import json
19+
import warnings
1920

2021
from gitlab import utils
2122

@@ -76,3 +77,21 @@ def test_json_serializable(self):
7677

7778
obj = utils.EncodedId("we got/a/path")
7879
assert '"we%20got%2Fa%2Fpath"' == json.dumps(obj)
80+
81+
82+
class TestWarningsWrapper:
83+
def test_warn(self):
84+
warn_message = "short and stout"
85+
warn_source = "teapot"
86+
87+
with warnings.catch_warnings(record=True) as caught_warnings:
88+
utils.warn(message=warn_message, category=UserWarning, source=warn_source)
89+
assert len(caught_warnings) == 1
90+
warning = caught_warnings[0]
91+
# File name is this file as it is the first file outside of the `gitlab/` path.
92+
assert __file__ == warning.filename
93+
assert warning.category == UserWarning
94+
assert isinstance(warning.message, UserWarning)
95+
assert warn_message in str(warning.message)
96+
assert __file__ in str(warning.message)
97+
assert warn_source == warning.source

0 commit comments

Comments
 (0)