Add typing information
This is not perfect as mypy at least doesn't consider e.g. subclasses of the property decorator class to behave the same way, but it's as good as we can get without introducing a mypy plugin. Signed-off-by: Stephen Finucane <stephenfin@redhat.com> Change-Id: Ia0c4110218ce8a48c3ec5e720f07e45fc2b831bf
This commit is contained in:
22
README.rst
22
README.rst
@@ -2,23 +2,29 @@
|
||||
Debtcollector
|
||||
=============
|
||||
|
||||
.. image:: https://governance.openstack.org/tc/badges/debtcollector.svg
|
||||
.. image:: https://governance.openstack.org/tc/badges/debtcollector
|
||||
:target: https://governance.openstack.org/tc/reference/projects/oslo.html
|
||||
|
||||
.. Change things from this point on
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/debtcollector.svg
|
||||
.. image:: https://img.shields.io/pypi/v/debtcollector
|
||||
:target: https://pypi.org/project/debtcollector/
|
||||
:alt: Latest Version
|
||||
|
||||
.. image:: https://img.shields.io/pypi/dm/debtcollector
|
||||
:target: https://pypi.org/project/debtcollector/
|
||||
:alt: Downloads
|
||||
|
||||
.. image:: https://img.shields.io/pypi/types/debtcollector
|
||||
:target: https://pypi.org/project/debtcollector/
|
||||
:alt: Typing Status
|
||||
|
||||
A collection of Python deprecation patterns and strategies that help you
|
||||
collect your technical debt in a non-destructive manner. The goal of this
|
||||
library is to provide well documented developer facing deprecation
|
||||
patterns that start of with a basic set and can expand into a larger
|
||||
set of patterns as time goes on. The desired output of these patterns
|
||||
is to apply the warnings module to emit DeprecationWarning or
|
||||
PendingDeprecationWarning or similar derivative to developers using libraries
|
||||
(or potentially applications) about future deprecations.
|
||||
|
||||
is to apply the ``warnings`` module to emit ``DeprecationWarning``,
|
||||
``PendingDeprecationWarning`` or similar derivative to developers using
|
||||
libraries (or potentially applications) about future deprecations.
|
||||
|
||||
* Free software: Apache license
|
||||
* Documentation: https://docs.openstack.org/debtcollector/latest
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
from debtcollector import _utils
|
||||
@@ -18,14 +20,14 @@ __version__ = importlib.metadata.version('debtcollector')
|
||||
|
||||
|
||||
def deprecate(
|
||||
prefix,
|
||||
postfix=None,
|
||||
message=None,
|
||||
version=None,
|
||||
removal_version=None,
|
||||
stacklevel=3,
|
||||
category=DeprecationWarning,
|
||||
):
|
||||
prefix: str,
|
||||
postfix: str | None = None,
|
||||
message: str | None = None,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
category: type[Warning] = DeprecationWarning,
|
||||
) -> None:
|
||||
"""Helper to deprecate some thing using generated message format.
|
||||
|
||||
:param prefix: prefix string used as the prefix of the output message
|
||||
|
||||
@@ -12,8 +12,14 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import builtins
|
||||
from collections.abc import Callable
|
||||
import functools
|
||||
import inspect
|
||||
import types
|
||||
from typing import Any
|
||||
import warnings
|
||||
|
||||
# See https://docs.python.org/3/library/builtins.html
|
||||
@@ -21,7 +27,11 @@ _BUILTIN_MODULES = ('builtins', 'exceptions')
|
||||
_enabled = True
|
||||
|
||||
|
||||
def deprecation(message, stacklevel=None, category=None):
|
||||
def deprecation(
|
||||
message: str,
|
||||
stacklevel: int | None = None,
|
||||
category: type[Warning] | None = None,
|
||||
) -> None:
|
||||
"""Warns about some type of deprecation that has been (or will be) made.
|
||||
|
||||
This helper function makes it easier to interact with the warnings module
|
||||
@@ -39,7 +49,7 @@ def deprecation(message, stacklevel=None, category=None):
|
||||
the deprecation warnings).
|
||||
"""
|
||||
if not _enabled:
|
||||
return
|
||||
return None
|
||||
if category is None:
|
||||
category = DeprecationWarning
|
||||
if stacklevel is None:
|
||||
@@ -47,8 +57,12 @@ def deprecation(message, stacklevel=None, category=None):
|
||||
else:
|
||||
warnings.warn(message, category=category, stacklevel=stacklevel)
|
||||
|
||||
return None
|
||||
|
||||
def get_qualified_name(obj):
|
||||
|
||||
def get_qualified_name(
|
||||
obj: Callable[..., Any] | types.ModuleType | builtins.function,
|
||||
) -> tuple[bool, str]:
|
||||
# Prefer the py3.x name (if we can get at it...)
|
||||
try:
|
||||
return (True, obj.__qualname__)
|
||||
@@ -57,8 +71,12 @@ def get_qualified_name(obj):
|
||||
|
||||
|
||||
def generate_message(
|
||||
prefix, postfix=None, message=None, version=None, removal_version=None
|
||||
):
|
||||
prefix: str,
|
||||
postfix: str | None = None,
|
||||
message: str | None = None,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
) -> str:
|
||||
"""Helper to generate a common message 'style' for deprecation helpers."""
|
||||
message_components = [prefix]
|
||||
if version:
|
||||
@@ -79,12 +97,15 @@ def generate_message(
|
||||
return ''.join(message_components)
|
||||
|
||||
|
||||
def get_assigned(decorator):
|
||||
def get_assigned(decorator: Any) -> tuple[str, ...]:
|
||||
"""Helper to fix/workaround https://bugs.python.org/issue3445"""
|
||||
return functools.WRAPPER_ASSIGNMENTS
|
||||
|
||||
|
||||
def get_class_name(obj, fully_qualified=True):
|
||||
def get_class_name(
|
||||
obj: type[Any] | Callable[..., Any],
|
||||
fully_qualified: bool = True,
|
||||
) -> str:
|
||||
"""Get class name for object.
|
||||
|
||||
If object is a type, fully qualified name of the type is returned.
|
||||
@@ -107,7 +128,7 @@ def get_class_name(obj, fully_qualified=True):
|
||||
return obj.__name__
|
||||
|
||||
|
||||
def get_method_self(method):
|
||||
def get_method_self(method: Any) -> Any:
|
||||
"""Gets the ``self`` object attached to this method (or none)."""
|
||||
if not inspect.ismethod(method):
|
||||
return None
|
||||
@@ -117,11 +138,12 @@ def get_method_self(method):
|
||||
return None
|
||||
|
||||
|
||||
def get_callable_name(function):
|
||||
def get_callable_name(function: Callable[..., Any]) -> str:
|
||||
"""Generate a name from callable.
|
||||
|
||||
Tries to do the best to guess fully qualified callable name.
|
||||
"""
|
||||
parts: tuple[str, str] | tuple[str, str, str]
|
||||
method_self = get_method_self(function)
|
||||
if method_self is not None:
|
||||
# This is a bound method.
|
||||
@@ -150,9 +172,10 @@ def get_callable_name(function):
|
||||
else:
|
||||
parts = (function.__module__, function.__name__)
|
||||
else:
|
||||
im_class = type(function)
|
||||
if im_class is type:
|
||||
if type(function) is type:
|
||||
im_class = function
|
||||
else:
|
||||
im_class = type(function)
|
||||
try:
|
||||
parts = (im_class.__module__, im_class.__qualname__)
|
||||
except AttributeError:
|
||||
|
||||
@@ -30,6 +30,6 @@ class DisableFixture(fixtures.Fixture):
|
||||
<some code that calls into depreciated code>
|
||||
"""
|
||||
|
||||
def _setUp(self):
|
||||
def _setUp(self) -> None:
|
||||
self.addCleanup(setattr, _utils, "_enabled", True)
|
||||
_utils._enabled = False
|
||||
|
||||
@@ -12,8 +12,12 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import functools
|
||||
import inspect
|
||||
from typing import Any, ParamSpec, TypeVar
|
||||
|
||||
import wrapt
|
||||
|
||||
@@ -24,26 +28,35 @@ _CLASS_MOVED_PREFIX_TPL = "Class '%s' has moved to '%s'"
|
||||
_MOVED_CALLABLE_POSTFIX = "()"
|
||||
_FUNC_MOVED_PREFIX_TPL = "Function '%s' has moved to '%s'"
|
||||
|
||||
P = ParamSpec('P')
|
||||
R = TypeVar('R')
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def _moved_decorator(
|
||||
kind,
|
||||
new_attribute_name,
|
||||
message=None,
|
||||
version=None,
|
||||
removal_version=None,
|
||||
stacklevel=3,
|
||||
attr_postfix=None,
|
||||
category=None,
|
||||
):
|
||||
kind: str,
|
||||
new_attribute_name: str,
|
||||
message: str | None = None,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
attr_postfix: str | None = None,
|
||||
category: type[Warning] | None = None,
|
||||
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
||||
"""Decorates a method/property that was moved to another location."""
|
||||
|
||||
def decorator(f):
|
||||
def decorator(f: Callable[P, R]) -> Callable[P, R]:
|
||||
fully_qualified, old_attribute_name = _utils.get_qualified_name(f)
|
||||
if attr_postfix:
|
||||
old_attribute_name += attr_postfix
|
||||
|
||||
@wrapt.decorator
|
||||
def wrapper(wrapped, instance, args, kwargs):
|
||||
def wrapper(
|
||||
wrapped: Callable[P, R],
|
||||
instance: object | None,
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any],
|
||||
) -> R:
|
||||
base_name = _utils.get_class_name(wrapped, fully_qualified=False)
|
||||
if fully_qualified:
|
||||
old_name = old_attribute_name
|
||||
@@ -68,15 +81,15 @@ def _moved_decorator(
|
||||
|
||||
|
||||
def moved_function(
|
||||
new_func,
|
||||
old_func_name,
|
||||
old_module_name,
|
||||
message=None,
|
||||
version=None,
|
||||
removal_version=None,
|
||||
stacklevel=3,
|
||||
category=None,
|
||||
):
|
||||
new_func: Callable[P, R],
|
||||
old_func_name: str,
|
||||
old_module_name: str,
|
||||
message: str | None = None,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
category: type[Warning] | None = None,
|
||||
) -> Callable[P, R]:
|
||||
"""Deprecates a function that was moved to another location.
|
||||
|
||||
This generates a wrapper around ``new_func`` that will emit a deprecation
|
||||
@@ -96,7 +109,7 @@ def moved_function(
|
||||
)
|
||||
|
||||
@functools.wraps(new_func, assigned=_utils.get_assigned(new_func))
|
||||
def old_new_func(*args, **kwargs):
|
||||
def old_new_func(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
_utils.deprecation(
|
||||
out_message, stacklevel=stacklevel, category=category
|
||||
)
|
||||
@@ -133,27 +146,29 @@ class moved_read_only_property:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
old_name,
|
||||
new_name,
|
||||
version=None,
|
||||
removal_version=None,
|
||||
stacklevel=3,
|
||||
category=None,
|
||||
old_name: str,
|
||||
new_name: str,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
category: type[Warning] | None = None,
|
||||
):
|
||||
self._old_name = old_name
|
||||
self._new_name = new_name
|
||||
self._message = _utils.generate_message(
|
||||
f"Read-only property '{self._old_name}' has moved"
|
||||
f" to '{self._new_name}'",
|
||||
f"Read-only property '{self._old_name}' has moved "
|
||||
f"to '{self._new_name}'",
|
||||
version=version,
|
||||
removal_version=removal_version,
|
||||
)
|
||||
self._stacklevel = stacklevel
|
||||
self._category = category
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
def __get__(self, instance: Any, owner: type | None = None) -> Any:
|
||||
_utils.deprecation(
|
||||
self._message, stacklevel=self._stacklevel, category=self._category
|
||||
self._message,
|
||||
stacklevel=self._stacklevel,
|
||||
category=self._category,
|
||||
)
|
||||
# This handles the descriptor being applied on a
|
||||
# instance or a class and makes both work correctly...
|
||||
@@ -165,13 +180,13 @@ class moved_read_only_property:
|
||||
|
||||
|
||||
def moved_method(
|
||||
new_method_name,
|
||||
message=None,
|
||||
version=None,
|
||||
removal_version=None,
|
||||
stacklevel=3,
|
||||
category=None,
|
||||
):
|
||||
new_method_name: str,
|
||||
message: str | None = None,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
category: type[Warning] | None = None,
|
||||
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
||||
"""Decorates an *instance* method that was moved to another location."""
|
||||
if not new_method_name.endswith(_MOVED_CALLABLE_POSTFIX):
|
||||
new_method_name += _MOVED_CALLABLE_POSTFIX
|
||||
@@ -188,13 +203,13 @@ def moved_method(
|
||||
|
||||
|
||||
def moved_property(
|
||||
new_attribute_name,
|
||||
message=None,
|
||||
version=None,
|
||||
removal_version=None,
|
||||
stacklevel=3,
|
||||
category=None,
|
||||
):
|
||||
new_attribute_name: str,
|
||||
message: str | None = None,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
category: type[Warning] | None = None,
|
||||
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
||||
"""Decorates an *instance* property that was moved to another location."""
|
||||
return _moved_decorator(
|
||||
'Property',
|
||||
@@ -208,15 +223,15 @@ def moved_property(
|
||||
|
||||
|
||||
def moved_class(
|
||||
new_class,
|
||||
old_class_name,
|
||||
old_module_name,
|
||||
message=None,
|
||||
version=None,
|
||||
removal_version=None,
|
||||
stacklevel=3,
|
||||
category=None,
|
||||
):
|
||||
new_class: type[T],
|
||||
old_class_name: str,
|
||||
old_module_name: str,
|
||||
message: str | None = None,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
category: type[Warning] | None = None,
|
||||
) -> type[T]:
|
||||
"""Deprecates a class that was moved to another location.
|
||||
|
||||
This creates a 'new-old' type that can be used for a
|
||||
@@ -241,17 +256,17 @@ def moved_class(
|
||||
removal_version=removal_version,
|
||||
)
|
||||
|
||||
def decorator(f):
|
||||
def decorator(f: Callable[P, R]) -> Callable[P, R]:
|
||||
@functools.wraps(f, assigned=_utils.get_assigned(f))
|
||||
def wrapper(self, *args, **kwargs):
|
||||
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
_utils.deprecation(
|
||||
out_message, stacklevel=stacklevel, category=category
|
||||
)
|
||||
return f(self, *args, **kwargs)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
old_class = type(old_class_name, (new_class,), {})
|
||||
old_class.__module__ = old_module_name
|
||||
old_class.__init__ = decorator(old_class.__init__)
|
||||
old_class.__init__ = decorator(old_class.__init__) # type: ignore
|
||||
return old_class
|
||||
|
||||
0
debtcollector/py.typed
Normal file
0
debtcollector/py.typed
Normal file
@@ -12,19 +12,40 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import builtins
|
||||
from collections.abc import Callable
|
||||
import functools
|
||||
import inspect
|
||||
import types
|
||||
from typing import Any
|
||||
from typing import overload
|
||||
from typing import ParamSpec
|
||||
from typing import TypeVar
|
||||
|
||||
import wrapt
|
||||
|
||||
from debtcollector import _utils
|
||||
|
||||
P = ParamSpec('P')
|
||||
R = TypeVar('R')
|
||||
T = TypeVar('T')
|
||||
|
||||
def _get_qualified_name(obj):
|
||||
|
||||
def _get_qualified_name(
|
||||
obj: Callable[..., Any] | types.ModuleType | builtins.function,
|
||||
) -> str:
|
||||
return _utils.get_qualified_name(obj)[1]
|
||||
|
||||
|
||||
def _fetch_first_result(fget, fset, fdel, apply_func, value_not_found=None):
|
||||
def _fetch_first_result(
|
||||
fget: Callable[[Any], Any] | None,
|
||||
fset: Callable[[Any, Any], None] | None,
|
||||
fdel: Callable[[Any], None] | None,
|
||||
apply_func: Callable[[Callable[..., Any] | builtins.function], str],
|
||||
value_not_found: str | None = None,
|
||||
) -> str | None:
|
||||
"""Fetch first non-none/empty result of applying ``apply_func``."""
|
||||
for f in filter(None, (fget, fset, fdel)):
|
||||
result = apply_func(f)
|
||||
@@ -61,17 +82,24 @@ class removed_property(property):
|
||||
'delete': "Deleting the '%s' property is deprecated",
|
||||
}
|
||||
|
||||
stacklevel: int
|
||||
category: type[Warning]
|
||||
version: str | None
|
||||
removal_version: str | None
|
||||
message: str | None
|
||||
_message_cache: dict[str, str]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fget=None,
|
||||
fset=None,
|
||||
fdel=None,
|
||||
doc=None,
|
||||
stacklevel=3,
|
||||
category=DeprecationWarning,
|
||||
version=None,
|
||||
removal_version=None,
|
||||
message=None,
|
||||
fget: Callable[[Any], Any] | None = None,
|
||||
fset: Callable[[Any, Any], None] | None = None,
|
||||
fdel: Callable[[Any], None] | None = None,
|
||||
doc: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
category: type[Warning] = DeprecationWarning,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
message: str | None = None,
|
||||
):
|
||||
if doc is None and inspect.isfunction(fget):
|
||||
doc = getattr(fget, '__doc__', None)
|
||||
@@ -83,7 +111,7 @@ class removed_property(property):
|
||||
self.message = message
|
||||
self._message_cache = {}
|
||||
|
||||
def _fetch_message_from_cache(self, kind):
|
||||
def _fetch_message_from_cache(self, kind: str) -> str:
|
||||
try:
|
||||
out_message = self._message_cache[kind]
|
||||
except KeyError:
|
||||
@@ -104,7 +132,11 @@ class removed_property(property):
|
||||
self._message_cache[kind] = out_message
|
||||
return out_message
|
||||
|
||||
def __call__(self, fget, **kwargs):
|
||||
def __call__(
|
||||
self,
|
||||
fget: Callable[[Any], Any],
|
||||
**kwargs: Any,
|
||||
) -> removed_property:
|
||||
return type(self)(
|
||||
fget,
|
||||
self.fset,
|
||||
@@ -117,7 +149,7 @@ class removed_property(property):
|
||||
kwargs.get('message', self.message),
|
||||
)
|
||||
|
||||
def __delete__(self, obj):
|
||||
def __delete__(self, obj: Any) -> None:
|
||||
if self.fdel is None:
|
||||
raise AttributeError("can't delete attribute")
|
||||
out_message = self._fetch_message_from_cache('delete')
|
||||
@@ -126,17 +158,17 @@ class removed_property(property):
|
||||
)
|
||||
self.fdel(obj)
|
||||
|
||||
def __set__(self, obj, value):
|
||||
def __set__(self, instance: Any, value: Any) -> None:
|
||||
if self.fset is None:
|
||||
raise AttributeError("can't set attribute")
|
||||
out_message = self._fetch_message_from_cache('set')
|
||||
_utils.deprecation(
|
||||
out_message, stacklevel=self.stacklevel, category=self.category
|
||||
)
|
||||
self.fset(obj, value)
|
||||
self.fset(instance, value)
|
||||
|
||||
def __get__(self, obj, value):
|
||||
if obj is None:
|
||||
def __get__(self, instance: Any, owner: type | None = None) -> Any:
|
||||
if instance is None:
|
||||
return self
|
||||
if self.fget is None:
|
||||
raise AttributeError("unreadable attribute")
|
||||
@@ -144,9 +176,9 @@ class removed_property(property):
|
||||
_utils.deprecation(
|
||||
out_message, stacklevel=self.stacklevel, category=self.category
|
||||
)
|
||||
return self.fget(obj)
|
||||
return self.fget(instance)
|
||||
|
||||
def getter(self, fget):
|
||||
def getter(self, fget: Callable[[Any], Any], /) -> removed_property:
|
||||
return type(self)(
|
||||
fget,
|
||||
self.fset,
|
||||
@@ -159,7 +191,7 @@ class removed_property(property):
|
||||
self.message,
|
||||
)
|
||||
|
||||
def setter(self, fset):
|
||||
def setter(self, fset: Callable[[Any, Any], None], /) -> removed_property:
|
||||
return type(self)(
|
||||
self.fget,
|
||||
fset,
|
||||
@@ -172,7 +204,7 @@ class removed_property(property):
|
||||
self.message,
|
||||
)
|
||||
|
||||
def deleter(self, fdel):
|
||||
def deleter(self, fdel: Callable[[Any], None], /) -> removed_property:
|
||||
return type(self)(
|
||||
self.fget,
|
||||
self.fset,
|
||||
@@ -186,14 +218,36 @@ class removed_property(property):
|
||||
)
|
||||
|
||||
|
||||
@overload
|
||||
def remove(
|
||||
f=None,
|
||||
message=None,
|
||||
version=None,
|
||||
removal_version=None,
|
||||
stacklevel=3,
|
||||
category=None,
|
||||
):
|
||||
f: Callable[P, R],
|
||||
message: str | None = None,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
category: type[Warning] | None = None,
|
||||
) -> Callable[P, R]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def remove(
|
||||
f: None = None,
|
||||
message: str | None = None,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
category: type[Warning] | None = None,
|
||||
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
||||
|
||||
|
||||
def remove(
|
||||
f: Callable[P, R] | None = None,
|
||||
message: str | None = None,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
category: type[Warning] | None = None,
|
||||
) -> Callable[P, R] | Callable[[Callable[P, R]], Callable[P, R]]:
|
||||
"""Decorates a function, method, or class to emit a deprecation warning
|
||||
|
||||
Due to limitations of the wrapt library (and python) itself, if this
|
||||
@@ -223,7 +277,12 @@ def remove(
|
||||
)
|
||||
|
||||
@wrapt.decorator
|
||||
def wrapper(f, instance, args, kwargs):
|
||||
def wrapper(
|
||||
wrapped: Callable[P, R],
|
||||
instance: Any,
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any],
|
||||
) -> R:
|
||||
qualified, f_name = _utils.get_qualified_name(f)
|
||||
if qualified:
|
||||
if inspect.isclass(f):
|
||||
@@ -240,7 +299,10 @@ def remove(
|
||||
if inspect.isclass(f):
|
||||
prefix_pre = "Using class"
|
||||
thing_post = ''
|
||||
module_name = _get_qualified_name(inspect.getmodule(f))
|
||||
module = inspect.getmodule(f)
|
||||
if module is None:
|
||||
raise TypeError('Could not retrieve module for {f}')
|
||||
module_name = _get_qualified_name(module)
|
||||
if module_name == '__main__':
|
||||
f_name = _utils.get_class_name(
|
||||
f, fully_qualified=False
|
||||
@@ -250,7 +312,10 @@ def remove(
|
||||
# Decorator was a used on a function
|
||||
else:
|
||||
thing_post = '()'
|
||||
module_name = _get_qualified_name(inspect.getmodule(f))
|
||||
module = inspect.getmodule(f)
|
||||
if module is None:
|
||||
raise TypeError('Could not retrieve module for {f}')
|
||||
module_name = _get_qualified_name(module)
|
||||
if module_name != '__main__':
|
||||
f_name = _utils.get_callable_name(f)
|
||||
# Decorator was used on a classmethod or instancemethod
|
||||
@@ -277,21 +342,20 @@ def remove(
|
||||
_utils.deprecation(
|
||||
out_message, stacklevel=stacklevel, category=category
|
||||
)
|
||||
return f(*args, **kwargs)
|
||||
return wrapped(*args, **kwargs)
|
||||
|
||||
return wrapper(f)
|
||||
|
||||
|
||||
def removed_kwarg(
|
||||
old_name,
|
||||
message=None,
|
||||
version=None,
|
||||
removal_version=None,
|
||||
stacklevel=3,
|
||||
category=None,
|
||||
):
|
||||
old_name: str,
|
||||
message: str | None = None,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
category: type[Warning] | None = None,
|
||||
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
||||
"""Decorates a kwarg accepting function to deprecate a removed kwarg."""
|
||||
|
||||
prefix = f"Using the '{old_name}' argument is deprecated"
|
||||
out_message = _utils.generate_message(
|
||||
prefix,
|
||||
@@ -302,30 +366,37 @@ def removed_kwarg(
|
||||
)
|
||||
|
||||
@wrapt.decorator
|
||||
def wrapper(f, instance, args, kwargs):
|
||||
def wrapper(
|
||||
wrapped: Callable[P, R],
|
||||
instance: Any,
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any],
|
||||
) -> R:
|
||||
if old_name in kwargs:
|
||||
_utils.deprecation(
|
||||
out_message, stacklevel=stacklevel, category=category
|
||||
)
|
||||
return f(*args, **kwargs)
|
||||
return wrapped(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
# FIXME(stephenfin): Depends on [1] or similar
|
||||
# [1] https://github.com/GrahamDumpleton/wrapt/pull/306
|
||||
return wrapper # type: ignore[return-value]
|
||||
|
||||
|
||||
def removed_class(
|
||||
cls_name,
|
||||
replacement=None,
|
||||
message=None,
|
||||
version=None,
|
||||
removal_version=None,
|
||||
stacklevel=3,
|
||||
category=None,
|
||||
):
|
||||
cls_name: str,
|
||||
replacement: None = None,
|
||||
message: str | None = None,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
category: type[Warning] | None = None,
|
||||
) -> Callable[[T], T]:
|
||||
"""Decorates a class to denote that it will be removed at some point."""
|
||||
|
||||
def _wrap_it(old_init, out_message):
|
||||
def _wrap_it(old_init: Any, out_message: str) -> Any:
|
||||
@functools.wraps(old_init, assigned=_utils.get_assigned(old_init))
|
||||
def new_init(self, *args, **kwargs):
|
||||
def new_init(self: Any, *args: Any, **kwargs: Any) -> Any:
|
||||
_utils.deprecation(
|
||||
out_message, stacklevel=stacklevel, category=category
|
||||
)
|
||||
@@ -333,19 +404,17 @@ def removed_class(
|
||||
|
||||
return new_init
|
||||
|
||||
def _check_it(cls):
|
||||
def _cls_decorator(cls: T) -> T:
|
||||
if not inspect.isclass(cls):
|
||||
_qual, type_name = _utils.get_qualified_name(type(cls))
|
||||
raise TypeError(
|
||||
f"Unexpected class type '{type_name}' (expected"
|
||||
" class type only)"
|
||||
f"Unexpected class type '{type_name}' (expected "
|
||||
f"class type only)"
|
||||
)
|
||||
|
||||
def _cls_decorator(cls):
|
||||
_check_it(cls)
|
||||
out_message = _utils.generate_message(
|
||||
f"Using class '{cls_name}' (either directly or via inheritance)"
|
||||
" is deprecated",
|
||||
f"Using class '{cls_name}' (either directly or via inheritance) "
|
||||
f"is deprecated",
|
||||
postfix=None,
|
||||
message=message,
|
||||
version=version,
|
||||
@@ -358,14 +427,14 @@ def removed_class(
|
||||
|
||||
|
||||
def removed_module(
|
||||
module,
|
||||
replacement=None,
|
||||
message=None,
|
||||
version=None,
|
||||
removal_version=None,
|
||||
stacklevel=3,
|
||||
category=None,
|
||||
):
|
||||
module: types.ModuleType | str,
|
||||
replacement: str | None = None,
|
||||
message: str | None = None,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
category: type[Warning] | None = None,
|
||||
) -> None:
|
||||
"""Helper to be called inside a module to emit a deprecation warning
|
||||
|
||||
:param str replacment: A location (or information about) of any potential
|
||||
@@ -387,8 +456,8 @@ def removed_module(
|
||||
else:
|
||||
_qual, type_name = _utils.get_qualified_name(type(module))
|
||||
raise TypeError(
|
||||
f"Unexpected module type '{type_name}' (expected string or"
|
||||
" module type only)"
|
||||
f"Unexpected module type '{type_name}' (expected string or "
|
||||
f"module type only)"
|
||||
)
|
||||
prefix = f"The '{module_name}' module usage is deprecated"
|
||||
if replacement:
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import wrapt
|
||||
|
||||
from debtcollector import _utils
|
||||
@@ -20,16 +25,17 @@ _KWARG_RENAMED_POSTFIX_TPL = ", please use the '%s' argument instead"
|
||||
_KWARG_RENAMED_PREFIX_TPL = "Using the '%s' argument is deprecated"
|
||||
|
||||
|
||||
# TODO(stephenfin): Figure out typing for return values
|
||||
def renamed_kwarg(
|
||||
old_name,
|
||||
new_name,
|
||||
message=None,
|
||||
version=None,
|
||||
removal_version=None,
|
||||
stacklevel=3,
|
||||
category=None,
|
||||
replace=False,
|
||||
):
|
||||
old_name: str,
|
||||
new_name: str,
|
||||
message: str | None = None,
|
||||
version: str | None = None,
|
||||
removal_version: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
category: type[Warning] | None = None,
|
||||
replace: bool = False,
|
||||
) -> Any:
|
||||
"""Decorates a kwarg accepting function to deprecate a renamed kwarg."""
|
||||
|
||||
prefix = _KWARG_RENAMED_PREFIX_TPL % old_name
|
||||
@@ -43,7 +49,12 @@ def renamed_kwarg(
|
||||
)
|
||||
|
||||
@wrapt.decorator
|
||||
def decorator(wrapped, instance, args, kwargs):
|
||||
def decorator(
|
||||
wrapped: Callable[..., Any],
|
||||
instance: Any,
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any],
|
||||
) -> Any:
|
||||
if old_name in kwargs:
|
||||
_utils.deprecation(
|
||||
out_message, stacklevel=stacklevel, category=category
|
||||
|
||||
@@ -165,11 +165,11 @@ class ThingB:
|
||||
def green_tristars(self):
|
||||
return 'green'
|
||||
|
||||
@green_tristars.setter
|
||||
@green_tristars.setter # type: ignore[no-redef]
|
||||
def green_tristars(self, value):
|
||||
pass
|
||||
|
||||
@green_tristars.deleter
|
||||
@green_tristars.deleter # type: ignore[no-redef]
|
||||
def green_tristars(self):
|
||||
pass
|
||||
|
||||
@@ -244,7 +244,7 @@ class MovedInheritableClassTest(test_base.TestCase):
|
||||
self.assertEqual(PendingDeprecationWarning, w.category)
|
||||
|
||||
def test_existing_refer_subclass(self):
|
||||
class MyOldThing(OldHotness):
|
||||
class MyOldThing(OldHotness): # type: ignore[valid-type,misc]
|
||||
pass
|
||||
|
||||
with warnings.catch_warnings(record=True) as capture:
|
||||
@@ -512,7 +512,7 @@ class RemovalTests(test_base.TestCase):
|
||||
warnings.simplefilter("always")
|
||||
o = ThingB()
|
||||
self.assertEqual('green', o.green_tristars)
|
||||
o.green_tristars = 'b'
|
||||
o.green_tristars = 'b' # type: ignore[method-assign]
|
||||
del o.green_tristars
|
||||
self.assertEqual(3, len(capture))
|
||||
w = capture[0]
|
||||
|
||||
@@ -12,9 +12,13 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import wrapt
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from inspect import signature
|
||||
from typing import Any
|
||||
|
||||
import wrapt
|
||||
|
||||
from debtcollector import _utils
|
||||
|
||||
@@ -26,15 +30,16 @@ _KWARG_UPDATED_PREFIX_TPL = (
|
||||
)
|
||||
|
||||
|
||||
# TODO(stephenfin): Figure out typing for return values
|
||||
def updated_kwarg_default_value(
|
||||
name,
|
||||
old_value,
|
||||
new_value,
|
||||
message=None,
|
||||
version=None,
|
||||
stacklevel=3,
|
||||
category=FutureWarning,
|
||||
):
|
||||
name: str,
|
||||
old_value: str,
|
||||
new_value: str,
|
||||
message: str | None = None,
|
||||
version: str | None = None,
|
||||
stacklevel: int = 3,
|
||||
category: type[Warning] = FutureWarning,
|
||||
) -> Any:
|
||||
"""Decorates a kwarg accepting function to change the default value"""
|
||||
|
||||
prefix = _KWARG_UPDATED_PREFIX_TPL % (name, new_value)
|
||||
@@ -43,12 +48,17 @@ def updated_kwarg_default_value(
|
||||
prefix, postfix=postfix, message=message, version=version
|
||||
)
|
||||
|
||||
def decorator(f):
|
||||
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
||||
sig = signature(f)
|
||||
varnames = list(sig.parameters.keys())
|
||||
|
||||
@wrapt.decorator
|
||||
def wrapper(wrapped, instance, args, kwargs):
|
||||
def wrapper(
|
||||
wrapped: Callable[..., Any],
|
||||
instance: Any,
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any],
|
||||
) -> Any:
|
||||
explicit_params = set(varnames[: len(args)] + list(kwargs.keys()))
|
||||
allparams = set(varnames)
|
||||
default_params = set(allparams - explicit_params)
|
||||
|
||||
@@ -24,6 +24,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -35,6 +36,20 @@ packages = [
|
||||
"debtcollector"
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.10"
|
||||
show_column_numbers = true
|
||||
show_error_context = true
|
||||
strict = true
|
||||
disable_error_code = ["import-untyped"]
|
||||
exclude = "(?x)(doc | releasenotes)"
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["debtcollector.tests.*"]
|
||||
disallow_untyped_calls = false
|
||||
disallow_untyped_defs = false
|
||||
disallow_subclassing_any = false
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 79
|
||||
|
||||
|
||||
33
tox.ini
33
tox.ini
@@ -4,9 +4,9 @@ envlist = py3,pep8
|
||||
|
||||
[testenv]
|
||||
deps =
|
||||
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
-r{toxinidir}/requirements.txt
|
||||
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
-r{toxinidir}/requirements.txt
|
||||
commands =
|
||||
stestr run --slowest {posargs}
|
||||
|
||||
@@ -14,12 +14,25 @@ commands =
|
||||
commands = oslo_debug_helper {posargs}
|
||||
|
||||
[testenv:pep8]
|
||||
description =
|
||||
Run style checks.
|
||||
deps =
|
||||
pre-commit>=2.6.0 # MIT
|
||||
pre-commit
|
||||
{[testenv:docs]deps}
|
||||
{[testenv:mypy]deps}
|
||||
commands =
|
||||
pre-commit run -a
|
||||
sphinx-build -b doctest doc/source doc/build
|
||||
{[testenv:mypy]commands}
|
||||
|
||||
[testenv:mypy]
|
||||
description =
|
||||
Run type checks.
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
mypy
|
||||
commands =
|
||||
mypy --cache-dir="{envdir}/mypy_cache" {posargs:debtcollector}
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
@@ -39,12 +52,16 @@ deps =
|
||||
-r{toxinidir}/doc/requirements.txt
|
||||
commands = sphinx-build -W --keep-going -b html -d doc/build/doctrees doc/source doc/build/html
|
||||
|
||||
[testenv:releasenotes]
|
||||
deps = {[testenv:docs]deps}
|
||||
commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
|
||||
|
||||
[flake8]
|
||||
# We only enable the hacking (H) checks
|
||||
select = H
|
||||
exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build
|
||||
|
||||
[testenv:releasenotes]
|
||||
deps = {[testenv:docs]deps}
|
||||
commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
|
||||
|
||||
[hacking]
|
||||
import_exceptions =
|
||||
collections.abc
|
||||
typing
|
||||
|
||||
Reference in New Issue
Block a user