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:
Stephen Finucane
2023-12-22 11:19:53 +00:00
parent 775fa84515
commit 7abbf7a443
12 changed files with 358 additions and 190 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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
View File

View 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:

View File

@@ -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

View File

@@ -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]

View File

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

View File

@@ -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
View File

@@ -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