deprecated-params 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Vizonex
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: deprecated-params
3
+ Version: 0.1.0
4
+ Summary: A Wrapper for functions, class objects and methods for deprecating keyword parameters
5
+ Author: Vizonex
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: typing-extensions; python_version < "3.10"
11
+ Dynamic: license-file
12
+
13
+ # Deprecated Params
14
+
15
+ Inspired after python's warning.deprecated wrapper, deprecated_params is made to serve the single purpose of deprecating parameter names to warn users
16
+ about incoming changes as well as retaining typehinting.
17
+
18
+
19
+
20
+ ## How to Deprecate Parameters
21
+ Parameters should be keyword arguments, not positional, Reason
22
+ for this implementation is that in theory you should've already
23
+ planned an alternative approch to an argument you wish
24
+ to deprecate.
25
+
26
+ ```python
27
+ from deprecated_params import deprecated_params
28
+
29
+ @deprecated_params(['x'])
30
+ def func(y, *, x:int = 0):
31
+ pass
32
+
33
+ # DeprecationWarning: Parameter "x" is deprecated
34
+ func(None, x=20)
35
+
36
+ # NOTE: **kw is accepted but also you could put down more than one
37
+ # parameter if needed...
38
+ @deprecated_params(['foo'], {"foo":"foo was removed in ... don't use it"}, display_kw=False)
39
+ class MyClass:
40
+ def __init__(self, spam:object, **kw):
41
+ self.spam = spam
42
+ self.foo = kw.get("foo", None)
43
+
44
+ # DeprecationWarning: foo was removed in ... don't use it
45
+ mc = MyClass("spam", foo="X")
46
+ ```
@@ -0,0 +1,34 @@
1
+ # Deprecated Params
2
+
3
+ Inspired after python's warning.deprecated wrapper, deprecated_params is made to serve the single purpose of deprecating parameter names to warn users
4
+ about incoming changes as well as retaining typehinting.
5
+
6
+
7
+
8
+ ## How to Deprecate Parameters
9
+ Parameters should be keyword arguments, not positional, Reason
10
+ for this implementation is that in theory you should've already
11
+ planned an alternative approch to an argument you wish
12
+ to deprecate.
13
+
14
+ ```python
15
+ from deprecated_params import deprecated_params
16
+
17
+ @deprecated_params(['x'])
18
+ def func(y, *, x:int = 0):
19
+ pass
20
+
21
+ # DeprecationWarning: Parameter "x" is deprecated
22
+ func(None, x=20)
23
+
24
+ # NOTE: **kw is accepted but also you could put down more than one
25
+ # parameter if needed...
26
+ @deprecated_params(['foo'], {"foo":"foo was removed in ... don't use it"}, display_kw=False)
27
+ class MyClass:
28
+ def __init__(self, spam:object, **kw):
29
+ self.spam = spam
30
+ self.foo = kw.get("foo", None)
31
+
32
+ # DeprecationWarning: foo was removed in ... don't use it
33
+ mc = MyClass("spam", foo="X")
34
+ ```
@@ -0,0 +1,273 @@
1
+ """
2
+ Deprecated Params
3
+ -----------------
4
+
5
+ A Library dedicated for warning users about deprecated paramter
6
+ names and changes
7
+
8
+
9
+
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import inspect
15
+ import sys
16
+ import warnings
17
+ import functools
18
+ import inspect
19
+ from types import MethodType
20
+ from typing import Callable, Sequence, TypeVar, Mapping, overload
21
+
22
+ if sys.version_info < (3, 10):
23
+ from typing_extensions import ParamSpec
24
+ else:
25
+ from typing import ParamSpec
26
+
27
+
28
+ __version__ = "0.1.0"
29
+ __license__ = "Appache 2.0"
30
+
31
+
32
+ _T = TypeVar("_T", covariant=True)
33
+ _P = ParamSpec("_P")
34
+
35
+ KEYWORD_ONLY = inspect.Parameter.KEYWORD_ONLY
36
+ VAR_KEYWORD = inspect.Parameter.VAR_KEYWORD
37
+
38
+
39
+ __all__ = (
40
+ "MissingKeywordsError",
41
+ "InvalidParametersError",
42
+ "deprecated_params",
43
+ )
44
+
45
+
46
+ class _KeywordsBaseException(Exception):
47
+ def __init__(self, keywords: set[str], *args):
48
+ self._keywords = keywords
49
+ super().__init__(*args)
50
+
51
+ @property
52
+ def keywords(self):
53
+ return self._keywords
54
+
55
+ @keywords.setter
56
+ def keywords(self, kw):
57
+ raise ValueError("keywords property is immutable")
58
+
59
+
60
+ class MissingKeywordsError(_KeywordsBaseException):
61
+ """Missing keyword for argument"""
62
+
63
+ def __init__(self, keywords: set[str], *args):
64
+ super().__init__(
65
+ keywords, f"Missing Keyword arguments for: {list(keywords)!r}", *args
66
+ )
67
+
68
+
69
+ class InvalidParametersError(_KeywordsBaseException):
70
+ """Parameters were positonal arguments without defaults or keyword arguments"""
71
+
72
+ def __init__(self, keywords: set[str], *args):
73
+ super().__init__(
74
+ keywords,
75
+ f"Arguments :{list(keywords)!r} should not be positional",
76
+ *args,
77
+ )
78
+
79
+
80
+ class deprecated_params:
81
+ """
82
+ A Wrapper inspired by python's wrapper deprecated from 3.13
83
+ and is used to deprecate parameters
84
+ """
85
+
86
+ def __init__(
87
+ self,
88
+ params: Sequence[str],
89
+ message: str | Mapping[str, str] = "is deprecated",
90
+ /,
91
+ *,
92
+ # default_message should be utilized when a keyword isn't
93
+ # given in message if messaged is defined as a dictionary.
94
+ default_message: str | None = None,
95
+ category: type[Warning] | None = DeprecationWarning,
96
+ stacklevel: int = 3,
97
+ display_kw: bool = True,
98
+ ):
99
+ if not isinstance(message, (str, dict, Mapping)):
100
+ raise TypeError(
101
+ f"Expected an object of type str or dict or Mappable type for 'message', not {type(message).__name__!r}"
102
+ )
103
+
104
+ self.params = set(params)
105
+ self.message = message or "is deprecated"
106
+ self.message_is_dict = isinstance(message, (Mapping, dict))
107
+ self.display_kw = display_kw
108
+ self.category = category
109
+ self.stacklevel = stacklevel
110
+ self.default_message = default_message or "do not use"
111
+
112
+ def __check_with_missing(
113
+ self,
114
+ fn,
115
+ missing: set[str] | None = None,
116
+ invalid_params: set[str] | None = None,
117
+ skip_missing: bool | None = None,
118
+ allow_miss: bool = False,
119
+ ):
120
+ sig = inspect.signature(fn)
121
+
122
+ missing = missing if missing is not None else set(self.params)
123
+ invalid_params = set() if invalid_params is None else invalid_params
124
+
125
+ skip_missing = (
126
+ any([p.kind == VAR_KEYWORD for p in sig.parameters.values()])
127
+ if skip_missing is None
128
+ else skip_missing
129
+ )
130
+
131
+ for m in self.params:
132
+ if not allow_miss:
133
+ p = sig.parameters[m]
134
+ else:
135
+ p = sig.parameters.get(m) # type: ignore
136
+ if p is None:
137
+ continue
138
+ print(p.kind, p.name)
139
+ # Check if were keyword only or aren't carrying a default param
140
+ if p.kind != KEYWORD_ONLY:
141
+ # Anything this isn't a keyword should be considered as deprecated
142
+ # as were still technically using it.
143
+ invalid_params.add(p.name)
144
+
145
+ if not skip_missing:
146
+ missing.remove(p.name)
147
+
148
+ return missing, invalid_params, skip_missing
149
+
150
+ def __check_for_missing_kwds(
151
+ self,
152
+ fn,
153
+ missing: set[str] | None = None,
154
+ invalid_params: set[str] | None = None,
155
+ skip_missing: bool | None = None,
156
+ allow_miss: bool = False,
157
+ ):
158
+ # copy sequence to check for missing parameter names
159
+ missing, invalid_params, skip_missing = self.__check_with_missing(
160
+ fn, missing, invalid_params, skip_missing, allow_miss
161
+ )
162
+
163
+ if invalid_params:
164
+ raise InvalidParametersError(invalid_params)
165
+
166
+ if missing and not skip_missing:
167
+ raise MissingKeywordsError(missing)
168
+
169
+ def __warn(self, kw_name: str):
170
+ msg = ""
171
+ if self.display_kw:
172
+ msg += 'Parameter "%s" ' % kw_name
173
+
174
+ if self.message_is_dict:
175
+ msg += self.message.get(kw_name, self.default_message) # type: ignore
176
+ else:
177
+ msg += self.message # type: ignore
178
+
179
+ warnings.warn(msg, self.category, stacklevel=self.stacklevel + 1)
180
+
181
+ @overload
182
+ def __call__(self, arg: type[_T]) -> type[_T]: ...
183
+
184
+ @overload
185
+ def __call__(self, arg: Callable[_P, _T]) -> Callable[_P, _T]: ...
186
+
187
+ # Mirrors python's deprecated wrapper with a few changes
188
+ def __call__(self, arg):
189
+ not_dispatched = self.params.copy()
190
+
191
+ @functools.wraps(not_dispatched)
192
+ def check_kw_arguments(kw: dict):
193
+ if not_dispatched:
194
+ for k in kw.keys():
195
+ print(k, not_dispatched)
196
+ if k in not_dispatched:
197
+ self.__warn(k)
198
+ # remove after so we don't repeat
199
+ not_dispatched.remove(k)
200
+
201
+ if isinstance(arg, type):
202
+ # NOTE: Combining init and new together is done to
203
+ # solve deprecation of both new_args and init_args
204
+
205
+ missing, invalid_params, skip_missing = self.__check_with_missing(
206
+ arg.__init__, allow_miss=True
207
+ )
208
+
209
+ original_new = arg.__new__
210
+ self.__check_for_missing_kwds(
211
+ original_new, missing, invalid_params, skip_missing, allow_miss=True
212
+ )
213
+
214
+ @functools.wraps(original_new)
215
+ def __new__(cls, /, *args, **kwargs):
216
+ check_kw_arguments(kwargs)
217
+ if original_new is not object.__new__:
218
+ return original_new(cls, *args, **kwargs)
219
+ # Python Comment: Mirrors a similar check in object.__new__.
220
+ elif cls.__init__ is object.__init__ and (args or kwargs):
221
+ raise TypeError(f"{cls.__name__}() takes no arguments")
222
+ else:
223
+ return original_new(cls)
224
+
225
+ arg.__new__ = staticmethod(__new__)
226
+
227
+ original_init_subclass = arg.__init_subclass__
228
+ # Python Comment: We need slightly different behavior if __init_subclass__
229
+ # is a bound method (likely if it was implemented in Python)
230
+ if isinstance(original_init_subclass, MethodType):
231
+ self.__check_for_missing_kwds(original_init_subclass)
232
+
233
+ original_init_subclass = original_init_subclass.__func__
234
+
235
+ @functools.wraps(original_init_subclass)
236
+ def __init_subclass__(*args, **kwargs):
237
+ check_kw_arguments(kwargs)
238
+ return original_init_subclass(*args, **kwargs)
239
+
240
+ arg.__init_subclass__ = classmethod(__init_subclass__)
241
+ # Python Comment: Or otherwise, which likely means it's a builtin such as
242
+ # object's implementation of __init_subclass__.
243
+ else:
244
+
245
+ @functools.wraps(original_init_subclass)
246
+ def __init_subclass__(*args, **kwargs):
247
+ check_kw_arguments(kwargs)
248
+ return original_init_subclass(*args, **kwargs)
249
+
250
+ arg.__init_subclass__ = __init_subclass__
251
+
252
+ return arg
253
+
254
+ elif callable(arg):
255
+ # Check for missing functon arguments
256
+ self.__check_for_missing_kwds(arg)
257
+
258
+ @functools.wraps(arg)
259
+ def wrapper(*args, **kwargs):
260
+ check_kw_arguments(kwargs)
261
+ return arg(*args, **kwargs)
262
+
263
+ if sys.version_info >= (3, 12):
264
+ if inspect.iscoroutinefunction(arg):
265
+ wrapper = inspect.markcoroutinefunction(wrapper)
266
+
267
+ return wrapper
268
+
269
+ else:
270
+ raise TypeError(
271
+ "@deprecated_params decorator with non-None category must be applied to "
272
+ f"a class or callable, not {arg!r}"
273
+ )
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: deprecated-params
3
+ Version: 0.1.0
4
+ Summary: A Wrapper for functions, class objects and methods for deprecating keyword parameters
5
+ Author: Vizonex
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: typing-extensions; python_version < "3.10"
11
+ Dynamic: license-file
12
+
13
+ # Deprecated Params
14
+
15
+ Inspired after python's warning.deprecated wrapper, deprecated_params is made to serve the single purpose of deprecating parameter names to warn users
16
+ about incoming changes as well as retaining typehinting.
17
+
18
+
19
+
20
+ ## How to Deprecate Parameters
21
+ Parameters should be keyword arguments, not positional, Reason
22
+ for this implementation is that in theory you should've already
23
+ planned an alternative approch to an argument you wish
24
+ to deprecate.
25
+
26
+ ```python
27
+ from deprecated_params import deprecated_params
28
+
29
+ @deprecated_params(['x'])
30
+ def func(y, *, x:int = 0):
31
+ pass
32
+
33
+ # DeprecationWarning: Parameter "x" is deprecated
34
+ func(None, x=20)
35
+
36
+ # NOTE: **kw is accepted but also you could put down more than one
37
+ # parameter if needed...
38
+ @deprecated_params(['foo'], {"foo":"foo was removed in ... don't use it"}, display_kw=False)
39
+ class MyClass:
40
+ def __init__(self, spam:object, **kw):
41
+ self.spam = spam
42
+ self.foo = kw.get("foo", None)
43
+
44
+ # DeprecationWarning: foo was removed in ... don't use it
45
+ mc = MyClass("spam", foo="X")
46
+ ```
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ deprecated_params/__init__.py
6
+ deprecated_params.egg-info/PKG-INFO
7
+ deprecated_params.egg-info/SOURCES.txt
8
+ deprecated_params.egg-info/dependency_links.txt
9
+ deprecated_params.egg-info/requires.txt
10
+ deprecated_params.egg-info/top_level.txt
11
+ tests/test_wrapper.py
@@ -0,0 +1,3 @@
1
+
2
+ [:python_version < "3.10"]
3
+ typing-extensions
@@ -0,0 +1 @@
1
+ deprecated_params
@@ -0,0 +1,17 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+
4
+ [project]
5
+ name = "deprecated-params"
6
+ version = "0.1.0"
7
+ description = "A Wrapper for functions, class objects and methods for deprecating keyword parameters"
8
+ readme = "README.md"
9
+ authors = [
10
+ { name = "Vizonex" },
11
+ ]
12
+ license = "MIT"
13
+ requires-python = ">=3.9"
14
+ dependencies = [
15
+ 'typing-extensions; python_version<"3.10"'
16
+ ]
17
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ from setuptools import setup
2
+
3
+ if __name__ == "__main__":
4
+ setup()
@@ -0,0 +1,52 @@
1
+ import pytest
2
+ from deprecated_params import deprecated_params
3
+
4
+
5
+ # should carry w x y
6
+ def test_deprecated_param():
7
+ @deprecated_params(["x"], "is deprecated")
8
+ def my_func(w: int, *, x: int = None, y: int = None):
9
+ pass
10
+
11
+ with pytest.warns(DeprecationWarning, match='Parameter "x" is deprecated'):
12
+ my_func(0, x=0)
13
+
14
+
15
+ def test_class_wrapper_and_kw_display_disabled():
16
+ @deprecated_params(["foo"], "foo is deprecated", display_kw=False)
17
+ class MyClass:
18
+ def __init__(self, spam: object, *, foo: object = None):
19
+ self.spam = spam
20
+ self.foo = foo
21
+
22
+ mc = MyClass("spam")
23
+ assert mc.spam == "spam"
24
+ assert mc.foo == None
25
+
26
+ with pytest.warns(DeprecationWarning, match="foo is deprecated"):
27
+ MyClass("spam", foo="foo")
28
+
29
+
30
+ class TornadoWarning(DeprecationWarning):
31
+ pass
32
+
33
+
34
+ def test_dataclasses_with_wrapper_message_dicts_custom_warning():
35
+ from dataclasses import dataclass, field
36
+
37
+ @deprecated_params(
38
+ ["foo"],
39
+ {"foo": "got foo", "spam": "got spam"},
40
+ display_kw=False,
41
+ category=TornadoWarning,
42
+ )
43
+ @dataclass
44
+ class Class:
45
+ foo: str | None = field(kw_only=True, default=None)
46
+ spam: str | None = field(kw_only=True, default=None)
47
+
48
+ with pytest.warns(TornadoWarning, match="got foo"):
49
+ Class(foo="foo")
50
+
51
+
52
+ # TODO: Metaclasses...