Skip to content

Commit dd7beb3

Browse files
Merge pull request #11416 from bluetech/fixtures-getfixtureclosure
fixtures: more tweaks
2 parents e5c81fa + 6ad9499 commit dd7beb3

File tree

4 files changed

+70
-75
lines changed

4 files changed

+70
-75
lines changed

src/_pytest/doctest.py

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -255,14 +255,20 @@ def __init__(
255255
self,
256256
name: str,
257257
parent: "Union[DoctestTextfile, DoctestModule]",
258-
runner: Optional["doctest.DocTestRunner"] = None,
259-
dtest: Optional["doctest.DocTest"] = None,
258+
runner: "doctest.DocTestRunner",
259+
dtest: "doctest.DocTest",
260260
) -> None:
261261
super().__init__(name, parent)
262262
self.runner = runner
263263
self.dtest = dtest
264+
265+
# Stuff needed for fixture support.
264266
self.obj = None
265-
self.fixture_request: Optional[TopRequest] = None
267+
fm = self.session._fixturemanager
268+
fixtureinfo = fm.getfixtureinfo(node=self, func=None, cls=None)
269+
self._fixtureinfo = fixtureinfo
270+
self.fixturenames = fixtureinfo.names_closure
271+
self._initrequest()
266272

267273
@classmethod
268274
def from_parent( # type: ignore
@@ -277,19 +283,18 @@ def from_parent( # type: ignore
277283
"""The public named constructor."""
278284
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
279285

286+
def _initrequest(self) -> None:
287+
self.funcargs: Dict[str, object] = {}
288+
self._request = TopRequest(self, _ispytest=True) # type: ignore[arg-type]
289+
280290
def setup(self) -> None:
281-
if self.dtest is not None:
282-
self.fixture_request = _setup_fixtures(self)
283-
globs = dict(getfixture=self.fixture_request.getfixturevalue)
284-
for name, value in self.fixture_request.getfixturevalue(
285-
"doctest_namespace"
286-
).items():
287-
globs[name] = value
288-
self.dtest.globs.update(globs)
291+
self._request._fillfixtures()
292+
globs = dict(getfixture=self._request.getfixturevalue)
293+
for name, value in self._request.getfixturevalue("doctest_namespace").items():
294+
globs[name] = value
295+
self.dtest.globs.update(globs)
289296

290297
def runtest(self) -> None:
291-
assert self.dtest is not None
292-
assert self.runner is not None
293298
_check_all_skipped(self.dtest)
294299
self._disable_output_capturing_for_darwin()
295300
failures: List["doctest.DocTestFailure"] = []
@@ -376,7 +381,6 @@ def repr_failure( # type: ignore[override]
376381
return ReprFailDoctest(reprlocation_lines)
377382

378383
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
379-
assert self.dtest is not None
380384
return self.path, self.dtest.lineno, "[doctest] %s" % self.name
381385

382386

@@ -396,17 +400,17 @@ def _get_flag_lookup() -> Dict[str, int]:
396400
)
397401

398402

399-
def get_optionflags(parent):
400-
optionflags_str = parent.config.getini("doctest_optionflags")
403+
def get_optionflags(config: Config) -> int:
404+
optionflags_str = config.getini("doctest_optionflags")
401405
flag_lookup_table = _get_flag_lookup()
402406
flag_acc = 0
403407
for flag in optionflags_str:
404408
flag_acc |= flag_lookup_table[flag]
405409
return flag_acc
406410

407411

408-
def _get_continue_on_failure(config):
409-
continue_on_failure = config.getvalue("doctest_continue_on_failure")
412+
def _get_continue_on_failure(config: Config) -> bool:
413+
continue_on_failure: bool = config.getvalue("doctest_continue_on_failure")
410414
if continue_on_failure:
411415
# We need to turn off this if we use pdb since we should stop at
412416
# the first failure.
@@ -429,7 +433,7 @@ def collect(self) -> Iterable[DoctestItem]:
429433
name = self.path.name
430434
globs = {"__name__": "__main__"}
431435

432-
optionflags = get_optionflags(self)
436+
optionflags = get_optionflags(self.config)
433437

434438
runner = _get_runner(
435439
verbose=False,
@@ -574,7 +578,7 @@ def _from_module(self, module, object):
574578
raise
575579
# Uses internal doctest module parsing mechanism.
576580
finder = MockAwareDocTestFinder()
577-
optionflags = get_optionflags(self)
581+
optionflags = get_optionflags(self.config)
578582
runner = _get_runner(
579583
verbose=False,
580584
optionflags=optionflags,
@@ -589,24 +593,6 @@ def _from_module(self, module, object):
589593
)
590594

591595

592-
def _setup_fixtures(doctest_item: DoctestItem) -> TopRequest:
593-
"""Used by DoctestTextfile and DoctestItem to setup fixture information."""
594-
595-
def func() -> None:
596-
pass
597-
598-
doctest_item.funcargs = {} # type: ignore[attr-defined]
599-
fm = doctest_item.session._fixturemanager
600-
fixtureinfo = fm.getfixtureinfo(
601-
node=doctest_item, func=func, cls=None, funcargs=False
602-
)
603-
doctest_item._fixtureinfo = fixtureinfo # type: ignore[attr-defined]
604-
doctest_item.fixturenames = fixtureinfo.names_closure # type: ignore[attr-defined]
605-
fixture_request = TopRequest(doctest_item, _ispytest=True) # type: ignore[arg-type]
606-
fixture_request._fillfixtures()
607-
return fixture_request
608-
609-
610596
def _init_checker_class() -> Type["doctest.OutputChecker"]:
611597
import doctest
612598
import re

src/_pytest/fixtures.py

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from collections import deque
99
from contextlib import suppress
1010
from pathlib import Path
11+
from typing import AbstractSet
1112
from typing import Any
1213
from typing import Callable
1314
from typing import cast
@@ -1382,7 +1383,7 @@ def pytest_addoption(parser: Parser) -> None:
13821383
)
13831384

13841385

1385-
def _get_direct_parametrize_args(node: nodes.Node) -> List[str]:
1386+
def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]:
13861387
"""Return all direct parametrization arguments of a node, so we don't
13871388
mistake them for fixtures.
13881389
@@ -1391,17 +1392,22 @@ def _get_direct_parametrize_args(node: nodes.Node) -> List[str]:
13911392
These things are done later as well when dealing with parametrization
13921393
so this could be improved.
13931394
"""
1394-
parametrize_argnames: List[str] = []
1395+
parametrize_argnames: Set[str] = set()
13951396
for marker in node.iter_markers(name="parametrize"):
13961397
if not marker.kwargs.get("indirect", False):
13971398
p_argnames, _ = ParameterSet._parse_parametrize_args(
13981399
*marker.args, **marker.kwargs
13991400
)
1400-
parametrize_argnames.extend(p_argnames)
1401-
1401+
parametrize_argnames.update(p_argnames)
14021402
return parametrize_argnames
14031403

14041404

1405+
def deduplicate_names(*seqs: Iterable[str]) -> Tuple[str, ...]:
1406+
"""De-duplicate the sequence of names while keeping the original order."""
1407+
# Ideally we would use a set, but it does not preserve insertion order.
1408+
return tuple(dict.fromkeys(name for seq in seqs for name in seq))
1409+
1410+
14051411
class FixtureManager:
14061412
"""pytest fixture definitions and information is stored and managed
14071413
from this class.
@@ -1454,13 +1460,12 @@ def __init__(self, session: "Session") -> None:
14541460
def getfixtureinfo(
14551461
self,
14561462
node: nodes.Item,
1457-
func: Callable[..., object],
1463+
func: Optional[Callable[..., object]],
14581464
cls: Optional[type],
1459-
funcargs: bool = True,
14601465
) -> FuncFixtureInfo:
14611466
"""Calculate the :class:`FuncFixtureInfo` for an item.
14621467
1463-
If ``funcargs`` is false, or if the item sets an attribute
1468+
If ``func`` is None, or if the item sets an attribute
14641469
``nofuncargs = True``, then ``func`` is not examined at all.
14651470
14661471
:param node:
@@ -1469,21 +1474,23 @@ def getfixtureinfo(
14691474
The item's function.
14701475
:param cls:
14711476
If the function is a method, the method's class.
1472-
:param funcargs:
1473-
Whether to look into func's parameters as fixture requests.
14741477
"""
1475-
if funcargs and not getattr(node, "nofuncargs", False):
1478+
if func is not None and not getattr(node, "nofuncargs", False):
14761479
argnames = getfuncargnames(func, name=node.name, cls=cls)
14771480
else:
14781481
argnames = ()
1482+
usefixturesnames = self._getusefixturesnames(node)
1483+
autousenames = self._getautousenames(node.nodeid)
1484+
initialnames = deduplicate_names(autousenames, usefixturesnames, argnames)
14791485

1480-
usefixtures = tuple(
1481-
arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
1482-
)
1483-
initialnames = usefixtures + argnames
1484-
initialnames, names_closure, arg2fixturedefs = self.getfixtureclosure(
1485-
initialnames, node, ignore_args=_get_direct_parametrize_args(node)
1486+
direct_parametrize_args = _get_direct_parametrize_args(node)
1487+
1488+
names_closure, arg2fixturedefs = self.getfixtureclosure(
1489+
parentnode=node,
1490+
initialnames=initialnames,
1491+
ignore_args=direct_parametrize_args,
14861492
)
1493+
14871494
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
14881495

14891496
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
@@ -1515,12 +1522,17 @@ def _getautousenames(self, nodeid: str) -> Iterator[str]:
15151522
if basenames:
15161523
yield from basenames
15171524

1525+
def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]:
1526+
"""Return the names of usefixtures fixtures applicable to node."""
1527+
for mark in node.iter_markers(name="usefixtures"):
1528+
yield from mark.args
1529+
15181530
def getfixtureclosure(
15191531
self,
1520-
fixturenames: Tuple[str, ...],
15211532
parentnode: nodes.Node,
1522-
ignore_args: Sequence[str] = (),
1523-
) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
1533+
initialnames: Tuple[str, ...],
1534+
ignore_args: AbstractSet[str],
1535+
) -> Tuple[List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
15241536
# Collect the closure of all fixtures, starting with the given
15251537
# fixturenames as the initial set. As we have to visit all
15261538
# factory definitions anyway, we also return an arg2fixturedefs
@@ -1529,19 +1541,7 @@ def getfixtureclosure(
15291541
# (discovering matching fixtures for a given name/node is expensive).
15301542

15311543
parentid = parentnode.nodeid
1532-
fixturenames_closure = list(self._getautousenames(parentid))
1533-
1534-
def merge(otherlist: Iterable[str]) -> None:
1535-
for arg in otherlist:
1536-
if arg not in fixturenames_closure:
1537-
fixturenames_closure.append(arg)
1538-
1539-
merge(fixturenames)
1540-
1541-
# At this point, fixturenames_closure contains what we call "initialnames",
1542-
# which is a set of fixturenames the function immediately requests. We
1543-
# need to return it as well, so save this.
1544-
initialnames = tuple(fixturenames_closure)
1544+
fixturenames_closure = list(initialnames)
15451545

15461546
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
15471547
lastlen = -1
@@ -1555,7 +1555,9 @@ def merge(otherlist: Iterable[str]) -> None:
15551555
fixturedefs = self.getfixturedefs(argname, parentid)
15561556
if fixturedefs:
15571557
arg2fixturedefs[argname] = fixturedefs
1558-
merge(fixturedefs[-1].argnames)
1558+
for arg in fixturedefs[-1].argnames:
1559+
if arg not in fixturenames_closure:
1560+
fixturenames_closure.append(arg)
15591561

15601562
def sort_by_scope(arg_name: str) -> Scope:
15611563
try:
@@ -1566,7 +1568,7 @@ def sort_by_scope(arg_name: str) -> Scope:
15661568
return fixturedefs[-1]._scope
15671569

15681570
fixturenames_closure.sort(key=sort_by_scope, reverse=True)
1569-
return initialnames, fixturenames_closure, arg2fixturedefs
1571+
return fixturenames_closure, arg2fixturedefs
15701572

15711573
def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
15721574
"""Generate new tests based on parametrized fixtures used by the given metafunc"""

src/_pytest/python.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,9 +1800,8 @@ def __init__(
18001800
self.keywords.update(keywords)
18011801

18021802
if fixtureinfo is None:
1803-
fixtureinfo = self.session._fixturemanager.getfixtureinfo(
1804-
self, self.obj, self.cls, funcargs=True
1805-
)
1803+
fm = self.session._fixturemanager
1804+
fixtureinfo = fm.getfixtureinfo(self, self.obj, self.cls)
18061805
self._fixtureinfo: FuncFixtureInfo = fixtureinfo
18071806
self.fixturenames = fixtureinfo.names_closure
18081807
self._initrequest()

testing/python/fixtures.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pytest
77
from _pytest.compat import getfuncargnames
88
from _pytest.config import ExitCode
9+
from _pytest.fixtures import deduplicate_names
910
from _pytest.fixtures import TopRequest
1011
from _pytest.monkeypatch import MonkeyPatch
1112
from _pytest.pytester import get_public_names
@@ -4531,3 +4532,10 @@ def test_fixt(custom):
45314532
result.assert_outcomes(errors=1)
45324533
result.stdout.fnmatch_lines([expected])
45334534
assert result.ret == ExitCode.TESTS_FAILED
4535+
4536+
4537+
def test_deduplicate_names() -> None:
4538+
items = deduplicate_names("abacd")
4539+
assert items == ("a", "b", "c", "d")
4540+
items = deduplicate_names(items + ("g", "f", "g", "e", "b"))
4541+
assert items == ("a", "b", "c", "d", "g", "f", "e")

0 commit comments

Comments
 (0)