main.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913
  1. """Core implementation of the testing process: init, session, runtest loop."""
  2. import argparse
  3. import dataclasses
  4. import fnmatch
  5. import functools
  6. import importlib
  7. import os
  8. import sys
  9. from pathlib import Path
  10. from typing import Callable
  11. from typing import Dict
  12. from typing import FrozenSet
  13. from typing import Iterator
  14. from typing import List
  15. from typing import Optional
  16. from typing import Sequence
  17. from typing import Set
  18. from typing import Tuple
  19. from typing import Type
  20. from typing import TYPE_CHECKING
  21. from typing import Union
  22. import _pytest._code
  23. from _pytest import nodes
  24. from _pytest.compat import final
  25. from _pytest.compat import overload
  26. from _pytest.config import Config
  27. from _pytest.config import directory_arg
  28. from _pytest.config import ExitCode
  29. from _pytest.config import hookimpl
  30. from _pytest.config import PytestPluginManager
  31. from _pytest.config import UsageError
  32. from _pytest.config.argparsing import Parser
  33. from _pytest.fixtures import FixtureManager
  34. from _pytest.outcomes import exit
  35. from _pytest.pathlib import absolutepath
  36. from _pytest.pathlib import bestrelpath
  37. from _pytest.pathlib import fnmatch_ex
  38. from _pytest.pathlib import safe_exists
  39. from _pytest.pathlib import visit
  40. from _pytest.reports import CollectReport
  41. from _pytest.reports import TestReport
  42. from _pytest.runner import collect_one_node
  43. from _pytest.runner import SetupState
  44. if TYPE_CHECKING:
  45. from typing_extensions import Literal
  46. def pytest_addoption(parser: Parser) -> None:
  47. parser.addini(
  48. "norecursedirs",
  49. "Directory patterns to avoid for recursion",
  50. type="args",
  51. default=[
  52. "*.egg",
  53. ".*",
  54. "_darcs",
  55. "build",
  56. "CVS",
  57. "dist",
  58. "node_modules",
  59. "venv",
  60. "{arch}",
  61. ],
  62. )
  63. parser.addini(
  64. "testpaths",
  65. "Directories to search for tests when no files or directories are given on the "
  66. "command line",
  67. type="args",
  68. default=[],
  69. )
  70. group = parser.getgroup("general", "Running and selection options")
  71. group._addoption(
  72. "-x",
  73. "--exitfirst",
  74. action="store_const",
  75. dest="maxfail",
  76. const=1,
  77. help="Exit instantly on first error or failed test",
  78. )
  79. group = parser.getgroup("pytest-warnings")
  80. group.addoption(
  81. "-W",
  82. "--pythonwarnings",
  83. action="append",
  84. help="Set which warnings to report, see -W option of Python itself",
  85. )
  86. parser.addini(
  87. "filterwarnings",
  88. type="linelist",
  89. help="Each line specifies a pattern for "
  90. "warnings.filterwarnings. "
  91. "Processed after -W/--pythonwarnings.",
  92. )
  93. group._addoption(
  94. "--maxfail",
  95. metavar="num",
  96. action="store",
  97. type=int,
  98. dest="maxfail",
  99. default=0,
  100. help="Exit after first num failures or errors",
  101. )
  102. group._addoption(
  103. "--strict-config",
  104. action="store_true",
  105. help="Any warnings encountered while parsing the `pytest` section of the "
  106. "configuration file raise errors",
  107. )
  108. group._addoption(
  109. "--strict-markers",
  110. action="store_true",
  111. help="Markers not registered in the `markers` section of the configuration "
  112. "file raise errors",
  113. )
  114. group._addoption(
  115. "--strict",
  116. action="store_true",
  117. help="(Deprecated) alias to --strict-markers",
  118. )
  119. group._addoption(
  120. "-c",
  121. "--config-file",
  122. metavar="FILE",
  123. type=str,
  124. dest="inifilename",
  125. help="Load configuration from `FILE` instead of trying to locate one of the "
  126. "implicit configuration files.",
  127. )
  128. group._addoption(
  129. "--continue-on-collection-errors",
  130. action="store_true",
  131. default=False,
  132. dest="continue_on_collection_errors",
  133. help="Force test execution even if collection errors occur",
  134. )
  135. group._addoption(
  136. "--rootdir",
  137. action="store",
  138. dest="rootdir",
  139. help="Define root directory for tests. Can be relative path: 'root_dir', './root_dir', "
  140. "'root_dir/another_dir/'; absolute path: '/home/user/root_dir'; path with variables: "
  141. "'$HOME/root_dir'.",
  142. )
  143. group = parser.getgroup("collect", "collection")
  144. group.addoption(
  145. "--collectonly",
  146. "--collect-only",
  147. "--co",
  148. action="store_true",
  149. help="Only collect tests, don't execute them",
  150. )
  151. group.addoption(
  152. "--pyargs",
  153. action="store_true",
  154. help="Try to interpret all arguments as Python packages",
  155. )
  156. group.addoption(
  157. "--ignore",
  158. action="append",
  159. metavar="path",
  160. help="Ignore path during collection (multi-allowed)",
  161. )
  162. group.addoption(
  163. "--ignore-glob",
  164. action="append",
  165. metavar="path",
  166. help="Ignore path pattern during collection (multi-allowed)",
  167. )
  168. group.addoption(
  169. "--deselect",
  170. action="append",
  171. metavar="nodeid_prefix",
  172. help="Deselect item (via node id prefix) during collection (multi-allowed)",
  173. )
  174. group.addoption(
  175. "--confcutdir",
  176. dest="confcutdir",
  177. default=None,
  178. metavar="dir",
  179. type=functools.partial(directory_arg, optname="--confcutdir"),
  180. help="Only load conftest.py's relative to specified dir",
  181. )
  182. group.addoption(
  183. "--noconftest",
  184. action="store_true",
  185. dest="noconftest",
  186. default=False,
  187. help="Don't load any conftest.py files",
  188. )
  189. group.addoption(
  190. "--keepduplicates",
  191. "--keep-duplicates",
  192. action="store_true",
  193. dest="keepduplicates",
  194. default=False,
  195. help="Keep duplicate tests",
  196. )
  197. group.addoption(
  198. "--collect-in-virtualenv",
  199. action="store_true",
  200. dest="collect_in_virtualenv",
  201. default=False,
  202. help="Don't ignore tests in a local virtualenv directory",
  203. )
  204. group.addoption(
  205. "--import-mode",
  206. default="prepend",
  207. choices=["prepend", "append", "importlib"],
  208. dest="importmode",
  209. help="Prepend/append to sys.path when importing test modules and conftest "
  210. "files. Default: prepend.",
  211. )
  212. group = parser.getgroup("debugconfig", "test session debugging and configuration")
  213. group.addoption(
  214. "--basetemp",
  215. dest="basetemp",
  216. default=None,
  217. type=validate_basetemp,
  218. metavar="dir",
  219. help=(
  220. "Base temporary directory for this test run. "
  221. "(Warning: this directory is removed if it exists.)"
  222. ),
  223. )
  224. def validate_basetemp(path: str) -> str:
  225. # GH 7119
  226. msg = "basetemp must not be empty, the current working directory or any parent directory of it"
  227. # empty path
  228. if not path:
  229. raise argparse.ArgumentTypeError(msg)
  230. def is_ancestor(base: Path, query: Path) -> bool:
  231. """Return whether query is an ancestor of base."""
  232. if base == query:
  233. return True
  234. return query in base.parents
  235. # check if path is an ancestor of cwd
  236. if is_ancestor(Path.cwd(), Path(path).absolute()):
  237. raise argparse.ArgumentTypeError(msg)
  238. # check symlinks for ancestors
  239. if is_ancestor(Path.cwd().resolve(), Path(path).resolve()):
  240. raise argparse.ArgumentTypeError(msg)
  241. return path
  242. def wrap_session(
  243. config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]]
  244. ) -> Union[int, ExitCode]:
  245. """Skeleton command line program."""
  246. session = Session.from_config(config)
  247. session.exitstatus = ExitCode.OK
  248. initstate = 0
  249. try:
  250. try:
  251. config._do_configure()
  252. initstate = 1
  253. config.hook.pytest_sessionstart(session=session)
  254. initstate = 2
  255. session.exitstatus = doit(config, session) or 0
  256. except UsageError:
  257. session.exitstatus = ExitCode.USAGE_ERROR
  258. raise
  259. except Failed:
  260. session.exitstatus = ExitCode.TESTS_FAILED
  261. except (KeyboardInterrupt, exit.Exception):
  262. excinfo = _pytest._code.ExceptionInfo.from_current()
  263. exitstatus: Union[int, ExitCode] = ExitCode.INTERRUPTED
  264. if isinstance(excinfo.value, exit.Exception):
  265. if excinfo.value.returncode is not None:
  266. exitstatus = excinfo.value.returncode
  267. if initstate < 2:
  268. sys.stderr.write(f"{excinfo.typename}: {excinfo.value.msg}\n")
  269. config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
  270. session.exitstatus = exitstatus
  271. except BaseException:
  272. session.exitstatus = ExitCode.INTERNAL_ERROR
  273. excinfo = _pytest._code.ExceptionInfo.from_current()
  274. try:
  275. config.notify_exception(excinfo, config.option)
  276. except exit.Exception as exc:
  277. if exc.returncode is not None:
  278. session.exitstatus = exc.returncode
  279. sys.stderr.write(f"{type(exc).__name__}: {exc}\n")
  280. else:
  281. if isinstance(excinfo.value, SystemExit):
  282. sys.stderr.write("mainloop: caught unexpected SystemExit!\n")
  283. finally:
  284. # Explicitly break reference cycle.
  285. excinfo = None # type: ignore
  286. os.chdir(session.startpath)
  287. if initstate >= 2:
  288. try:
  289. config.hook.pytest_sessionfinish(
  290. session=session, exitstatus=session.exitstatus
  291. )
  292. except exit.Exception as exc:
  293. if exc.returncode is not None:
  294. session.exitstatus = exc.returncode
  295. sys.stderr.write(f"{type(exc).__name__}: {exc}\n")
  296. config._ensure_unconfigure()
  297. return session.exitstatus
  298. def pytest_cmdline_main(config: Config) -> Union[int, ExitCode]:
  299. return wrap_session(config, _main)
  300. def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]:
  301. """Default command line protocol for initialization, session,
  302. running tests and reporting."""
  303. config.hook.pytest_collection(session=session)
  304. config.hook.pytest_runtestloop(session=session)
  305. if session.testsfailed:
  306. return ExitCode.TESTS_FAILED
  307. elif session.testscollected == 0:
  308. return ExitCode.NO_TESTS_COLLECTED
  309. return None
  310. def pytest_collection(session: "Session") -> None:
  311. session.perform_collect()
  312. def pytest_runtestloop(session: "Session") -> bool:
  313. if session.testsfailed and not session.config.option.continue_on_collection_errors:
  314. raise session.Interrupted(
  315. "%d error%s during collection"
  316. % (session.testsfailed, "s" if session.testsfailed != 1 else "")
  317. )
  318. if session.config.option.collectonly:
  319. return True
  320. for i, item in enumerate(session.items):
  321. nextitem = session.items[i + 1] if i + 1 < len(session.items) else None
  322. item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
  323. if session.shouldfail:
  324. raise session.Failed(session.shouldfail)
  325. if session.shouldstop:
  326. raise session.Interrupted(session.shouldstop)
  327. return True
  328. def _in_venv(path: Path) -> bool:
  329. """Attempt to detect if ``path`` is the root of a Virtual Environment by
  330. checking for the existence of the appropriate activate script."""
  331. bindir = path.joinpath("Scripts" if sys.platform.startswith("win") else "bin")
  332. try:
  333. if not bindir.is_dir():
  334. return False
  335. except OSError:
  336. return False
  337. activates = (
  338. "activate",
  339. "activate.csh",
  340. "activate.fish",
  341. "Activate",
  342. "Activate.bat",
  343. "Activate.ps1",
  344. )
  345. return any(fname.name in activates for fname in bindir.iterdir())
  346. def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]:
  347. ignore_paths = config._getconftest_pathlist(
  348. "collect_ignore", path=collection_path.parent, rootpath=config.rootpath
  349. )
  350. ignore_paths = ignore_paths or []
  351. excludeopt = config.getoption("ignore")
  352. if excludeopt:
  353. ignore_paths.extend(absolutepath(x) for x in excludeopt)
  354. if collection_path in ignore_paths:
  355. return True
  356. ignore_globs = config._getconftest_pathlist(
  357. "collect_ignore_glob", path=collection_path.parent, rootpath=config.rootpath
  358. )
  359. ignore_globs = ignore_globs or []
  360. excludeglobopt = config.getoption("ignore_glob")
  361. if excludeglobopt:
  362. ignore_globs.extend(absolutepath(x) for x in excludeglobopt)
  363. if any(fnmatch.fnmatch(str(collection_path), str(glob)) for glob in ignore_globs):
  364. return True
  365. allow_in_venv = config.getoption("collect_in_virtualenv")
  366. if not allow_in_venv and _in_venv(collection_path):
  367. return True
  368. if collection_path.is_dir():
  369. norecursepatterns = config.getini("norecursedirs")
  370. if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns):
  371. return True
  372. return None
  373. def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> None:
  374. deselect_prefixes = tuple(config.getoption("deselect") or [])
  375. if not deselect_prefixes:
  376. return
  377. remaining = []
  378. deselected = []
  379. for colitem in items:
  380. if colitem.nodeid.startswith(deselect_prefixes):
  381. deselected.append(colitem)
  382. else:
  383. remaining.append(colitem)
  384. if deselected:
  385. config.hook.pytest_deselected(items=deselected)
  386. items[:] = remaining
  387. class FSHookProxy:
  388. def __init__(self, pm: PytestPluginManager, remove_mods) -> None:
  389. self.pm = pm
  390. self.remove_mods = remove_mods
  391. def __getattr__(self, name: str):
  392. x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
  393. self.__dict__[name] = x
  394. return x
  395. class Interrupted(KeyboardInterrupt):
  396. """Signals that the test run was interrupted."""
  397. __module__ = "builtins" # For py3.
  398. class Failed(Exception):
  399. """Signals a stop as failed test run."""
  400. @dataclasses.dataclass
  401. class _bestrelpath_cache(Dict[Path, str]):
  402. __slots__ = ("path",)
  403. path: Path
  404. def __missing__(self, path: Path) -> str:
  405. r = bestrelpath(self.path, path)
  406. self[path] = r
  407. return r
  408. @final
  409. class Session(nodes.FSCollector):
  410. """The root of the collection tree.
  411. ``Session`` collects the initial paths given as arguments to pytest.
  412. """
  413. Interrupted = Interrupted
  414. Failed = Failed
  415. # Set on the session by runner.pytest_sessionstart.
  416. _setupstate: SetupState
  417. # Set on the session by fixtures.pytest_sessionstart.
  418. _fixturemanager: FixtureManager
  419. exitstatus: Union[int, ExitCode]
  420. def __init__(self, config: Config) -> None:
  421. super().__init__(
  422. path=config.rootpath,
  423. fspath=None,
  424. parent=None,
  425. config=config,
  426. session=self,
  427. nodeid="",
  428. )
  429. self.testsfailed = 0
  430. self.testscollected = 0
  431. self.shouldstop: Union[bool, str] = False
  432. self.shouldfail: Union[bool, str] = False
  433. self.trace = config.trace.root.get("collection")
  434. self._initialpaths: FrozenSet[Path] = frozenset()
  435. self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath)
  436. self.config.pluginmanager.register(self, name="session")
  437. @classmethod
  438. def from_config(cls, config: Config) -> "Session":
  439. session: Session = cls._create(config=config)
  440. return session
  441. def __repr__(self) -> str:
  442. return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % (
  443. self.__class__.__name__,
  444. self.name,
  445. getattr(self, "exitstatus", "<UNSET>"),
  446. self.testsfailed,
  447. self.testscollected,
  448. )
  449. @property
  450. def startpath(self) -> Path:
  451. """The path from which pytest was invoked.
  452. .. versionadded:: 7.0.0
  453. """
  454. return self.config.invocation_params.dir
  455. def _node_location_to_relpath(self, node_path: Path) -> str:
  456. # bestrelpath is a quite slow function.
  457. return self._bestrelpathcache[node_path]
  458. @hookimpl(tryfirst=True)
  459. def pytest_collectstart(self) -> None:
  460. if self.shouldfail:
  461. raise self.Failed(self.shouldfail)
  462. if self.shouldstop:
  463. raise self.Interrupted(self.shouldstop)
  464. @hookimpl(tryfirst=True)
  465. def pytest_runtest_logreport(
  466. self, report: Union[TestReport, CollectReport]
  467. ) -> None:
  468. if report.failed and not hasattr(report, "wasxfail"):
  469. self.testsfailed += 1
  470. maxfail = self.config.getvalue("maxfail")
  471. if maxfail and self.testsfailed >= maxfail:
  472. self.shouldfail = "stopping after %d failures" % (self.testsfailed)
  473. pytest_collectreport = pytest_runtest_logreport
  474. def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
  475. # Optimization: Path(Path(...)) is much slower than isinstance.
  476. path_ = path if isinstance(path, Path) else Path(path)
  477. return path_ in self._initialpaths
  478. def gethookproxy(self, fspath: "os.PathLike[str]"):
  479. # Optimization: Path(Path(...)) is much slower than isinstance.
  480. path = fspath if isinstance(fspath, Path) else Path(fspath)
  481. pm = self.config.pluginmanager
  482. # Check if we have the common case of running
  483. # hooks with all conftest.py files.
  484. my_conftestmodules = pm._getconftestmodules(
  485. path,
  486. self.config.getoption("importmode"),
  487. rootpath=self.config.rootpath,
  488. )
  489. remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
  490. if remove_mods:
  491. # One or more conftests are not in use at this fspath.
  492. from .config.compat import PathAwareHookProxy
  493. proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods))
  494. else:
  495. # All plugins are active for this fspath.
  496. proxy = self.config.hook
  497. return proxy
  498. def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
  499. if direntry.name == "__pycache__":
  500. return False
  501. fspath = Path(direntry.path)
  502. ihook = self.gethookproxy(fspath.parent)
  503. if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
  504. return False
  505. return True
  506. def _collectfile(
  507. self, fspath: Path, handle_dupes: bool = True
  508. ) -> Sequence[nodes.Collector]:
  509. assert (
  510. fspath.is_file()
  511. ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
  512. fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink()
  513. )
  514. ihook = self.gethookproxy(fspath)
  515. if not self.isinitpath(fspath):
  516. if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
  517. return ()
  518. if handle_dupes:
  519. keepduplicates = self.config.getoption("keepduplicates")
  520. if not keepduplicates:
  521. duplicate_paths = self.config.pluginmanager._duplicatepaths
  522. if fspath in duplicate_paths:
  523. return ()
  524. else:
  525. duplicate_paths.add(fspath)
  526. return ihook.pytest_collect_file(file_path=fspath, parent=self) # type: ignore[no-any-return]
  527. @overload
  528. def perform_collect(
  529. self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ...
  530. ) -> Sequence[nodes.Item]:
  531. ...
  532. @overload
  533. def perform_collect( # noqa: F811
  534. self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
  535. ) -> Sequence[Union[nodes.Item, nodes.Collector]]:
  536. ...
  537. def perform_collect( # noqa: F811
  538. self, args: Optional[Sequence[str]] = None, genitems: bool = True
  539. ) -> Sequence[Union[nodes.Item, nodes.Collector]]:
  540. """Perform the collection phase for this session.
  541. This is called by the default :hook:`pytest_collection` hook
  542. implementation; see the documentation of this hook for more details.
  543. For testing purposes, it may also be called directly on a fresh
  544. ``Session``.
  545. This function normally recursively expands any collectors collected
  546. from the session to their items, and only items are returned. For
  547. testing purposes, this may be suppressed by passing ``genitems=False``,
  548. in which case the return value contains these collectors unexpanded,
  549. and ``session.items`` is empty.
  550. """
  551. if args is None:
  552. args = self.config.args
  553. self.trace("perform_collect", self, args)
  554. self.trace.root.indent += 1
  555. self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = []
  556. self._initial_parts: List[Tuple[Path, List[str]]] = []
  557. self.items: List[nodes.Item] = []
  558. hook = self.config.hook
  559. items: Sequence[Union[nodes.Item, nodes.Collector]] = self.items
  560. try:
  561. initialpaths: List[Path] = []
  562. for arg in args:
  563. fspath, parts = resolve_collection_argument(
  564. self.config.invocation_params.dir,
  565. arg,
  566. as_pypath=self.config.option.pyargs,
  567. )
  568. self._initial_parts.append((fspath, parts))
  569. initialpaths.append(fspath)
  570. self._initialpaths = frozenset(initialpaths)
  571. rep = collect_one_node(self)
  572. self.ihook.pytest_collectreport(report=rep)
  573. self.trace.root.indent -= 1
  574. if self._notfound:
  575. errors = []
  576. for arg, collectors in self._notfound:
  577. if collectors:
  578. errors.append(
  579. f"not found: {arg}\n(no name {arg!r} in any of {collectors!r})"
  580. )
  581. else:
  582. errors.append(f"found no collectors for {arg}")
  583. raise UsageError(*errors)
  584. if not genitems:
  585. items = rep.result
  586. else:
  587. if rep.passed:
  588. for node in rep.result:
  589. self.items.extend(self.genitems(node))
  590. self.config.pluginmanager.check_pending()
  591. hook.pytest_collection_modifyitems(
  592. session=self, config=self.config, items=items
  593. )
  594. finally:
  595. hook.pytest_collection_finish(session=self)
  596. self.testscollected = len(items)
  597. return items
  598. def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
  599. from _pytest.python import Package
  600. # Keep track of any collected nodes in here, so we don't duplicate fixtures.
  601. node_cache1: Dict[Path, Sequence[nodes.Collector]] = {}
  602. node_cache2: Dict[Tuple[Type[nodes.Collector], Path], nodes.Collector] = {}
  603. # Keep track of any collected collectors in matchnodes paths, so they
  604. # are not collected more than once.
  605. matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {}
  606. # Directories of pkgs with dunder-init files.
  607. pkg_roots: Dict[Path, Package] = {}
  608. for argpath, names in self._initial_parts:
  609. self.trace("processing argument", (argpath, names))
  610. self.trace.root.indent += 1
  611. # Start with a Session root, and delve to argpath item (dir or file)
  612. # and stack all Packages found on the way.
  613. # No point in finding packages when collecting doctests.
  614. if not self.config.getoption("doctestmodules", False):
  615. pm = self.config.pluginmanager
  616. for parent in (argpath, *argpath.parents):
  617. if not pm._is_in_confcutdir(argpath):
  618. break
  619. if parent.is_dir():
  620. pkginit = parent / "__init__.py"
  621. if pkginit.is_file() and pkginit not in node_cache1:
  622. col = self._collectfile(pkginit, handle_dupes=False)
  623. if col:
  624. if isinstance(col[0], Package):
  625. pkg_roots[parent] = col[0]
  626. node_cache1[col[0].path] = [col[0]]
  627. # If it's a directory argument, recurse and look for any Subpackages.
  628. # Let the Package collector deal with subnodes, don't collect here.
  629. if argpath.is_dir():
  630. assert not names, f"invalid arg {(argpath, names)!r}"
  631. seen_dirs: Set[Path] = set()
  632. for direntry in visit(argpath, self._recurse):
  633. if not direntry.is_file():
  634. continue
  635. path = Path(direntry.path)
  636. dirpath = path.parent
  637. if dirpath not in seen_dirs:
  638. # Collect packages first.
  639. seen_dirs.add(dirpath)
  640. pkginit = dirpath / "__init__.py"
  641. if pkginit.exists():
  642. for x in self._collectfile(pkginit):
  643. yield x
  644. if isinstance(x, Package):
  645. pkg_roots[dirpath] = x
  646. if dirpath in pkg_roots:
  647. # Do not collect packages here.
  648. continue
  649. for x in self._collectfile(path):
  650. key2 = (type(x), x.path)
  651. if key2 in node_cache2:
  652. yield node_cache2[key2]
  653. else:
  654. node_cache2[key2] = x
  655. yield x
  656. else:
  657. assert argpath.is_file()
  658. if argpath in node_cache1:
  659. col = node_cache1[argpath]
  660. else:
  661. collect_root = pkg_roots.get(argpath.parent, self)
  662. col = collect_root._collectfile(argpath, handle_dupes=False)
  663. if col:
  664. node_cache1[argpath] = col
  665. matching = []
  666. work: List[
  667. Tuple[Sequence[Union[nodes.Item, nodes.Collector]], Sequence[str]]
  668. ] = [(col, names)]
  669. while work:
  670. self.trace("matchnodes", col, names)
  671. self.trace.root.indent += 1
  672. matchnodes, matchnames = work.pop()
  673. for node in matchnodes:
  674. if not matchnames:
  675. matching.append(node)
  676. continue
  677. if not isinstance(node, nodes.Collector):
  678. continue
  679. key = (type(node), node.nodeid)
  680. if key in matchnodes_cache:
  681. rep = matchnodes_cache[key]
  682. else:
  683. rep = collect_one_node(node)
  684. matchnodes_cache[key] = rep
  685. if rep.passed:
  686. submatchnodes = []
  687. for r in rep.result:
  688. # TODO: Remove parametrized workaround once collection structure contains
  689. # parametrization.
  690. if (
  691. r.name == matchnames[0]
  692. or r.name.split("[")[0] == matchnames[0]
  693. ):
  694. submatchnodes.append(r)
  695. if submatchnodes:
  696. work.append((submatchnodes, matchnames[1:]))
  697. else:
  698. # Report collection failures here to avoid failing to run some test
  699. # specified in the command line because the module could not be
  700. # imported (#134).
  701. node.ihook.pytest_collectreport(report=rep)
  702. self.trace("matchnodes finished -> ", len(matching), "nodes")
  703. self.trace.root.indent -= 1
  704. if not matching:
  705. report_arg = "::".join((str(argpath), *names))
  706. self._notfound.append((report_arg, col))
  707. continue
  708. # If __init__.py was the only file requested, then the matched
  709. # node will be the corresponding Package (by default), and the
  710. # first yielded item will be the __init__ Module itself, so
  711. # just use that. If this special case isn't taken, then all the
  712. # files in the package will be yielded.
  713. if argpath.name == "__init__.py" and isinstance(matching[0], Package):
  714. try:
  715. yield next(iter(matching[0].collect()))
  716. except StopIteration:
  717. # The package collects nothing with only an __init__.py
  718. # file in it, which gets ignored by the default
  719. # "python_files" option.
  720. pass
  721. continue
  722. yield from matching
  723. self.trace.root.indent -= 1
  724. def genitems(
  725. self, node: Union[nodes.Item, nodes.Collector]
  726. ) -> Iterator[nodes.Item]:
  727. self.trace("genitems", node)
  728. if isinstance(node, nodes.Item):
  729. node.ihook.pytest_itemcollected(item=node)
  730. yield node
  731. else:
  732. assert isinstance(node, nodes.Collector)
  733. rep = collect_one_node(node)
  734. if rep.passed:
  735. for subnode in rep.result:
  736. yield from self.genitems(subnode)
  737. node.ihook.pytest_collectreport(report=rep)
  738. def search_pypath(module_name: str) -> str:
  739. """Search sys.path for the given a dotted module name, and return its file system path."""
  740. try:
  741. spec = importlib.util.find_spec(module_name)
  742. # AttributeError: looks like package module, but actually filename
  743. # ImportError: module does not exist
  744. # ValueError: not a module name
  745. except (AttributeError, ImportError, ValueError):
  746. return module_name
  747. if spec is None or spec.origin is None or spec.origin == "namespace":
  748. return module_name
  749. elif spec.submodule_search_locations:
  750. return os.path.dirname(spec.origin)
  751. else:
  752. return spec.origin
  753. def resolve_collection_argument(
  754. invocation_path: Path, arg: str, *, as_pypath: bool = False
  755. ) -> Tuple[Path, List[str]]:
  756. """Parse path arguments optionally containing selection parts and return (fspath, names).
  757. Command-line arguments can point to files and/or directories, and optionally contain
  758. parts for specific tests selection, for example:
  759. "pkg/tests/test_foo.py::TestClass::test_foo"
  760. This function ensures the path exists, and returns a tuple:
  761. (Path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"])
  762. When as_pypath is True, expects that the command-line argument actually contains
  763. module paths instead of file-system paths:
  764. "pkg.tests.test_foo::TestClass::test_foo"
  765. In which case we search sys.path for a matching module, and then return the *path* to the
  766. found module.
  767. If the path doesn't exist, raise UsageError.
  768. If the path is a directory and selection parts are present, raise UsageError.
  769. """
  770. base, squacket, rest = str(arg).partition("[")
  771. strpath, *parts = base.split("::")
  772. if parts:
  773. parts[-1] = f"{parts[-1]}{squacket}{rest}"
  774. if as_pypath:
  775. strpath = search_pypath(strpath)
  776. fspath = invocation_path / strpath
  777. fspath = absolutepath(fspath)
  778. if not safe_exists(fspath):
  779. msg = (
  780. "module or package not found: {arg} (missing __init__.py?)"
  781. if as_pypath
  782. else "file or directory not found: {arg}"
  783. )
  784. raise UsageError(msg.format(arg=arg))
  785. if parts and fspath.is_dir():
  786. msg = (
  787. "package argument cannot contain :: selection parts: {arg}"
  788. if as_pypath
  789. else "directory argument cannot contain :: selection parts: {arg}"
  790. )
  791. raise UsageError(msg.format(arg=arg))
  792. return fspath, parts