From 90f43a8672987cce8fe81c55fb02f19a6a2e239c Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Mon, 1 Jun 2026 12:13:47 -0400 Subject: [PATCH 1/4] Add decorator for deprecation tests --- baseclasses/testing/__init__.py | 4 +-- baseclasses/testing/decorators.py | 44 +++++++++++++++++++++++++++++++ setup.py | 1 + 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/baseclasses/testing/__init__.py b/baseclasses/testing/__init__.py index 69ebece..e75e149 100644 --- a/baseclasses/testing/__init__.py +++ b/baseclasses/testing/__init__.py @@ -1,4 +1,4 @@ from .pyRegTest import BaseRegTest, getTol -from .decorators import require_mpi +from .decorators import require_mpi, fails_at_version -__all__ = ["BaseRegTest", "getTol", "require_mpi"] +__all__ = ["BaseRegTest", "getTol", "require_mpi", "fails_at_version"] diff --git a/baseclasses/testing/decorators.py b/baseclasses/testing/decorators.py index ab15825..f69b412 100644 --- a/baseclasses/testing/decorators.py +++ b/baseclasses/testing/decorators.py @@ -1,4 +1,6 @@ import functools +from importlib.metadata import version +from packaging.version import Version import unittest from importlib.util import find_spec @@ -62,3 +64,45 @@ def skip_wrapper(*args, **kwargs): # module found, we just return the function and proceed normally else: return func + + +def fails_at_version(packageName: str, removalVersion: str): + """Decorator that fails a unittest test method once a package reaches ``removalVersion``. + + Use to self-decommission tests that covers a deprecated API. When the installed + package version reaches the stated removal version, the test raises an + ``AssertionError`` reminding the developer to delete both the deprecated code + path and the test. + + Parameters + ---------- + packageName : str + The name of the package to check. Needs to match the name you would use in a pip install command, e.g. + ``"mdolab-baseclasses"`` rather than ``"baseclasses"``. + removalVersion : str + Removal version, e.g. ``"3.14"``. Parsed with :class:`packaging.version.Version`. + + Examples + -------- + >>> class TestDeprecated(unittest.TestCase): + ... @fails_at_version("pygeo", "1.20") + ... def test_old_api_still_warns(self): ... + """ + removal = Version(removalVersion) + + def decorator(method): + @functools.wraps(method) + def wrapper(self, *args, **kwargs): + current = Version(version(packageName)) + if current >= removal: + raise AssertionError( + f"{type(self).__name__}.{method.__name__} is testing a " + f"deprecation that should be removed in {packageName} v{removal}, " + f"current version is v{current}. Please delete the " + f"deprecated API and this test." + ) + return method(self, *args, **kwargs) + + return wrapper + + return decorator diff --git a/setup.py b/setup.py index ed103ff..0f86b96 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,7 @@ python_requires=">=3.11", install_requires=[ "numpy>=1.25", + "packaging", ], extras_require={ "docs": docs_require, From 849c5b941eff31950be311afa6911bf5acc10672 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Thu, 11 Jun 2026 10:44:20 -0400 Subject: [PATCH 2/4] Change to `expire_deprecation` decorator --- baseclasses/testing/__init__.py | 4 +-- baseclasses/testing/decorators.py | 50 +++++++++++-------------------- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/baseclasses/testing/__init__.py b/baseclasses/testing/__init__.py index e75e149..638a9cb 100644 --- a/baseclasses/testing/__init__.py +++ b/baseclasses/testing/__init__.py @@ -1,4 +1,4 @@ from .pyRegTest import BaseRegTest, getTol -from .decorators import require_mpi, fails_at_version +from .decorators import require_mpi, expire_deprecation -__all__ = ["BaseRegTest", "getTol", "require_mpi", "fails_at_version"] +__all__ = ["BaseRegTest", "getTol", "require_mpi", "expire_deprecation"] diff --git a/baseclasses/testing/decorators.py b/baseclasses/testing/decorators.py index f69b412..bb7d60c 100644 --- a/baseclasses/testing/decorators.py +++ b/baseclasses/testing/decorators.py @@ -1,5 +1,7 @@ import functools from importlib.metadata import version +from typing import Optional +import warnings from packaging.version import Version import unittest from importlib.util import find_spec @@ -66,42 +68,26 @@ def skip_wrapper(*args, **kwargs): return func -def fails_at_version(packageName: str, removalVersion: str): - """Decorator that fails a unittest test method once a package reaches ``removalVersion``. +def expire_deprecation(package_name: str, removal_version: str, new_name: Optional[str] = None): + removal = Version(removal_version) + current = Version(version(package_name)) - Use to self-decommission tests that covers a deprecated API. When the installed - package version reaches the stated removal version, the test raises an - ``AssertionError`` reminding the developer to delete both the deprecated code - path and the test. + def decorator(func): + if current >= removal: + raise AssertionError( + f"{func.__qualname__}: deprecated API should have been " + f"removed in {package_name} v{removal} " + f"(current: v{current}). Please delete this shim." + ) - Parameters - ---------- - packageName : str - The name of the package to check. Needs to match the name you would use in a pip install command, e.g. - ``"mdolab-baseclasses"`` rather than ``"baseclasses"``. - removalVersion : str - Removal version, e.g. ``"3.14"``. Parsed with :class:`packaging.version.Version`. + msg = f"{func.__name__}() is deprecated and will be removed in {package_name} v{removal}." + if new_name is not None: + msg += f" Use {new_name} instead." - Examples - -------- - >>> class TestDeprecated(unittest.TestCase): - ... @fails_at_version("pygeo", "1.20") - ... def test_old_api_still_warns(self): ... - """ - removal = Version(removalVersion) - - def decorator(method): - @functools.wraps(method) + @functools.wraps(func) def wrapper(self, *args, **kwargs): - current = Version(version(packageName)) - if current >= removal: - raise AssertionError( - f"{type(self).__name__}.{method.__name__} is testing a " - f"deprecation that should be removed in {packageName} v{removal}, " - f"current version is v{current}. Please delete the " - f"deprecated API and this test." - ) - return method(self, *args, **kwargs) + warnings.warn(msg, DeprecationWarning, stacklevel=2) + return func(self, *args, **kwargs) return wrapper From 51a0c48af8651f08d79a5c9180b0c9531328a31b Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Thu, 11 Jun 2026 10:55:19 -0400 Subject: [PATCH 3/4] Add docstring --- baseclasses/testing/decorators.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/baseclasses/testing/decorators.py b/baseclasses/testing/decorators.py index bb7d60c..0138598 100644 --- a/baseclasses/testing/decorators.py +++ b/baseclasses/testing/decorators.py @@ -69,6 +69,21 @@ def skip_wrapper(*args, **kwargs): def expire_deprecation(package_name: str, removal_version: str, new_name: Optional[str] = None): + """Decorator for methods that have been deprecated and potentially replaced by a new method + + This decorator will raise a warning when the decorated method is called, and will raise an error if the current + version of the package is greater than or equal to the specified removal version, helping to ensure that deprecated + APIs are removed in a timely manner. + + Parameters + ---------- + package_name : str + _description_ + removal_version : str + _description_ + new_name : Optional[str], optional + _description_, by default None + """ removal = Version(removal_version) current = Version(version(package_name)) From a37cf0f743cbaad5b2b115710a0521c679366ac4 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Thu, 11 Jun 2026 11:07:00 -0400 Subject: [PATCH 4/4] Bump minor version --- baseclasses/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/baseclasses/__init__.py b/baseclasses/__init__.py index 5b23643..41f6d29 100644 --- a/baseclasses/__init__.py +++ b/baseclasses/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.8.5" +__version__ = "1.9.0" from .problems import ( AeroProblem,