GeezerVison is showing github.com — chore: create a custom `warnings.warn` wrapper · python-gitlab/python-gitlab@6ca9aa2
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: gitlab/__init__.py +8-5Lines changed: 8 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 20 | 20 | from typing import Any | 21 | 21 |
| 22 | 22 | import gitlab.config # noqa: F401 | | 23 | +from gitlab import utils as _utils | 23 | 24 | from gitlab._version import ( # noqa: F401 | 24 | 25 | __author__, | 25 | 26 | __copyright__, | | 40 | 41 | def __getattr__(name: str) -> Any: | 41 | 42 | # Deprecate direct access to constants without namespace | 42 | 43 | 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, | 48 | 51 | ) | 49 | 52 | return getattr(gitlab.const, name) | 50 | 53 | raise AttributeError(f"module {__name__} has no attribute {name}") |
Collapse file Copy file name to clipboardExpand all lines: gitlab/utils.py +37-1Lines changed: 37 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 15 | 15 | # You should have received a copy of the GNU Lesser General Public License | 16 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | 17 | 17 |
| | 18 | +import pathlib | | 19 | +import traceback | 18 | 20 | 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 | 20 | 23 |
| 21 | 24 | import requests | 22 | 25 |
| @@ -90,3 +93,36 @@ def __new__( # type: ignore | 90 | 93 |
| 91 | 94 | def remove_none_from_dict(data: Dict[str, Any]) -> Dict[str, Any]: | 92 | 95 | 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 | + ) |
Collapse file Copy file name to clipboardExpand all lines: gitlab/v4/objects/artifacts.py +6-5Lines changed: 6 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 2 | 2 | GitLab API: | 3 | 3 | https://docs.gitlab.com/ee/api/job_artifacts.html | 4 | 4 | """ | 5 | | -import warnings | 6 | 5 | from typing import Any, Callable, Optional, TYPE_CHECKING | 7 | 6 |
| 8 | 7 | import requests | @@ -34,10 +33,12 @@ def __call__( | 34 | 33 | *args: Any, | 35 | 34 | **kwargs: Any, | 36 | 35 | ) -> 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, | 41 | 42 | ) | 42 | 43 | return self.download( | 43 | 44 | *args, | |
Collapse file Copy file name to clipboardExpand all lines: gitlab/v4/objects/projects.py +12-9Lines changed: 12 additions & 9 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | | -import warnings | 2 | 1 | from typing import Any, Callable, cast, Dict, List, Optional, TYPE_CHECKING, Union | 3 | 2 |
| 4 | 3 | import requests | @@ -548,10 +547,12 @@ def transfer(self, to_namespace: Union[int, str], **kwargs: Any) -> None: | 548 | 547 |
| 549 | 548 | @cli.register_custom_action("Project", ("to_namespace",)) | 550 | 549 | 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, | 555 | 556 | ) | 556 | 557 | return self.transfer(*args, **kwargs) | 557 | 558 |
| @@ -562,10 +563,12 @@ def artifact( | 562 | 563 | *args: Any, | 563 | 564 | **kwargs: Any, | 564 | 565 | ) -> 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, | 569 | 572 | ) | 570 | 573 | return self.artifacts.raw(*args, **kwargs) | 571 | 574 |
| |
Collapse file Copy file name to clipboardExpand all lines: tests/unit/test_utils.py +19Lines changed: 19 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 16 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | 17 | 17 |
| 18 | 18 | import json | | 19 | +import warnings | 19 | 20 |
| 20 | 21 | from gitlab import utils | 21 | 22 |
| @@ -76,3 +77,21 @@ def test_json_serializable(self): | 76 | 77 |
| 77 | 78 | obj = utils.EncodedId("we got/a/path") | 78 | 79 | 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 |
|