Skip to content

Commit b8d9622

Browse files
committed
Improve .util.minimum_version()
- Allow the pytest.MarkDecorator to attach itself to the subject function. - Handle AssertionError by default. - Add raises=… parameter to allow specifying additional exception classes.
1 parent 3d016c3 commit b8d9622

File tree

1 file changed

+33
-25
lines changed

1 file changed

+33
-25
lines changed

message_ix_models/util/__init__.py

+33-25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import logging
22
from collections import ChainMap, defaultdict
3-
from collections.abc import Callable, Collection, Mapping, MutableMapping, Sequence
3+
from collections.abc import (
4+
Callable,
5+
Collection,
6+
Iterable,
7+
Mapping,
8+
MutableMapping,
9+
Sequence,
10+
)
411
from datetime import datetime
512
from functools import partial, singledispatch, update_wrapper
613
from importlib.metadata import version
@@ -547,16 +554,19 @@ def merge_data(base: "MutableParameterData", *others: "ParameterData") -> None:
547554
base[par] = pd.concat([base.get(par, None), df])
548555

549556

550-
def minimum_version(expr: str) -> Callable:
557+
def minimum_version(
558+
expr: str, raises: Optional[Iterable[type[Exception]]] = None
559+
) -> Callable:
551560
"""Decorator for functions that require a minimum version of some upstream package.
552561
553562
If the decorated function is called and the condition in `expr` is not met,
554563
:class:`.NotImplementedError` is raised with an informative message.
555564
556-
The decorated function gains an attribute :py:`.minimum_version`, another decorator
557-
that can be used on associated test code. This marks the test as XFAIL, raising
558-
:class:`.NotImplementedError` or :class:`.RuntimeError` (e.g. for :mod:`.click`
559-
testing).
565+
The decorated function gains an attribute :py:`.minimum_version`, a pytest
566+
MarkDecorator that can be used on associated test code. This marks the test as
567+
XFAIL, raising :class:`.NotImplementedError` (directly); :class:`.RuntimeError` or
568+
:class:`.AssertionError` (for instance, via :mod:`.click` test utilities), or any
569+
of the classes given in the `raises` argument.
560570
561571
See :func:`.prepare_reporter` / :func:`.test_prepare_reporter` for a usage example.
562572
@@ -586,28 +596,26 @@ def wrapper(*args, **kwargs):
586596

587597
update_wrapper(wrapper, func)
588598

589-
# Create a test function decorator
590-
def marker(test_func):
591-
# Import pytest only when there is a test function to mark
599+
try:
592600
import pytest
593601

594-
# Create the mark
595-
mark = pytest.mark.xfail(
596-
condition=condition,
597-
raises=(NotImplementedError, RuntimeError),
598-
reason=f"Not supported{message}",
602+
# Create a MarkDecorator and store as an attribute of "wrapper"
603+
setattr(
604+
wrapper,
605+
"minimum_version",
606+
pytest.mark.xfail(
607+
condition=condition,
608+
raises=(
609+
NotImplementedError, # Raised directly, above
610+
AssertionError, # e.g. through CliRunner.assert_exit_0()
611+
RuntimeError, # e.g. through genno.Computer
612+
)
613+
+ tuple(raises or ()), # Other exception classes
614+
reason=f"Not supported{message}",
615+
),
599616
)
600-
601-
# Attach to the test function
602-
try:
603-
test_func.pytestmark.append(mark)
604-
except AttributeError:
605-
test_func.pytestmark = [mark]
606-
607-
return test_func
608-
609-
# Store the decorator on the wrapped function
610-
setattr(wrapper, "minimum_version", marker)
617+
except ImportError:
618+
pass # Pytest not present; testing is not happening
611619

612620
return wrapper
613621

0 commit comments

Comments
 (0)