Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions pyaml/common/deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
PyAML deprecation helper.

Provides the ``deprecated`` decorator, used to mark functions, methods and
classes that are being phased out. Using a decorated object emits a
``DeprecationWarning`` pointing at the caller.
"""

import functools
import warnings
from typing import Any, Callable, TypeVar

T = TypeVar("T")


def deprecated(message: str | None = None) -> Callable[[T], T]:
"""
Mark a function, method or class as deprecated.

Each use emits a ``DeprecationWarning`` that names the deprecated object
and, if given, the custom ``message``.

Parameters
----------
message : str, optional
Extra guidance appended to the warning, e.g. the replacement to use.

Returns
-------
Callable
A decorator that returns the wrapped object unchanged in behaviour.

Examples
--------
>>> @deprecated("use new_api_function instead")
... def old_api_function():
... return 1
"""

def decorator(obj: T) -> T:
name = getattr(obj, "__qualname__", getattr(obj, "__name__", repr(obj)))
text = f"{name} is deprecated"
if message:
text = f"{text}: {message}"

if isinstance(obj, type):
original_init: Any = obj.__init__

@functools.wraps(original_init)
def init_wrapper(self, *args, **kwargs):
warnings.warn(text, DeprecationWarning, stacklevel=2)
return original_init(self, *args, **kwargs)

obj.__init__ = init_wrapper # type: ignore[misc]
return obj

@functools.wraps(obj) # type: ignore[arg-type]
def wrapper(*args: Any, **kwargs: Any) -> Any:
warnings.warn(text, DeprecationWarning, stacklevel=2)
return obj(*args, **kwargs) # type: ignore[operator]

return wrapper # type: ignore[return-value]

return decorator
64 changes: 64 additions & 0 deletions tests/common/test_deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import warnings

import pytest

from pyaml.common.deprecation import deprecated


def test_function_warns_and_returns_value():
@deprecated()
def old_func(x):
return x * 2

with pytest.warns(DeprecationWarning, match="old_func is deprecated"):
assert old_func(3) == 6


def test_custom_message_is_included():
@deprecated("use new_func instead")
def old_func():
return 1

with pytest.warns(DeprecationWarning, match="use new_func instead"):
old_func()


def test_method_warns_with_qualified_name():
class Thing:
@deprecated("use other()")
def old(self):
return "ok"

with pytest.warns(DeprecationWarning, match="Thing.old is deprecated: use other"):
assert Thing().old() == "ok"


def test_class_warns_on_instantiation():
@deprecated("use NewThing")
class OldThing:
def __init__(self, value):
self.value = value

with pytest.warns(DeprecationWarning, match="OldThing is deprecated: use NewThing"):
obj = OldThing(5)
assert obj.value == 5


def test_warning_points_at_caller():
@deprecated()
def old_func():
return None

with warnings.catch_warnings(record=True) as record:
warnings.simplefilter("always")
old_func()
assert record[0].filename == __file__


def test_wrapped_function_keeps_metadata():
@deprecated()
def documented():
"""Docstring kept."""

assert documented.__name__ == "documented"
assert documented.__doc__ == "Docstring kept."
Loading