1"""Core implementation of the testing process: init, session, runtest loop."""

2 import argparse

3 import fnmatch

4 import functools

5 import importlib

6 import os

7 import sys

8 from pathlib import Path

9 from typing import Callable

10 from typing import Dict

11 from typing import FrozenSet

12 from typing import Iterator

13 from typing import List

14 from typing import Optional

15 from typing import Sequence

16 from typing import Set

17 from typing import Tuple

18 from typing import Type

19 from typing import TYPE_CHECKING

20 from typing import Union

21

22 import attr

23

24 import _pytest._code

25 from _pytest import nodes

26 from _pytest.compat import final

27 from _pytest.compat import overload

28 from _pytest.config import Config

29 from _pytest.config import directory_arg

30 from _pytest.config import ExitCode

31 from _pytest.config import hookimpl

32 from _pytest.config import PytestPluginManager

33 from _pytest.config import UsageError

34 from _pytest.config.argparsing import Parser

35 from _pytest.fixtures import FixtureManager

36 from _pytest.outcomes import exit

37 from _pytest.pathlib import absolutepath

38 from _pytest.pathlib import bestrelpath

39 from _pytest.pathlib import fnmatch_ex

40 from _pytest.pathlib import visit

41 from _pytest.reports import CollectReport

42 from _pytest.reports import TestReport

43 from _pytest.runner import collect_one_node

44 from _pytest.runner import SetupState

45

46

47 if TYPE_CHECKING:

48 from typing_extensions import Literal

49

50

51 def pytest_addoption(parser: Parser) -> None:

52 parser.addini(

53 "norecursedirs",

54 "Directory patterns to avoid for recursion",

55 type="args",

56 default=[

57 "*.egg",

58 ".*",

59 "_darcs",

60 "build",

61 "CVS",

62 "dist",

63 "node_modules",

64 "venv",

65 "{arch}",

66 ],

67 )

68 parser.addini(

69 "testpaths",

70 "Directories to search for tests when no files or directories are given on the "

71 "command line",

72 type="args",

73 default=[],

74 )

75 group = parser.getgroup("general", "Running and selection options")

76 group._addoption(

77 "-x",

78 "--exitfirst",

79 action="store_const",

80 dest="maxfail",

81 const=1,

82 help="Exit instantly on first error or failed test",

83 )

84 group = parser.getgroup("pytest-warnings")

85 group.addoption(

86 "-W",

87 "--pythonwarnings",

88 action="append",

89 help="Set which warnings to report, see -W option of Python itself",

90 )

91 parser.addini(

92 "filterwarnings",

93 type="linelist",

94 help="Each line specifies a pattern for "

95 "warnings.filterwarnings. "

96 "Processed after -W/--pythonwarnings.",

97 )

98 group._addoption(

99 "--maxfail",

100 metavar="num",

101 action="store",

102 type=int,

103 dest="maxfail",

104 default=0,

105 help="Exit after first num failures or errors",

106 )

107 group._addoption(

108 "--strict-config",

109 action="store_true",

110 help="Any warnings encountered while parsing the `pytest` section of the "

111 "configuration file raise errors",

112 )

113 group._addoption(

114 "--strict-markers",

115 action="store_true",

116 help="Markers not registered in the `markers` section of the configuration "

117 "file raise errors",

118 )

119 group._addoption(

120 "--strict",

121 action="store_true",

122 help="(Deprecated) alias to --strict-markers",

123 )

124 group._addoption(

125 "-c",

126 metavar="file",

127 type=str,

128 dest="inifilename",

129 help="Load configuration from `file` instead of trying to locate one of the "

130 "implicit configuration files",

131 )

132 group._addoption(

133 "--continue-on-collection-errors",

134 action="store_true",

135 default=False,

136 dest="continue_on_collection_errors",

137 help="Force test execution even if collection errors occur",

138 )

139 group._addoption(

140 "--rootdir",

141 action="store",

142 dest="rootdir",

143 help="Define root directory for tests. Can be relative path: 'root_dir', './root_dir', "

144 "'root_dir/another_dir/'; absolute path: '/home/user/root_dir'; path with variables: "

145 "'$HOME/root_dir'.",

146 )

147

148 group = parser.getgroup("collect", "collection")

149 group.addoption(

150 "--collectonly",

151 "--collect-only",

152 "--co",

153 action="store_true",

154 help="Only collect tests, don't execute them",

155 )

156 group.addoption(

157 "--pyargs",

158 action="store_true",

159 help="Try to interpret all arguments as Python packages",

160 )

161 group.addoption(

162 "--ignore",

163 action="append",

164 metavar="path",

165 help="Ignore path during collection (multi-allowed)",

166 )

167 group.addoption(

168 "--ignore-glob",

169 action="append",

170 metavar="path",

171 help="Ignore path pattern during collection (multi-allowed)",

172 )

173 group.addoption(

174 "--deselect",

175 action="append",

176 metavar="nodeid_prefix",

177 help="Deselect item (via node id prefix) during collection (multi-allowed)",

178 )

179 group.addoption(

180 "--confcutdir",

181 dest="confcutdir",

182 default=None,

183 metavar="dir",

184 type=functools.partial(directory_arg, optname="--confcutdir"),

185 help="Only load conftest.py's relative to specified dir",

186 )

187 group.addoption(

188 "--noconftest",

189 action="store_true",

190 dest="noconftest",

191 default=False,

192 help="Don't load any conftest.py files",

193 )

194 group.addoption(

195 "--keepduplicates",

196 "--keep-duplicates",

197 action="store_true",

198 dest="keepduplicates",

199 default=False,

200 help="Keep duplicate tests",

201 )

202 group.addoption(

203 "--collect-in-virtualenv",

204 action="store_true",

205 dest="collect_in_virtualenv",

206 default=False,

207 help="Don't ignore tests in a local virtualenv directory",

208 )

209 group.addoption(

210 "--import-mode",

211 default="prepend",

212 choices=["prepend", "append", "importlib"],

213 dest="importmode",

214 help="Prepend/append to sys.path when importing test modules and conftest "

215 "files. Default: prepend.",

216 )

217

218 group = parser.getgroup("debugconfig", "test session debugging and configuration")

219 group.addoption(

220 "--basetemp",

221 dest="basetemp",

222 default=None,

223 type=validate_basetemp,

224 metavar="dir",

225 help=(

226 "Base temporary directory for this test run. "

227 "(Warning: this directory is removed if it exists.)"

228 ),

229 )

230

231

232 def validate_basetemp(path: str) -> str:

233 # GH 7119

234 msg = "basetemp must not be empty, the current working directory or any parent directory of it"

235

236 # empty path

237 if not path:

238 raise argparse.ArgumentTypeError(msg)

239

240 def is_ancestor(base: Path, query: Path) -> bool:

241 """Return whether query is an ancestor of base."""

242 if base == query:

243 return True

244 return query in base.parents

245

246 # check if path is an ancestor of cwd

247 if is_ancestor(Path.cwd(), Path(path).absolute()):

248 raise argparse.ArgumentTypeError(msg)

249

250 # check symlinks for ancestors

251 if is_ancestor(Path.cwd().resolve(), Path(path).resolve()):

252 raise argparse.ArgumentTypeError(msg)

253

254 return path

255

256

257 def wrap_session(

258 config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]]

259) -> Union[int, ExitCode]:

260 """Skeleton command line program."""

261 session = Session.from_config(config)

262 session.exitstatus = ExitCode.OK

263 initstate = 0

264 try:

265 try:

266 config._do_configure()

267 initstate = 1

268 config.hook.pytest_sessionstart(session=session)

269 initstate = 2

270 session.exitstatus = doit(config, session) or 0

271 except UsageError:

272 session.exitstatus = ExitCode.USAGE_ERROR

273 raise

274 except Failed:

275 session.exitstatus = ExitCode.TESTS_FAILED

276 except (KeyboardInterrupt, exit.Exception):

277 excinfo = _pytest._code.ExceptionInfo.from_current()

278 exitstatus: Union[int, ExitCode] = ExitCode.INTERRUPTED

279 if isinstance(excinfo.value, exit.Exception):

280 if excinfo.value.returncode is not None:

281 exitstatus = excinfo.value.returncode

282 if initstate < 2:

283 sys.stderr.write(f"{excinfo.typename}: {excinfo.value.msg}\n")

284 config.hook.pytest_keyboard_interrupt(excinfo=excinfo)

285 session.exitstatus = exitstatus

286 except BaseException:

287 session.exitstatus = ExitCode.INTERNAL_ERROR

288 excinfo = _pytest._code.ExceptionInfo.from_current()

289 try:

290 config.notify_exception(excinfo, config.option)

291 except exit.Exception as exc:

292 if exc.returncode is not None:

293 session.exitstatus = exc.returncode

294 sys.stderr.write(f"{type(exc).__name__}: {exc}\n")

295 else:

296 if isinstance(excinfo.value, SystemExit):

297 sys.stderr.write("mainloop: caught unexpected SystemExit!\n")

298

299 finally:

300 # Explicitly break reference cycle.

301 excinfo = None # type: ignore

302 os.chdir(session.startpath)

303 if initstate >= 2:

304 try:

305 config.hook.pytest_sessionfinish(

306 session=session, exitstatus=session.exitstatus

307 )

308 except exit.Exception as exc:

309 if exc.returncode is not None:

310 session.exitstatus = exc.returncode

311 sys.stderr.write(f"{type(exc).__name__}: {exc}\n")

312 config._ensure_unconfigure()

313 return session.exitstatus

314

315

316 def pytest_cmdline_main(config: Config) -> Union[int, ExitCode]:

317 return wrap_session(config, _main)

318

319

320 def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]:

321 """Default command line protocol for initialization, session,

322 running tests and reporting."""

323 config.hook.pytest_collection(session=session)

324 config.hook.pytest_runtestloop(session=session)

325

326 if session.testsfailed:

327 return ExitCode.TESTS_FAILED

328 elif session.testscollected == 0:

329 return ExitCode.NO_TESTS_COLLECTED

330 return None

331

332

333 def pytest_collection(session: "Session") -> None:

334 session.perform_collect()

335

336

337 def pytest_runtestloop(session: "Session") -> bool:

338 if session.testsfailed and not session.config.option.continue_on_collection_errors:

339 raise session.Interrupted(

340 "%d error%s during collection"

341 % (session.testsfailed, "s" if session.testsfailed != 1 else "")

342 )

343

344 if session.config.option.collectonly:

345 return True

346

347 for i, item in enumerate(session.items):

348 nextitem = session.items[i + 1] if i + 1 < len(session.items) else None

349 item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)

350 if session.shouldfail:

351 raise session.Failed(session.shouldfail)

352 if session.shouldstop:

353 raise session.Interrupted(session.shouldstop)

354 return True

355

356

357 def _in_venv(path: Path) -> bool:

358 """Attempt to detect if ``path`` is the root of a Virtual Environment by

359 checking for the existence of the appropriate activate script."""

360 bindir = path.joinpath("Scripts" if sys.platform.startswith("win") else "bin")

361 try:

362 if not bindir.is_dir():

363 return False

364 except OSError:

365 return False

366 activates = (

367 "activate",

368 "activate.csh",

369 "activate.fish",

370 "Activate",

371 "Activate.bat",

372 "Activate.ps1",

373 )

374 return any(fname.name in activates for fname in bindir.iterdir())

375

376

377 def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]:

378 ignore_paths = config._getconftest_pathlist(

379 "collect_ignore", path=collection_path.parent, rootpath=config.rootpath

380 )

381 ignore_paths = ignore_paths or []

382 excludeopt = config.getoption("ignore")

383 if excludeopt:

384 ignore_paths.extend(absolutepath(x) for x in excludeopt)

385

386 if collection_path in ignore_paths:

387 return True

388

389 ignore_globs = config._getconftest_pathlist(

390 "collect_ignore_glob", path=collection_path.parent, rootpath=config.rootpath

391 )

392 ignore_globs = ignore_globs or []

393 excludeglobopt = config.getoption("ignore_glob")

394 if excludeglobopt:

395 ignore_globs.extend(absolutepath(x) for x in excludeglobopt)

396

397 if any(fnmatch.fnmatch(str(collection_path), str(glob)) for glob in ignore_globs):

398 return True

399

400 allow_in_venv = config.getoption("collect_in_virtualenv")

401 if not allow_in_venv and _in_venv(collection_path):

402 return True

403 return None

404

405

406 def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> None:

407 deselect_prefixes = tuple(config.getoption("deselect") or [])

408 if not deselect_prefixes:

409 return

410

411 remaining = []

412 deselected = []

413 for colitem in items:

414 if colitem.nodeid.startswith(deselect_prefixes):

415 deselected.append(colitem)

416 else:

417 remaining.append(colitem)

418

419 if deselected:

420 config.hook.pytest_deselected(items=deselected)

421 items[:] = remaining

422

423

424 class FSHookProxy:

425 def __init__(self, pm: PytestPluginManager, remove_mods) -> None:

426 self.pm = pm

427 self.remove_mods = remove_mods

428

429 def __getattr__(self, name: str):

430 x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)

431 self.__dict__[name] = x

432 return x

433

434

435 class Interrupted(KeyboardInterrupt):

436 """Signals that the test run was interrupted."""

437

438 __module__ = "builtins" # For py3.

439

440

441 class Failed(Exception):

442 """Signals a stop as failed test run."""

443

444

445@attr.s(slots=True, auto_attribs=True)

446 class _bestrelpath_cache(Dict[Path, str]):

447 path: Path

448

449 def __missing__(self, path: Path) -> str:

450 r = bestrelpath(self.path, path)

451 self[path] = r

452 return r

453

454

455@final

456 class Session(nodes.FSCollector):

457 Interrupted = Interrupted

458 Failed = Failed

459 # Set on the session by runner.pytest_sessionstart.

460 _setupstate: SetupState

461 # Set on the session by fixtures.pytest_sessionstart.

462 _fixturemanager: FixtureManager

463 exitstatus: Union[int, ExitCode]

464

465 def __init__(self, config: Config) -> None:

466 super().__init__(

467 path=config.rootpath,

468 fspath=None,

469 parent=None,

470 config=config,

471 session=self,

472 nodeid="",

473 )

474 self.testsfailed = 0

475 self.testscollected = 0

476 self.shouldstop: Union[bool, str] = False

477 self.shouldfail: Union[bool, str] = False

478 self.trace = config.trace.root.get("collection")

479 self._initialpaths: FrozenSet[Path] = frozenset()

480

481 self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath)

482

483 self.config.pluginmanager.register(self, name="session")

484

485 @classmethod

486 def from_config(cls, config: Config) -> "Session":

487 session: Session = cls._create(config=config)

488 return session

489

490 def __repr__(self) -> str:

491 return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % (

492 self.__class__.__name__,

493 self.name,

494 getattr(self, "exitstatus", "<UNSET>"),

495 self.testsfailed,

496 self.testscollected,

497 )

498

499 @property

500 def startpath(self) -> Path:

501 """The path from which pytest was invoked.

502

503 .. versionadded:: 7.0.0

504 """

505 return self.config.invocation_params.dir

506

507 def _node_location_to_relpath(self, node_path: Path) -> str:

508 # bestrelpath is a quite slow function.

509 return self._bestrelpathcache[node_path]

510

511 @hookimpl(tryfirst=True)

512 def pytest_collectstart(self) -> None:

513 if self.shouldfail:

514 raise self.Failed(self.shouldfail)

515 if self.shouldstop:

516 raise self.Interrupted(self.shouldstop)

517

518 @hookimpl(tryfirst=True)

519 def pytest_runtest_logreport(

520 self, report: Union[TestReport, CollectReport]

521 ) -> None:

522 if report.failed and not hasattr(report, "wasxfail"):

523 self.testsfailed += 1

524 maxfail = self.config.getvalue("maxfail")

525 if maxfail and self.testsfailed >= maxfail:

526 self.shouldfail = "stopping after %d failures" % (self.testsfailed)

527

528 pytest_collectreport = pytest_runtest_logreport

529

530 def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:

531 # Optimization: Path(Path(...)) is much slower than isinstance.

532 path_ = path if isinstance(path, Path) else Path(path)

533 return path_ in self._initialpaths

534

535 def gethookproxy(self, fspath: "os.PathLike[str]"):

536 # Optimization: Path(Path(...)) is much slower than isinstance.

537 path = fspath if isinstance(fspath, Path) else Path(fspath)

538 pm = self.config.pluginmanager

539 # Check if we have the common case of running

540 # hooks with all conftest.py files.

541 my_conftestmodules = pm._getconftestmodules(

542 path,

543 self.config.getoption("importmode"),

544 rootpath=self.config.rootpath,

545 )

546 remove_mods = pm._conftest_plugins.difference(my_conftestmodules)

547 if remove_mods:

548 # One or more conftests are not in use at this fspath.

549 from .config.compat import PathAwareHookProxy

550

551 proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods))

552 else:

553 # All plugins are active for this fspath.

554 proxy = self.config.hook

555 return proxy

556

557 def _recurse(self, direntry: "os.DirEntry[str]") -> bool:

558 if direntry.name == "__pycache__":

559 return False

560 fspath = Path(direntry.path)

561 ihook = self.gethookproxy(fspath.parent)

562 if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):

563 return False

564 norecursepatterns = self.config.getini("norecursedirs")

565 if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):

566 return False

567 return True

568

569 def _collectfile(

570 self, fspath: Path, handle_dupes: bool = True

571 ) -> Sequence[nodes.Collector]:

572 assert (

573 fspath.is_file()

574 ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(

575 fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink()

576 )

577 ihook = self.gethookproxy(fspath)

578 if not self.isinitpath(fspath):

579 if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):

580 return ()

581

582 if handle_dupes:

583 keepduplicates = self.config.getoption("keepduplicates")

584 if not keepduplicates:

585 duplicate_paths = self.config.pluginmanager._duplicatepaths

586 if fspath in duplicate_paths:

587 return ()

588 else:

589 duplicate_paths.add(fspath)

590

591 return ihook.pytest_collect_file(file_path=fspath, parent=self) # type: ignore[no-any-return]

592

593 @overload

594 def perform_collect(

595 self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ...

596 ) -> Sequence[nodes.Item]:

597 ...

598

599 @overload

600 def perform_collect( # noqa: F811

601 self, args: Optional[Sequence[str]] = ..., genitems: bool = ...

602 ) -> Sequence[Union[nodes.Item, nodes.Collector]]:

603 ...

604

605 def perform_collect( # noqa: F811

606 self, args: Optional[Sequence[str]] = None, genitems: bool = True

607 ) -> Sequence[Union[nodes.Item, nodes.Collector]]:

608 """Perform the collection phase for this session.

609

610 This is called by the default :hook:`pytest_collection` hook

611 implementation; see the documentation of this hook for more details.

612 For testing purposes, it may also be called directly on a fresh

613 ``Session``.

614

615 This function normally recursively expands any collectors collected

616 from the session to their items, and only items are returned. For

617 testing purposes, this may be suppressed by passing ``genitems=False``,

618 in which case the return value contains these collectors unexpanded,

619 and ``session.items`` is empty.

620 """

621 if args is None:

622 args = self.config.args

623

624 self.trace("perform_collect", self, args)

625 self.trace.root.indent += 1

626

627 self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = []

628 self._initial_parts: List[Tuple[Path, List[str]]] = []

629 self.items: List[nodes.Item] = []

630

631 hook = self.config.hook

632

633 items: Sequence[Union[nodes.Item, nodes.Collector]] = self.items

634 try:

635 initialpaths: List[Path] = []

636 for arg in args:

637 fspath, parts = resolve_collection_argument(

638 self.config.invocation_params.dir,

639 arg,

640 as_pypath=self.config.option.pyargs,

641 )

642 self._initial_parts.append((fspath, parts))

643 initialpaths.append(fspath)

644 self._initialpaths = frozenset(initialpaths)

645 rep = collect_one_node(self)

646 self.ihook.pytest_collectreport(report=rep)

647 self.trace.root.indent -= 1

648 if self._notfound:

649 errors = []

650 for arg, collectors in self._notfound:

651 if collectors:

652 errors.append(

653 f"not found: {arg}\n(no name {arg!r} in any of {collectors!r})"

654 )

655 else:

656 errors.append(f"found no collectors for {arg}")

657

658 raise UsageError(*errors)

659 if not genitems:

660 items = rep.result

661 else:

662 if rep.passed:

663 for node in rep.result:

664 self.items.extend(self.genitems(node))

665

666 self.config.pluginmanager.check_pending()

667 hook.pytest_collection_modifyitems(

668 session=self, config=self.config, items=items

669 )

670 finally:

671 hook.pytest_collection_finish(session=self)

672

673 self.testscollected = len(items)

674 return items

675

676 def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:

677 from _pytest.python import Package

678

679 # Keep track of any collected nodes in here, so we don't duplicate fixtures.

680 node_cache1: Dict[Path, Sequence[nodes.Collector]] = {}

681 node_cache2: Dict[Tuple[Type[nodes.Collector], Path], nodes.Collector] = {}

682

683 # Keep track of any collected collectors in matchnodes paths, so they

684 # are not collected more than once.

685 matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {}

686

687 # Dirnames of pkgs with dunder-init files.

688 pkg_roots: Dict[str, Package] = {}

689

690 for argpath, names in self._initial_parts:

691 self.trace("processing argument", (argpath, names))

692 self.trace.root.indent += 1

693

694 # Start with a Session root, and delve to argpath item (dir or file)

695 # and stack all Packages found on the way.

696 # No point in finding packages when collecting doctests.

697 if not self.config.getoption("doctestmodules", False):

698 pm = self.config.pluginmanager

699 for parent in (argpath, *argpath.parents):

700 if not pm._is_in_confcutdir(argpath):

701 break

702

703 if parent.is_dir():

704 pkginit = parent / "__init__.py"

705 if pkginit.is_file() and pkginit not in node_cache1:

706 col = self._collectfile(pkginit, handle_dupes=False)

707 if col:

708 if isinstance(col[0], Package):

709 pkg_roots[str(parent)] = col[0]

710 node_cache1[col[0].path] = [col[0]]

711

712 # If it's a directory argument, recurse and look for any Subpackages.

713 # Let the Package collector deal with subnodes, don't collect here.

714 if argpath.is_dir():

715 assert not names, f"invalid arg {(argpath, names)!r}"

716

717 seen_dirs: Set[Path] = set()

718 for direntry in visit(str(argpath), self._recurse):

719 if not direntry.is_file():

720 continue

721

722 path = Path(direntry.path)

723 dirpath = path.parent

724

725 if dirpath not in seen_dirs:

726 # Collect packages first.

727 seen_dirs.add(dirpath)

728 pkginit = dirpath / "__init__.py"

729 if pkginit.exists():

730 for x in self._collectfile(pkginit):

731 yield x

732 if isinstance(x, Package):

733 pkg_roots[str(dirpath)] = x

734 if str(dirpath) in pkg_roots:

735 # Do not collect packages here.

736 continue

737

738 for x in self._collectfile(path):

739 key2 = (type(x), x.path)

740 if key2 in node_cache2:

741 yield node_cache2[key2]

742 else:

743 node_cache2[key2] = x

744 yield x

745 else:

746 assert argpath.is_file()

747

748 if argpath in node_cache1:

749 col = node_cache1[argpath]

750 else:

751 collect_root = pkg_roots.get(str(argpath.parent), self)

752 col = collect_root._collectfile(argpath, handle_dupes=False)

753 if col:

754 node_cache1[argpath] = col

755

756 matching = []

757 work: List[

758 Tuple[Sequence[Union[nodes.Item, nodes.Collector]], Sequence[str]]

759 ] = [(col, names)]

760 while work:

761 self.trace("matchnodes", col, names)

762 self.trace.root.indent += 1

763

764 matchnodes, matchnames = work.pop()

765 for node in matchnodes:

766 if not matchnames:

767 matching.append(node)

768 continue

769 if not isinstance(node, nodes.Collector):

770 continue

771 key = (type(node), node.nodeid)

772 if key in matchnodes_cache:

773 rep = matchnodes_cache[key]

774 else:

775 rep = collect_one_node(node)

776 matchnodes_cache[key] = rep

777 if rep.passed:

778 submatchnodes = []

779 for r in rep.result:

780 # TODO: Remove parametrized workaround once collection structure contains

781 # parametrization.

782 if (

783 r.name == matchnames[0]

784 or r.name.split("[")[0] == matchnames[0]

785 ):

786 submatchnodes.append(r)

787 if submatchnodes:

788 work.append((submatchnodes, matchnames[1:]))

789 else:

790 # Report collection failures here to avoid failing to run some test

791 # specified in the command line because the module could not be

792 # imported (#134).

793 node.ihook.pytest_collectreport(report=rep)

794

795 self.trace("matchnodes finished -> ", len(matching), "nodes")

796 self.trace.root.indent -= 1

797

798 if not matching:

799 report_arg = "::".join((str(argpath), *names))

800 self._notfound.append((report_arg, col))

801 continue

802

803 # If __init__.py was the only file requested, then the matched

804 # node will be the corresponding Package (by default), and the

805 # first yielded item will be the __init__ Module itself, so

806 # just use that. If this special case isn't taken, then all the

807 # files in the package will be yielded.

808 if argpath.name == "__init__.py" and isinstance(matching[0], Package):

809 try:

810 yield next(iter(matching[0].collect()))

811 except StopIteration:

812 # The package collects nothing with only an __init__.py

813 # file in it, which gets ignored by the default

814 # "python_files" option.

815 pass

816 continue

817

818 yield from matching

819

820 self.trace.root.indent -= 1

821

822 def genitems(

823 self, node: Union[nodes.Item, nodes.Collector]

824 ) -> Iterator[nodes.Item]:

825 self.trace("genitems", node)

826 if isinstance(node, nodes.Item):

827 node.ihook.pytest_itemcollected(item=node)

828 yield node

829 else:

830 assert isinstance(node, nodes.Collector)

831 rep = collect_one_node(node)

832 if rep.passed:

833 for subnode in rep.result:

834 yield from self.genitems(subnode)

835 node.ihook.pytest_collectreport(report=rep)

836

837

838 def search_pypath(module_name: str) -> str:

839 """Search sys.path for the given a dotted module name, and return its file system path."""

840 try:

841 spec = importlib.util.find_spec(module_name)

842 # AttributeError: looks like package module, but actually filename

843 # ImportError: module does not exist

844 # ValueError: not a module name

845 except (AttributeError, ImportError, ValueError):

846 return module_name

847 if spec is None or spec.origin is None or spec.origin == "namespace":

848 return module_name

849 elif spec.submodule_search_locations:

850 return os.path.dirname(spec.origin)

851 else:

852 return spec.origin

853

854

855 def resolve_collection_argument(

856 invocation_path: Path, arg: str, *, as_pypath: bool = False

857) -> Tuple[Path, List[str]]:

858 """Parse path arguments optionally containing selection parts and return (fspath, names).

859

860 Command-line arguments can point to files and/or directories, and optionally contain

861 parts for specific tests selection, for example:

862

863 "pkg/tests/test_foo.py::TestClass::test_foo"

864

865 This function ensures the path exists, and returns a tuple:

866

867 (Path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"])

868

869 When as_pypath is True, expects that the command-line argument actually contains

870 module paths instead of file-system paths:

871

872 "pkg.tests.test_foo::TestClass::test_foo"

873

874 In which case we search sys.path for a matching module, and then return the *path* to the

875 found module.

876

877 If the path doesn't exist, raise UsageError.

878 If the path is a directory and selection parts are present, raise UsageError.

879 """

880 base, squacket, rest = str(arg).partition("[")

881 strpath, *parts = base.split("::")

882 if parts:

883 parts[-1] = f"{parts[-1]}{squacket}{rest}"

884 if as_pypath:

885 strpath = search_pypath(strpath)

886 fspath = invocation_path / strpath

887 fspath = absolutepath(fspath)

888 if not fspath.exists():

889 msg = (

890 "module or package not found: {arg} (missing __init__.py?)"

891 if as_pypath

892 else "file or directory not found: {arg}"

893 )

894 raise UsageError(msg.format(arg=arg))

895 if parts and fspath.is_dir():

896 msg = (

897 "package argument cannot contain :: selection parts: {arg}"

898 if as_pypath

899 else "directory argument cannot contain :: selection parts: {arg}"

900 )

901 raise UsageError(msg.format(arg=arg))

902 return fspath, parts