diff --git a/gitlab/__init__.py b/gitlab/__init__.py
index 5f168acb2..8cffecd62 100644
--- a/gitlab/__init__.py
+++ b/gitlab/__init__.py
@@ -20,6 +20,7 @@
from typing import Any
import gitlab.config # noqa: F401
+from gitlab import utils as _utils
from gitlab._version import ( # noqa: F401
__author__,
__copyright__,
@@ -40,11 +41,13 @@
def __getattr__(name: str) -> Any:
# Deprecate direct access to constants without namespace
if name in gitlab.const._DEPRECATED:
- warnings.warn(
- f"\nDirect access to 'gitlab.{name}' is deprecated and will be "
- f"removed in a future major python-gitlab release. Please "
- f"use 'gitlab.const.{name}' instead.",
- DeprecationWarning,
+ _utils.warn(
+ message=(
+ f"\nDirect access to 'gitlab.{name}' is deprecated and will be "
+ f"removed in a future major python-gitlab release. Please "
+ f"use 'gitlab.const.{name}' instead."
+ ),
+ category=DeprecationWarning,
)
return getattr(gitlab.const, name)
raise AttributeError(f"module {__name__} has no attribute {name}")
diff --git a/gitlab/utils.py b/gitlab/utils.py
index 7b01d178d..197935549 100644
--- a/gitlab/utils.py
+++ b/gitlab/utils.py
@@ -15,8 +15,11 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see .
+import pathlib
+import traceback
import urllib.parse
-from typing import Any, Callable, Dict, Optional, Union
+import warnings
+from typing import Any, Callable, Dict, Optional, Type, Union
import requests
@@ -90,3 +93,36 @@ def __new__( # type: ignore
def remove_none_from_dict(data: Dict[str, Any]) -> Dict[str, Any]:
return {k: v for k, v in data.items() if v is not None}
+
+
+def warn(
+ message: str,
+ *,
+ category: Optional[Type] = None,
+ source: Optional[Any] = None,
+) -> None:
+ """This `warnings.warn` wrapper function attempts to show the location causing the
+ warning in the user code that called the library.
+
+ It does this by walking up the stack trace to find the first frame located outside
+ the `gitlab/` directory. This is helpful to users as it shows them their code that
+ is causing the warning.
+ """
+ # Get `stacklevel` for user code so we indicate where issue is in
+ # their code.
+ pg_dir = pathlib.Path(__file__).parent.resolve()
+ stack = traceback.extract_stack()
+ stacklevel = 1
+ warning_from = ""
+ for stacklevel, frame in enumerate(reversed(stack), start=1):
+ if stacklevel == 2:
+ warning_from = f" (python-gitlab: {frame.filename}:{frame.lineno})"
+ frame_dir = str(pathlib.Path(frame.filename).parent.resolve())
+ if not frame_dir.startswith(str(pg_dir)):
+ break
+ warnings.warn(
+ message=message + warning_from,
+ category=category,
+ stacklevel=stacklevel,
+ source=source,
+ )
diff --git a/gitlab/v4/objects/artifacts.py b/gitlab/v4/objects/artifacts.py
index dee28804e..55d762be1 100644
--- a/gitlab/v4/objects/artifacts.py
+++ b/gitlab/v4/objects/artifacts.py
@@ -2,7 +2,6 @@
GitLab API:
https://docs.gitlab.com/ee/api/job_artifacts.html
"""
-import warnings
from typing import Any, Callable, Optional, TYPE_CHECKING
import requests
@@ -34,10 +33,12 @@ def __call__(
*args: Any,
**kwargs: Any,
) -> Optional[bytes]:
- warnings.warn(
- "The project.artifacts() method is deprecated and will be "
- "removed in a future version. Use project.artifacts.download() instead.\n",
- DeprecationWarning,
+ utils.warn(
+ message=(
+ "The project.artifacts() method is deprecated and will be removed in a "
+ "future version. Use project.artifacts.download() instead.\n"
+ ),
+ category=DeprecationWarning,
)
return self.download(
*args,
diff --git a/gitlab/v4/objects/projects.py b/gitlab/v4/objects/projects.py
index d1e993b4c..81eb62496 100644
--- a/gitlab/v4/objects/projects.py
+++ b/gitlab/v4/objects/projects.py
@@ -1,4 +1,3 @@
-import warnings
from typing import Any, Callable, cast, Dict, List, Optional, TYPE_CHECKING, Union
import requests
@@ -548,10 +547,12 @@ def transfer(self, to_namespace: Union[int, str], **kwargs: Any) -> None:
@cli.register_custom_action("Project", ("to_namespace",))
def transfer_project(self, *args: Any, **kwargs: Any) -> None:
- warnings.warn(
- "The project.transfer_project() method is deprecated and will be "
- "removed in a future version. Use project.transfer() instead.",
- DeprecationWarning,
+ utils.warn(
+ message=(
+ "The project.transfer_project() method is deprecated and will be "
+ "removed in a future version. Use project.transfer() instead."
+ ),
+ category=DeprecationWarning,
)
return self.transfer(*args, **kwargs)
@@ -562,10 +563,12 @@ def artifact(
*args: Any,
**kwargs: Any,
) -> Optional[bytes]:
- warnings.warn(
- "The project.artifact() method is deprecated and will be "
- "removed in a future version. Use project.artifacts.raw() instead.",
- DeprecationWarning,
+ utils.warn(
+ message=(
+ "The project.artifact() method is deprecated and will be "
+ "removed in a future version. Use project.artifacts.raw() instead."
+ ),
+ category=DeprecationWarning,
)
return self.artifacts.raw(*args, **kwargs)
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 9f909830d..7641c6979 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -16,6 +16,7 @@
# along with this program. If not, see .
import json
+import warnings
from gitlab import utils
@@ -76,3 +77,21 @@ def test_json_serializable(self):
obj = utils.EncodedId("we got/a/path")
assert '"we%20got%2Fa%2Fpath"' == json.dumps(obj)
+
+
+class TestWarningsWrapper:
+ def test_warn(self):
+ warn_message = "short and stout"
+ warn_source = "teapot"
+
+ with warnings.catch_warnings(record=True) as caught_warnings:
+ utils.warn(message=warn_message, category=UserWarning, source=warn_source)
+ assert len(caught_warnings) == 1
+ warning = caught_warnings[0]
+ # File name is this file as it is the first file outside of the `gitlab/` path.
+ assert __file__ == warning.filename
+ assert warning.category == UserWarning
+ assert isinstance(warning.message, UserWarning)
+ assert warn_message in str(warning.message)
+ assert __file__ in str(warning.message)
+ assert warn_source == warning.source