Skip to content

Commit 0b61f2c

Browse files
authored
Merge pull request #13438 from bluetech/reorder-items-comments
Some small cleanup/comment changes
2 parents 433f3a6 + 7ac5c18 commit 0b61f2c

File tree

3 files changed

+70
-30
lines changed

3 files changed

+70
-30
lines changed

src/_pytest/fixtures.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ def get_scope_package(
132132

133133

134134
def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None:
135+
"""Get the closest parent node (including self) which matches the given
136+
scope.
137+
138+
If there is no parent node for the scope (e.g. asking for class scope on a
139+
Module, or on a Function when not defined in a class), returns None.
140+
"""
135141
import _pytest.python
136142

137143
if scope is Scope.Function:
@@ -165,22 +171,30 @@ def getfixturemarker(obj: object) -> FixtureFunctionMarker | None:
165171

166172

167173
@dataclasses.dataclass(frozen=True)
168-
class FixtureArgKey:
174+
class ParamArgKey:
175+
"""A key for a high-scoped parameter used by an item.
176+
177+
For use as a hashable key in `reorder_items`. The combination of fields
178+
is meant to uniquely identify a particular "instance" of a param,
179+
potentially shared by multiple items in a scope.
180+
"""
181+
182+
#: The param name.
169183
argname: str
170184
param_index: int
185+
#: For scopes Package, Module, Class, the path to the file (directory in
186+
#: Package's case) of the package/module/class where the item is defined.
171187
scoped_item_path: Path | None
188+
#: For Class scope, the class where the item is defined.
172189
item_cls: type | None
173190

174191

175192
_V = TypeVar("_V")
176193
OrderedSet = dict[_V, None]
177194

178195

179-
def get_parametrized_fixture_argkeys(
180-
item: nodes.Item, scope: Scope
181-
) -> Iterator[FixtureArgKey]:
182-
"""Return list of keys for all parametrized arguments which match
183-
the specified scope."""
196+
def get_param_argkeys(item: nodes.Item, scope: Scope) -> Iterator[ParamArgKey]:
197+
"""Return all ParamArgKeys for item matching the specified high scope."""
184198
assert scope is not Scope.Function
185199

186200
try:
@@ -206,19 +220,17 @@ def get_parametrized_fixture_argkeys(
206220
if callspec._arg2scope[argname] != scope:
207221
continue
208222
param_index = callspec.indices[argname]
209-
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
223+
yield ParamArgKey(argname, param_index, scoped_item_path, item_cls)
210224

211225

212226
def reorder_items(items: Sequence[nodes.Item]) -> list[nodes.Item]:
213-
argkeys_by_item: dict[Scope, dict[nodes.Item, OrderedSet[FixtureArgKey]]] = {}
214-
items_by_argkey: dict[
215-
Scope, dict[FixtureArgKey, OrderedDict[nodes.Item, None]]
216-
] = {}
227+
argkeys_by_item: dict[Scope, dict[nodes.Item, OrderedSet[ParamArgKey]]] = {}
228+
items_by_argkey: dict[Scope, dict[ParamArgKey, OrderedDict[nodes.Item, None]]] = {}
217229
for scope in HIGH_SCOPES:
218230
scoped_argkeys_by_item = argkeys_by_item[scope] = {}
219231
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict)
220232
for item in items:
221-
argkeys = dict.fromkeys(get_parametrized_fixture_argkeys(item, scope))
233+
argkeys = dict.fromkeys(get_param_argkeys(item, scope))
222234
if argkeys:
223235
scoped_argkeys_by_item[item] = argkeys
224236
for argkey in argkeys:
@@ -234,9 +246,9 @@ def reorder_items(items: Sequence[nodes.Item]) -> list[nodes.Item]:
234246

235247
def reorder_items_atscope(
236248
items: OrderedSet[nodes.Item],
237-
argkeys_by_item: Mapping[Scope, Mapping[nodes.Item, OrderedSet[FixtureArgKey]]],
249+
argkeys_by_item: Mapping[Scope, Mapping[nodes.Item, OrderedSet[ParamArgKey]]],
238250
items_by_argkey: Mapping[
239-
Scope, Mapping[FixtureArgKey, OrderedDict[nodes.Item, None]]
251+
Scope, Mapping[ParamArgKey, OrderedDict[nodes.Item, None]]
240252
],
241253
scope: Scope,
242254
) -> OrderedSet[nodes.Item]:
@@ -246,7 +258,7 @@ def reorder_items_atscope(
246258
scoped_items_by_argkey = items_by_argkey[scope]
247259
scoped_argkeys_by_item = argkeys_by_item[scope]
248260

249-
ignore: set[FixtureArgKey] = set()
261+
ignore: set[ParamArgKey] = set()
250262
items_deque = deque(items)
251263
items_done: OrderedSet[nodes.Item] = {}
252264
while items_deque:

src/_pytest/mark/structures.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,33 @@ def get_empty_parameterset_mark(
7878

7979

8080
class ParameterSet(NamedTuple):
81+
"""A set of values for a set of parameters along with associated marks and
82+
an optional ID for the set.
83+
84+
Examples::
85+
86+
pytest.param(1, 2, 3)
87+
# ParameterSet(values=(1, 2, 3), marks=(), id=None)
88+
89+
pytest.param("hello", id="greeting")
90+
# ParameterSet(values=("hello",), marks=(), id="greeting")
91+
92+
# Parameter set with marks
93+
pytest.param(42, marks=pytest.mark.xfail)
94+
# ParameterSet(values=(42,), marks=(MarkDecorator(...),), id=None)
95+
96+
# From parametrize mark (parameter names + list of parameter sets)
97+
pytest.mark.parametrize(
98+
("a", "b", "expected"),
99+
[
100+
(1, 2, 3),
101+
pytest.param(40, 2, 42, id="everything"),
102+
],
103+
)
104+
# ParameterSet(values=(1, 2, 3), marks=(), id=None)
105+
# ParameterSet(values=(2, 2, 3), marks=(), id="everything")
106+
"""
107+
81108
values: Sequence[object | NotSetType]
82109
marks: Collection[MarkDecorator | Mark]
83110
id: str | _HiddenParam | None

src/_pytest/python.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,7 @@ class CallSpec2:
10561056
params: dict[str, object] = dataclasses.field(default_factory=dict)
10571057
# arg name -> arg index.
10581058
indices: dict[str, int] = dataclasses.field(default_factory=dict)
1059+
# arg name -> parameter scope.
10591060
# Used for sorting parametrized resources.
10601061
_arg2scope: Mapping[str, Scope] = dataclasses.field(default_factory=dict)
10611062
# Parts which will be added to the item's name in `[..]` separated by "-".
@@ -1168,7 +1169,7 @@ def parametrize(
11681169
"""Add new invocations to the underlying test function using the list
11691170
of argvalues for the given argnames. Parametrization is performed
11701171
during the collection phase. If you need to setup expensive resources
1171-
see about setting indirect to do it rather than at test setup time.
1172+
see about setting ``indirect`` to do it at test setup time instead.
11721173
11731174
Can be called multiple times per test function (but only on different
11741175
argument names), in which case each call parametrizes all previous
@@ -1192,7 +1193,7 @@ def parametrize(
11921193
If N argnames were specified, argvalues must be a list of
11931194
N-tuples, where each tuple-element specifies a value for its
11941195
respective argname.
1195-
:type argvalues: Iterable[_pytest.mark.structures.ParameterSet | Sequence[object] | object]
1196+
11961197
:param indirect:
11971198
A list of arguments' names (subset of argnames) or a boolean.
11981199
If True the list contains all names from the argnames. Each
@@ -1270,15 +1271,20 @@ def parametrize(
12701271
if _param_mark and _param_mark._param_ids_from and generated_ids is None:
12711272
object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids)
12721273

1273-
# Add funcargs as fixturedefs to fixtureinfo.arg2fixturedefs by registering
1274-
# artificial "pseudo" FixtureDef's so that later at test execution time we can
1275-
# rely on a proper FixtureDef to exist for fixture setup.
1274+
# Calculate directness.
1275+
arg_directness = self._resolve_args_directness(argnames, indirect)
1276+
self._params_directness.update(arg_directness)
1277+
1278+
# Add direct parametrizations as fixturedefs to arg2fixturedefs by
1279+
# registering artificial "pseudo" FixtureDef's such that later at test
1280+
# setup time we can rely on FixtureDefs to exist for all argnames.
12761281
node = None
1277-
# If we have a scope that is higher than function, we need
1278-
# to make sure we only ever create an according fixturedef on
1279-
# a per-scope basis. We thus store and cache the fixturedef on the
1280-
# node related to the scope.
1281-
if scope_ is not Scope.Function:
1282+
# For scopes higher than function, a "pseudo" FixtureDef might have
1283+
# already been created for the scope. We thus store and cache the
1284+
# FixtureDef on the node related to the scope.
1285+
if scope_ is Scope.Function:
1286+
name2pseudofixturedef = None
1287+
else:
12821288
collector = self.definition.parent
12831289
assert collector is not None
12841290
node = get_scope_node(collector, scope_)
@@ -1294,15 +1300,10 @@ def parametrize(
12941300
node = collector.session
12951301
else:
12961302
assert False, f"Unhandled missing scope: {scope}"
1297-
if node is None:
1298-
name2pseudofixturedef = None
1299-
else:
13001303
default: dict[str, FixtureDef[Any]] = {}
13011304
name2pseudofixturedef = node.stash.setdefault(
13021305
name2pseudofixturedef_key, default
13031306
)
1304-
arg_directness = self._resolve_args_directness(argnames, indirect)
1305-
self._params_directness.update(arg_directness)
13061307
for argname in argnames:
13071308
if arg_directness[argname] == "indirect":
13081309
continue

0 commit comments

Comments
 (0)