diff --git a/src/pyrecest/deprecation.py b/src/pyrecest/deprecation.py index 69400f3ecd..1075bd1897 100644 --- a/src/pyrecest/deprecation.py +++ b/src/pyrecest/deprecation.py @@ -3,6 +3,7 @@ from __future__ import annotations import functools +import inspect import warnings from collections.abc import Callable from typing import ParamSpec, TypeVar @@ -48,6 +49,18 @@ def decorator(func: Callable[P, R]) -> Callable[P, R]: if replacement: message += f" Use {replacement} instead." + if inspect.iscoroutinefunction(func): + + @functools.wraps(func) + async def async_wrapper(*args: P.args, **kwargs: P.kwargs): + warnings.warn(message, DeprecationWarning, stacklevel=2) + return await func(*args, **kwargs) + + async_wrapper.__deprecated_since__ = since # type: ignore[attr-defined] + async_wrapper.__deprecated_remove_in__ = remove_in # type: ignore[attr-defined] + async_wrapper.__deprecated_replacement__ = replacement # type: ignore[attr-defined] + return async_wrapper # type: ignore[return-value] + @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: warnings.warn(message, DeprecationWarning, stacklevel=2) diff --git a/tests/test_deprecation_helper.py b/tests/test_deprecation_helper.py index 7624e6e810..d1a8222517 100644 --- a/tests/test_deprecation_helper.py +++ b/tests/test_deprecation_helper.py @@ -1,3 +1,5 @@ +import asyncio +import inspect import warnings import pytest @@ -18,6 +20,21 @@ def legacy_function(): assert "new_function" in str(caught[0].message) +def test_deprecated_decorator_preserves_async_function_contract(): + @deprecated(since="2.3.0", remove_in="3.0.0", replacement="new_async_function") + async def legacy_async_function(value): + return value + 1 + + assert inspect.iscoroutinefunction(legacy_async_function) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + assert asyncio.run(legacy_async_function(1)) == 2 + + assert len(caught) == 1 + assert issubclass(caught[0].category, DeprecationWarning) + assert "new_async_function" in str(caught[0].message) + + def test_deprecated_decorator_rejects_blank_since(): with pytest.raises(ValueError, match="since must be a non-empty string"): deprecated(since=" ", remove_in="3.0.0")