Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
10 changes: 9 additions & 1 deletion scipy_doctest/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class DTConfig:
Default is False.
pytest_extra_ignore : list
A list of names/modules to ignore when run under pytest plugin. This is
equivalent to using `--ignore=...` cmdline switch.
equivalent to using ``--ignore=...`` cmdline switch.
pytest_extra_skip : dict
Names/modules to skip when run under pytest plugin. This is
equivalent to decorating the doctest with `@pytest.mark.skip` or adding
Expand All @@ -92,6 +92,12 @@ class DTConfig:
adding `# may vary` to the outputs of all examples.
Each key is a doctest name to skip, and the corresponding value is
a string. If not empty, the string value is used as the skip reason.
pytest_extra_requires : dict
Paths or functions to conditionally ignore unless requirements are met.
The format is ``{path/or/glob/pattern: requirement, full.func.name: requiremet}``,
where the values are PEP 508 dependency specifiers. If a requirement is not met,
the behavior is equivalent to using the ``--ignore=...`` command line switch for
paths, and to using a `pytest_extra_skip` for function names.
CheckerKlass : object, optional
The class for the Checker object. Must mimic the ``DTChecker`` API:
subclass the `doctest.OutputChecker` and make the constructor signature
Expand Down Expand Up @@ -125,6 +131,7 @@ def __init__(self, *, # DTChecker configuration
pytest_extra_ignore=None,
pytest_extra_skip=None,
pytest_extra_xfail=None,
pytest_extra_requires=None,
):
### DTChecker configuration ###
self.CheckerKlass = CheckerKlass or DTChecker
Expand Down Expand Up @@ -217,6 +224,7 @@ def __init__(self, *, # DTChecker configuration
self.pytest_extra_ignore = pytest_extra_ignore or []
self.pytest_extra_skip = pytest_extra_skip or {}
self.pytest_extra_xfail = pytest_extra_xfail or {}
self.pytest_extra_requires = pytest_extra_requires or {}


def try_convert_namedtuple(got):
Expand Down
17 changes: 15 additions & 2 deletions scipy_doctest/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from .impl import DTParser, DebugDTRunner
from .conftest import dt_config
from .util import np_errstate, matplotlib_make_nongui, temp_cwd
from .util import np_errstate, matplotlib_make_nongui, temp_cwd, is_req_satisfied
from .frontend import find_doctests


Expand Down Expand Up @@ -82,10 +82,16 @@ def pytest_ignore_collect(collection_path, config):
if "tests" in path_str or "test_" in path_str:
return True

fnmatch_ex = _pytest.pathlib.fnmatch_ex

for entry in config.dt_config.pytest_extra_ignore:
if entry in str(collection_path):
if fnmatch_ex(entry, collection_path):
return True

for entry, req_str in config.dt_config.pytest_extra_requires.items():
if fnmatch_ex(entry, collection_path):
return not is_req_satisfied(req_str)


def is_private(item):
"""Decide if an DocTestItem `item` is private.
Expand Down Expand Up @@ -125,6 +131,13 @@ def _maybe_add_markers(item, config):
pytest.mark.xfail(reason=reason)
)

extra_requires = dt_config.pytest_extra_requires
if req_str := extra_requires.get(item.name, None):
if not is_req_satisfied(req_str):
item.add_marker(
pytest.mark.skip(reason=f"requires {req_str}")
)


def pytest_collection_modifyitems(config, items):
"""
Expand Down
12 changes: 12 additions & 0 deletions scipy_doctest/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import inspect
from contextlib import contextmanager

from importlib.metadata import version as get_version, PackageNotFoundError
from packaging.requirements import Requirement

@contextmanager
def matplotlib_make_nongui():
Expand Down Expand Up @@ -255,6 +257,16 @@ def get_public_objects(module, skiplist=None):
return (items, names), failures


def is_req_satisfied(req_str):
""" Check if a PEP 508-compliant requirement is satisfied or not.
"""
req = Requirement(req_str)
try:
return get_version(req.name) in req.specifier
except PackageNotFoundError:
return False


# XXX: not used ATM
modules = []
def generate_log(module, test):
Expand Down