Skip to content

Commit

Permalink
Merge pull request #1882 from python-gitlab/jlvillal/custom_warn
Browse files Browse the repository at this point in the history
chore: create a custom `warnings.warn` wrapper
  • Loading branch information
nejch committed Feb 6, 2022
2 parents 4cb7d92 + 6ca9aa2 commit 5beda3b
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 20 deletions.
13 changes: 8 additions & 5 deletions gitlab/__init__.py
Expand Up @@ -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__,
Expand All @@ -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}")
38 changes: 37 additions & 1 deletion gitlab/utils.py
Expand Up @@ -15,8 +15,11 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

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

Expand Down Expand Up @@ -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,
)
11 changes: 6 additions & 5 deletions gitlab/v4/objects/artifacts.py
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
21 changes: 12 additions & 9 deletions 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
Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand Down
19 changes: 19 additions & 0 deletions tests/unit/test_utils.py
Expand Up @@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import json
import warnings

from gitlab import utils

Expand Down Expand Up @@ -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

0 comments on commit 5beda3b

Please sign in to comment.