Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/main.py: 66%
468 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
1"""Core implementation of the testing process: init, session, runtest loop."""
2import argparse
3import fnmatch
4import functools
5import importlib
6import os
7import sys
8from pathlib import Path
9from typing import Callable
10from typing import Dict
11from typing import FrozenSet
12from typing import Iterator
13from typing import List
14from typing import Optional
15from typing import Sequence
16from typing import Set
17from typing import Tuple
18from typing import Type
19from typing import TYPE_CHECKING
20from typing import Union
22import attr
24import _pytest._code
25from _pytest import nodes
26from _pytest.compat import final
27from _pytest.compat import overload
28from _pytest.config import Config
29from _pytest.config import directory_arg
30from _pytest.config import ExitCode
31from _pytest.config import hookimpl
32from _pytest.config import PytestPluginManager
33from _pytest.config import UsageError
34from _pytest.config.argparsing import Parser
35from _pytest.fixtures import FixtureManager
36from _pytest.outcomes import exit
37from _pytest.pathlib import absolutepath
38from _pytest.pathlib import bestrelpath
39from _pytest.pathlib import fnmatch_ex
40from _pytest.pathlib import visit
41from _pytest.reports import CollectReport
42from _pytest.reports import TestReport
43from _pytest.runner import collect_one_node
44from _pytest.runner import SetupState
47if TYPE_CHECKING:
48 from typing_extensions import Literal
51def 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 )
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 )
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 )
232def 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"
236 # empty path
237 if not path:
238 raise argparse.ArgumentTypeError(msg)
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
246 # check if path is an ancestor of cwd
247 if is_ancestor(Path.cwd(), Path(path).absolute()):
248 raise argparse.ArgumentTypeError(msg)
250 # check symlinks for ancestors
251 if is_ancestor(Path.cwd().resolve(), Path(path).resolve()):
252 raise argparse.ArgumentTypeError(msg)
254 return path
257def 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")
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
316def pytest_cmdline_main(config: Config) -> Union[int, ExitCode]:
317 return wrap_session(config, _main)
320def _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)
326 if session.testsfailed:
327 return ExitCode.TESTS_FAILED
328 elif session.testscollected == 0:
329 return ExitCode.NO_TESTS_COLLECTED
330 return None
333def pytest_collection(session: "Session") -> None:
334 session.perform_collect()
337def 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 )
344 if session.config.option.collectonly:
345 return True
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
357def _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())
377def 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)
386 if collection_path in ignore_paths:
387 return True
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)
397 if any(fnmatch.fnmatch(str(collection_path), str(glob)) for glob in ignore_globs):
398 return True
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
406def 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
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)
419 if deselected:
420 config.hook.pytest_deselected(items=deselected)
421 items[:] = remaining
424class FSHookProxy:
425 def __init__(self, pm: PytestPluginManager, remove_mods) -> None:
426 self.pm = pm
427 self.remove_mods = remove_mods
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
435class Interrupted(KeyboardInterrupt):
436 """Signals that the test run was interrupted."""
438 __module__ = "builtins" # For py3.
441class Failed(Exception):
442 """Signals a stop as failed test run."""
445@attr.s(slots=True, auto_attribs=True)
446class _bestrelpath_cache(Dict[Path, str]):
447 path: Path
449 def __missing__(self, path: Path) -> str:
450 r = bestrelpath(self.path, path)
451 self[path] = r
452 return r
455@final
456class 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]
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()
481 self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath)
483 self.config.pluginmanager.register(self, name="session")
485 @classmethod
486 def from_config(cls, config: Config) -> "Session":
487 session: Session = cls._create(config=config)
488 return session
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 )
499 @property
500 def startpath(self) -> Path:
501 """The path from which pytest was invoked.
503 .. versionadded:: 7.0.0
504 """
505 return self.config.invocation_params.dir
507 def _node_location_to_relpath(self, node_path: Path) -> str:
508 # bestrelpath is a quite slow function.
509 return self._bestrelpathcache[node_path]
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)
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)
528 pytest_collectreport = pytest_runtest_logreport
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
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
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
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
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 ()
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)
591 return ihook.pytest_collect_file(file_path=fspath, parent=self) # type: ignore[no-any-return]
593 @overload
594 def perform_collect(
595 self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ...
596 ) -> Sequence[nodes.Item]:
597 ...
599 @overload
600 def perform_collect( # noqa: F811
601 self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
602 ) -> Sequence[Union[nodes.Item, nodes.Collector]]:
603 ...
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.
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``.
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
624 self.trace("perform_collect", self, args)
625 self.trace.root.indent += 1
627 self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = []
628 self._initial_parts: List[Tuple[Path, List[str]]] = []
629 self.items: List[nodes.Item] = []
631 hook = self.config.hook
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}")
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))
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)
673 self.testscollected = len(items)
674 return items
676 def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
677 from _pytest.python import Package
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] = {}
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] = {}
687 # Dirnames of pkgs with dunder-init files.
688 pkg_roots: Dict[str, Package] = {}
690 for argpath, names in self._initial_parts:
691 self.trace("processing argument", (argpath, names))
692 self.trace.root.indent += 1
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
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]]
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}"
717 seen_dirs: Set[Path] = set()
718 for direntry in visit(str(argpath), self._recurse):
719 if not direntry.is_file():
720 continue
722 path = Path(direntry.path)
723 dirpath = path.parent
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
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()
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
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
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)
795 self.trace("matchnodes finished -> ", len(matching), "nodes")
796 self.trace.root.indent -= 1
798 if not matching:
799 report_arg = "::".join((str(argpath), *names))
800 self._notfound.append((report_arg, col))
801 continue
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
818 yield from matching
820 self.trace.root.indent -= 1
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)
838def 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
855def 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).
860 Command-line arguments can point to files and/or directories, and optionally contain
861 parts for specific tests selection, for example:
863 "pkg/tests/test_foo.py::TestClass::test_foo"
865 This function ensures the path exists, and returns a tuple:
867 (Path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"])
869 When as_pypath is True, expects that the command-line argument actually contains
870 module paths instead of file-system paths:
872 "pkg.tests.test_foo::TestClass::test_foo"
874 In which case we search sys.path for a matching module, and then return the *path* to the
875 found module.
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