Skip to content

Commit dd0c181

Browse files
authored
ENH: add DTConfig.pytest_extra_{ignore,skip,xfail} flags (#136)
1 parent ccec95d commit dd0c181

File tree

2 files changed

+89
-38
lines changed

2 files changed

+89
-38
lines changed

scpdt/impl.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,21 @@ class DTConfig:
6868
NameErrors. Set to True if you want to see these, or if your test
6969
is actually expected to raise NameErrors.
7070
Default is False.
71-
pytest_extra_skips : list
72-
A list of names/modules to skip when run under pytest plugin. Ignored
73-
otherwise.
74-
71+
pytest_extra_ignore : list
72+
A list of names/modules to ignore when run under pytest plugin. This is
73+
equivalent to using `--ignore=...` cmdline switch.
74+
pytest_extra_skip : dict
75+
Names/modules to skip when run under pytest plugin. This is
76+
equivalent to decorating the doctest with `@pytest.mark.skip` or adding
77+
`# doctest: + SKIP` to its examples.
78+
Each key is a doctest name to skip, and the corresponding value is
79+
a string. If not empty, the string value is used as the skip reason.
80+
pytest_extra_xfail : dict
81+
Names/modules to xfail when run under pytest plugin. This is
82+
equivalent to decorating the doctest with `@pytest.mark.xfail` or
83+
adding `# may vary` to the outputs of all examples.
84+
Each key is a doctest name to skip, and the corresponding value is
85+
a string. If not empty, the string value is used as the skip reason.
7586
"""
7687
def __init__(self, *, # DTChecker configuration
7788
default_namespace=None,
@@ -91,13 +102,14 @@ def __init__(self, *, # DTChecker configuration
91102
# Obscure switches
92103
parse_namedtuples=True, # Checker
93104
nameerror_after_exception=False, # Runner
94-
pytest_extra_skips=None, # plugin/collection
105+
# plugin
106+
pytest_extra_ignore=None,
107+
pytest_extra_skip=None,
108+
pytest_extra_xfail=None,
95109
):
96110
### DTChecker configuration ###
97111
# The namespace to run examples in
98-
if default_namespace is None:
99-
default_namespace = {}
100-
self.default_namespace = default_namespace
112+
self.default_namespace = default_namespace or {}
101113

102114
# The namespace to do checks in
103115
if check_namespace is None:
@@ -167,18 +179,16 @@ def __init__(self, *, # DTChecker configuration
167179
self.user_context_mgr = user_context_mgr
168180

169181
#### Local resources: None or dict {test: list-of-files-to-copy}
170-
if local_resources is None:
171-
local_resources = dict()
172-
self.local_resources=local_resources
182+
self.local_resources=local_resources or dict()
173183

174184
#### Obscure switches, best leave intact
175185
self.parse_namedtuples = parse_namedtuples
176186
self.nameerror_after_exception = nameerror_after_exception
177187

178188
#### pytest plugin additional switches
179-
if pytest_extra_skips is None:
180-
pytest_extra_skips = []
181-
self.pytest_extra_skips = pytest_extra_skips
189+
self.pytest_extra_ignore = pytest_extra_ignore or []
190+
self.pytest_extra_skip = pytest_extra_skip or {}
191+
self.pytest_extra_xfail = pytest_extra_xfail or {}
182192

183193

184194
def try_convert_namedtuple(got):

scpdt/plugin.py

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import warnings
88
import doctest
99

10-
from _pytest import doctest as pydoctest, outcomes
10+
11+
import pytest
12+
from _pytest import doctest as pydoctest, mark, outcomes
1113
from _pytest.doctest import DoctestModule, DoctestTextfile
1214
from _pytest.pathlib import import_path
1315

@@ -54,11 +56,50 @@ def pytest_ignore_collect(collection_path, config):
5456
if "tests" in path_str or "test_" in path_str:
5557
return True
5658

57-
for entry in config.dt_config.pytest_extra_skips:
59+
for entry in config.dt_config.pytest_extra_ignore:
5860
if entry in str(collection_path):
5961
return True
6062

6163

64+
def is_private(item):
65+
"""Decide if an DocTestItem `item` is private.
66+
67+
Private items are ignored in pytest_collect_modifyitem`.
68+
"""
69+
# Here we look at the name of a test module/object. A seemingly less
70+
# hacky alternative is to populate a set of seen `item.dtest` attributes
71+
# (which are actual DocTest objects). The issue with that is it's tricky
72+
# for explicit skips/ignores. Do we skip linalg.det or linalg._basic.det?
73+
# (collection order is not guaranteed)
74+
parent_full_name = item.parent.module.__name__
75+
is_private = "._" in parent_full_name
76+
return is_private
77+
78+
79+
def _maybe_add_markers(item, config):
80+
"""Add xfail/skip markers to `item` if DTConfig says so.
81+
82+
Modifies the item in-place.
83+
"""
84+
dt_config = config.dt_config
85+
86+
extra_skip = dt_config.pytest_extra_skip
87+
skip_it = item.name in extra_skip
88+
if item.name in extra_skip:
89+
reason = extra_skip[item.name] or ''
90+
item.add_marker(
91+
pytest.mark.skip(reason=reason)
92+
)
93+
94+
extra_xfail = dt_config.pytest_extra_xfail
95+
fail_it = item.name in extra_xfail
96+
if fail_it:
97+
reason = extra_xfail[item.name] or ''
98+
item.add_marker(
99+
pytest.mark.xfail(reason=reason)
100+
)
101+
102+
62103
def pytest_collection_modifyitems(config, items):
63104
"""
64105
This hook is executed after test collection and allows you to modify the list of collected items.
@@ -73,45 +114,45 @@ def pytest_collection_modifyitems(config, items):
73114
# XXX: The logic in this function can probably be folded into DTModule.collect.
74115
# I (E.B.) quickly tried it and it does not seem to just work. Apparently something
75116
# pytest-y runs in between DTModule.collect and this hook (should that something
76-
# be the proper home for all collection?)
117+
# be the proper home for all collection?).
118+
# Also note that DTTextfile needs _maybe_add_markers, too.
77119

78-
need_filter_unique = config.getvalue("collection_strategy") == 'api'
79-
80-
if config.getoption("--doctest-modules") and need_filter_unique:
81-
unique_items = []
120+
need_filter_unique = (
121+
config.getoption("--doctest-modules") and
122+
config.getvalue("collection_strategy") == 'api'
123+
)
82124

83-
for item in items:
84-
assert isinstance(item.parent, DTModule)
125+
unique_items = []
85126

127+
for item in items:
128+
if isinstance(item.parent, DTModule) and need_filter_unique:
86129
# objects are collected twice: from their public module + from the impl module
87130
# e.g. for `levy_stable` we have
88131
# (Pdb) p item.name, item.parent.name
89132
# ('scipy.stats.levy_stable', 'build-install/lib/python3.10/site-packages/scipy/stats/__init__.py')
133+
# and
90134
# ('scipy.stats.distributions.levy_stable', 'distributions.py')
91135
# so we filter out the second occurence
92136
#
93137
# There are two options:
94-
# - either the impl module has a leading underscore, or
95-
# - it needs to be explicitly listed in 'extra_skips' config key
138+
# - either the impl module has a leading underscore (scipy.linalg._basic), or
139+
# - it needs to be explicitly listed in the 'extra_ignore' config key (distributions.py)
96140
#
97141
# Note that the last part cannot be automated: scipy.cluster.vq is public, but
98142
# scipy.stats.distributions is not
99-
extra_skips = config.dt_config.pytest_extra_skips
100-
101-
# NB: The below looks at the name of a test module/object. A seemingly less
102-
# hacky alternative is to populate a set of seen `item.dtest` attributes
103-
# (which are actual DocTest objects). The issue with that is it's tricky
104-
# for skips. Do we skip linalg.det or linalg._basic.det? (collection order
105-
# is not guaranteed)
143+
extra_ignore = config.dt_config.pytest_extra_ignore
106144
parent_full_name = item.parent.module.__name__
107-
is_public = "._" not in parent_full_name
108-
is_duplicate = parent_full_name in extra_skips or item.name in extra_skips
145+
is_duplicate = parent_full_name in extra_ignore or item.name in extra_ignore
146+
147+
if is_duplicate or is_private(item):
148+
# ignore it
149+
continue
109150

110-
if is_public and not is_duplicate:
111-
unique_items.append(item)
151+
_maybe_add_markers(item, config)
152+
unique_items.append(item)
112153

113-
# Replace the original list of test items with the unique ones
114-
items[:] = unique_items
154+
# Replace the original list of test items with the unique ones
155+
items[:] = unique_items
115156

116157

117158
class DTModule(DoctestModule):

0 commit comments

Comments
 (0)