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

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 

21 

22import attr 

23 

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 

45 

46 

47if TYPE_CHECKING: 

48 from typing_extensions import Literal 

49 

50 

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 ) 

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 

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" 

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 

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") 

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 

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

317 return wrap_session(config, _main) 

318 

319 

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) 

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 

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

334 session.perform_collect() 

335 

336 

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 ) 

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 

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()) 

375 

376 

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) 

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 

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 

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 

424class 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 

435class Interrupted(KeyboardInterrupt): 

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

437 

438 __module__ = "builtins" # For py3. 

439 

440 

441class Failed(Exception): 

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

443 

444 

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

446class _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 

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] 

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 

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 

853 

854 

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). 

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