Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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

2import enum 

3import fnmatch 

4import functools 

5import importlib 

6import os 

7import sys 

8from typing import Dict 

9 

10import attr 

11import py 

12 

13import _pytest._code 

14from _pytest import nodes 

15from _pytest.config import directory_arg 

16from _pytest.config import hookimpl 

17from _pytest.config import UsageError 

18from _pytest.fixtures import FixtureManager 

19from _pytest.outcomes import exit 

20from _pytest.runner import collect_one_node 

21from _pytest.runner import SetupState 

22 

23 

24class ExitCode(enum.IntEnum): 

25 """ 

26 .. versionadded:: 5.0 

27 

28 Encodes the valid exit codes by pytest. 

29 

30 Currently users and plugins may supply other exit codes as well. 

31 """ 

32 

33 #: tests passed 

34 OK = 0 

35 #: tests failed 

36 TESTS_FAILED = 1 

37 #: pytest was interrupted 

38 INTERRUPTED = 2 

39 #: an internal error got in the way 

40 INTERNAL_ERROR = 3 

41 #: pytest was misused 

42 USAGE_ERROR = 4 

43 #: pytest couldn't find tests 

44 NO_TESTS_COLLECTED = 5 

45 

46 

47def pytest_addoption(parser): 

48 parser.addini( 

49 "norecursedirs", 

50 "directory patterns to avoid for recursion", 

51 type="args", 

52 default=[".*", "build", "dist", "CVS", "_darcs", "{arch}", "*.egg", "venv"], 

53 ) 

54 parser.addini( 

55 "testpaths", 

56 "directories to search for tests when no files or directories are given in the " 

57 "command line.", 

58 type="args", 

59 default=[], 

60 ) 

61 group = parser.getgroup("general", "running and selection options") 

62 group._addoption( 

63 "-x", 

64 "--exitfirst", 

65 action="store_const", 

66 dest="maxfail", 

67 const=1, 

68 help="exit instantly on first error or failed test.", 

69 ), 

70 group._addoption( 

71 "--maxfail", 

72 metavar="num", 

73 action="store", 

74 type=int, 

75 dest="maxfail", 

76 default=0, 

77 help="exit after first num failures or errors.", 

78 ) 

79 group._addoption( 

80 "--strict-markers", 

81 "--strict", 

82 action="store_true", 

83 help="markers not registered in the `markers` section of the configuration file raise errors.", 

84 ) 

85 group._addoption( 

86 "-c", 

87 metavar="file", 

88 type=str, 

89 dest="inifilename", 

90 help="load configuration from `file` instead of trying to locate one of the implicit " 

91 "configuration files.", 

92 ) 

93 group._addoption( 

94 "--continue-on-collection-errors", 

95 action="store_true", 

96 default=False, 

97 dest="continue_on_collection_errors", 

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

99 ) 

100 group._addoption( 

101 "--rootdir", 

102 action="store", 

103 dest="rootdir", 

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

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

106 "'$HOME/root_dir'.", 

107 ) 

108 

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

110 group.addoption( 

111 "--collectonly", 

112 "--collect-only", 

113 "--co", 

114 action="store_true", 

115 help="only collect tests, don't execute them.", 

116 ), 

117 group.addoption( 

118 "--pyargs", 

119 action="store_true", 

120 help="try to interpret all arguments as python packages.", 

121 ) 

122 group.addoption( 

123 "--ignore", 

124 action="append", 

125 metavar="path", 

126 help="ignore path during collection (multi-allowed).", 

127 ) 

128 group.addoption( 

129 "--ignore-glob", 

130 action="append", 

131 metavar="path", 

132 help="ignore path pattern during collection (multi-allowed).", 

133 ) 

134 group.addoption( 

135 "--deselect", 

136 action="append", 

137 metavar="nodeid_prefix", 

138 help="deselect item during collection (multi-allowed).", 

139 ) 

140 # when changing this to --conf-cut-dir, config.py Conftest.setinitial 

141 # needs upgrading as well 

142 group.addoption( 

143 "--confcutdir", 

144 dest="confcutdir", 

145 default=None, 

146 metavar="dir", 

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

148 help="only load conftest.py's relative to specified dir.", 

149 ) 

150 group.addoption( 

151 "--noconftest", 

152 action="store_true", 

153 dest="noconftest", 

154 default=False, 

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

156 ) 

157 group.addoption( 

158 "--keepduplicates", 

159 "--keep-duplicates", 

160 action="store_true", 

161 dest="keepduplicates", 

162 default=False, 

163 help="Keep duplicate tests.", 

164 ) 

165 group.addoption( 

166 "--collect-in-virtualenv", 

167 action="store_true", 

168 dest="collect_in_virtualenv", 

169 default=False, 

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

171 ) 

172 

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

174 group.addoption( 

175 "--basetemp", 

176 dest="basetemp", 

177 default=None, 

178 metavar="dir", 

179 help=( 

180 "base temporary directory for this test run." 

181 "(warning: this directory is removed if it exists)" 

182 ), 

183 ) 

184 

185 

186def wrap_session(config, doit): 

187 """Skeleton command line program""" 

188 session = Session(config) 

189 session.exitstatus = ExitCode.OK 

190 initstate = 0 

191 try: 

192 try: 

193 config._do_configure() 

194 initstate = 1 

195 config.hook.pytest_sessionstart(session=session) 

196 initstate = 2 

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

198 except UsageError: 

199 session.exitstatus = ExitCode.USAGE_ERROR 

200 raise 

201 except Failed: 

202 session.exitstatus = ExitCode.TESTS_FAILED 

203 except (KeyboardInterrupt, exit.Exception): 

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

205 exitstatus = ExitCode.INTERRUPTED 

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

207 if excinfo.value.returncode is not None: 

208 exitstatus = excinfo.value.returncode 

209 if initstate < 2: 

210 sys.stderr.write( 

211 "{}: {}\n".format(excinfo.typename, excinfo.value.msg) 

212 ) 

213 config.hook.pytest_keyboard_interrupt(excinfo=excinfo) 

214 session.exitstatus = exitstatus 

215 except: # noqa 

216 session.exitstatus = ExitCode.INTERNAL_ERROR 

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

218 try: 

219 config.notify_exception(excinfo, config.option) 

220 except exit.Exception as exc: 

221 if exc.returncode is not None: 

222 session.exitstatus = exc.returncode 

223 sys.stderr.write("{}: {}\n".format(type(exc).__name__, exc)) 

224 else: 

225 if excinfo.errisinstance(SystemExit): 

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

227 

228 finally: 

229 excinfo = None # Explicitly break reference cycle. 

230 session.startdir.chdir() 

231 if initstate >= 2: 

232 config.hook.pytest_sessionfinish( 

233 session=session, exitstatus=session.exitstatus 

234 ) 

235 config._ensure_unconfigure() 

236 return session.exitstatus 

237 

238 

239def pytest_cmdline_main(config): 

240 return wrap_session(config, _main) 

241 

242 

243def _main(config, session): 

244 """ default command line protocol for initialization, session, 

245 running tests and reporting. """ 

246 config.hook.pytest_collection(session=session) 

247 config.hook.pytest_runtestloop(session=session) 

248 

249 if session.testsfailed: 

250 return ExitCode.TESTS_FAILED 

251 elif session.testscollected == 0: 

252 return ExitCode.NO_TESTS_COLLECTED 

253 

254 

255def pytest_collection(session): 

256 return session.perform_collect() 

257 

258 

259def pytest_runtestloop(session): 

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

261 raise session.Interrupted( 

262 "%d error%s during collection" 

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

264 ) 

265 

266 if session.config.option.collectonly: 

267 return True 

268 

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

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

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

272 if session.shouldfail: 

273 raise session.Failed(session.shouldfail) 

274 if session.shouldstop: 

275 raise session.Interrupted(session.shouldstop) 

276 return True 

277 

278 

279def _in_venv(path): 

280 """Attempts to detect if ``path`` is the root of a Virtual Environment by 

281 checking for the existence of the appropriate activate script""" 

282 bindir = path.join("Scripts" if sys.platform.startswith("win") else "bin") 

283 if not bindir.isdir(): 

284 return False 

285 activates = ( 

286 "activate", 

287 "activate.csh", 

288 "activate.fish", 

289 "Activate", 

290 "Activate.bat", 

291 "Activate.ps1", 

292 ) 

293 return any([fname.basename in activates for fname in bindir.listdir()]) 

294 

295 

296def pytest_ignore_collect(path, config): 

297 ignore_paths = config._getconftest_pathlist("collect_ignore", path=path.dirpath()) 

298 ignore_paths = ignore_paths or [] 

299 excludeopt = config.getoption("ignore") 

300 if excludeopt: 

301 ignore_paths.extend([py.path.local(x) for x in excludeopt]) 

302 

303 if py.path.local(path) in ignore_paths: 

304 return True 

305 

306 ignore_globs = config._getconftest_pathlist( 

307 "collect_ignore_glob", path=path.dirpath() 

308 ) 

309 ignore_globs = ignore_globs or [] 

310 excludeglobopt = config.getoption("ignore_glob") 

311 if excludeglobopt: 

312 ignore_globs.extend([py.path.local(x) for x in excludeglobopt]) 

313 

314 if any(fnmatch.fnmatch(str(path), str(glob)) for glob in ignore_globs): 

315 return True 

316 

317 allow_in_venv = config.getoption("collect_in_virtualenv") 

318 if not allow_in_venv and _in_venv(path): 

319 return True 

320 

321 return False 

322 

323 

324def pytest_collection_modifyitems(items, config): 

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

326 if not deselect_prefixes: 

327 return 

328 

329 remaining = [] 

330 deselected = [] 

331 for colitem in items: 

332 if colitem.nodeid.startswith(deselect_prefixes): 

333 deselected.append(colitem) 

334 else: 

335 remaining.append(colitem) 

336 

337 if deselected: 

338 config.hook.pytest_deselected(items=deselected) 

339 items[:] = remaining 

340 

341 

342class FSHookProxy: 

343 def __init__(self, fspath, pm, remove_mods): 

344 self.fspath = fspath 

345 self.pm = pm 

346 self.remove_mods = remove_mods 

347 

348 def __getattr__(self, name): 

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

350 self.__dict__[name] = x 

351 return x 

352 

353 

354class NoMatch(Exception): 

355 """ raised if matching cannot locate a matching names. """ 

356 

357 

358class Interrupted(KeyboardInterrupt): 

359 """ signals an interrupted test run. """ 

360 

361 __module__ = "builtins" # for py3 

362 

363 

364class Failed(Exception): 

365 """ signals a stop as failed test run. """ 

366 

367 

368@attr.s 

369class _bestrelpath_cache(dict): 

370 path = attr.ib() 

371 

372 def __missing__(self, path: str) -> str: 

373 r = self.path.bestrelpath(path) # type: str 

374 self[path] = r 

375 return r 

376 

377 

378class Session(nodes.FSCollector): 

379 Interrupted = Interrupted 

380 Failed = Failed 

381 # Set on the session by runner.pytest_sessionstart. 

382 _setupstate = None # type: SetupState 

383 # Set on the session by fixtures.pytest_sessionstart. 

384 _fixturemanager = None # type: FixtureManager 

385 

386 def __init__(self, config): 

387 nodes.FSCollector.__init__( 

388 self, config.rootdir, parent=None, config=config, session=self, nodeid="" 

389 ) 

390 self.testsfailed = 0 

391 self.testscollected = 0 

392 self.shouldstop = False 

393 self.shouldfail = False 

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

395 self._norecursepatterns = config.getini("norecursedirs") 

396 self.startdir = config.invocation_dir 

397 self._initialpaths = frozenset() 

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

399 self._node_cache = {} 

400 self._bestrelpathcache = _bestrelpath_cache( 

401 config.rootdir 

402 ) # type: Dict[str, str] 

403 # Dirnames of pkgs with dunder-init files. 

404 self._pkg_roots = {} 

405 

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

407 

408 def __repr__(self): 

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

410 self.__class__.__name__, 

411 self.name, 

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

413 self.testsfailed, 

414 self.testscollected, 

415 ) 

416 

417 def _node_location_to_relpath(self, node_path: str) -> str: 

418 # bestrelpath is a quite slow function 

419 return self._bestrelpathcache[node_path] 

420 

421 @hookimpl(tryfirst=True) 

422 def pytest_collectstart(self): 

423 if self.shouldfail: 

424 raise self.Failed(self.shouldfail) 

425 if self.shouldstop: 

426 raise self.Interrupted(self.shouldstop) 

427 

428 @hookimpl(tryfirst=True) 

429 def pytest_runtest_logreport(self, report): 

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

431 self.testsfailed += 1 

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

433 if maxfail and self.testsfailed >= maxfail: 

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

435 

436 pytest_collectreport = pytest_runtest_logreport 

437 

438 def isinitpath(self, path): 

439 return path in self._initialpaths 

440 

441 def gethookproxy(self, fspath): 

442 # check if we have the common case of running 

443 # hooks with all conftest.py files 

444 pm = self.config.pluginmanager 

445 my_conftestmodules = pm._getconftestmodules(fspath) 

446 remove_mods = pm._conftest_plugins.difference(my_conftestmodules) 

447 if remove_mods: 

448 # one or more conftests are not in use at this fspath 

449 proxy = FSHookProxy(fspath, pm, remove_mods) 

450 else: 

451 # all plugins are active for this fspath 

452 proxy = self.config.hook 

453 return proxy 

454 

455 def perform_collect(self, args=None, genitems=True): 

456 hook = self.config.hook 

457 try: 

458 items = self._perform_collect(args, genitems) 

459 self.config.pluginmanager.check_pending() 

460 hook.pytest_collection_modifyitems( 

461 session=self, config=self.config, items=items 

462 ) 

463 finally: 

464 hook.pytest_collection_finish(session=self) 

465 self.testscollected = len(items) 

466 return items 

467 

468 def _perform_collect(self, args, genitems): 

469 if args is None: 

470 args = self.config.args 

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

472 self.trace.root.indent += 1 

473 self._notfound = [] 

474 initialpaths = [] 

475 self._initialparts = [] 

476 self.items = items = [] 

477 for arg in args: 

478 parts = self._parsearg(arg) 

479 self._initialparts.append(parts) 

480 initialpaths.append(parts[0]) 

481 self._initialpaths = frozenset(initialpaths) 

482 rep = collect_one_node(self) 

483 self.ihook.pytest_collectreport(report=rep) 

484 self.trace.root.indent -= 1 

485 if self._notfound: 

486 errors = [] 

487 for arg, exc in self._notfound: 

488 line = "(no name {!r} in any of {!r})".format(arg, exc.args[0]) 

489 errors.append("not found: {}\n{}".format(arg, line)) 

490 raise UsageError(*errors) 

491 if not genitems: 

492 return rep.result 

493 else: 

494 if rep.passed: 

495 for node in rep.result: 

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

497 return items 

498 

499 def collect(self): 

500 for initialpart in self._initialparts: 

501 self.trace("processing argument", initialpart) 

502 self.trace.root.indent += 1 

503 try: 

504 yield from self._collect(initialpart) 

505 except NoMatch: 

506 report_arg = "::".join(map(str, initialpart)) 

507 # we are inside a make_report hook so 

508 # we cannot directly pass through the exception 

509 self._notfound.append((report_arg, sys.exc_info()[1])) 

510 

511 self.trace.root.indent -= 1 

512 

513 def _collect(self, arg): 

514 from _pytest.python import Package 

515 

516 names = arg[:] 

517 argpath = names.pop(0) 

518 

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

520 # and stack all Packages found on the way. 

521 # No point in finding packages when collecting doctests 

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

523 pm = self.config.pluginmanager 

524 for parent in reversed(argpath.parts()): 

525 if pm._confcutdir and pm._confcutdir.relto(parent): 

526 break 

527 

528 if parent.isdir(): 

529 pkginit = parent.join("__init__.py") 

530 if pkginit.isfile(): 

531 if pkginit not in self._node_cache: 

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

533 if col: 

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

535 self._pkg_roots[parent] = col[0] 

536 # always store a list in the cache, matchnodes expects it 

537 self._node_cache[col[0].fspath] = [col[0]] 

538 

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

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

541 if argpath.check(dir=1): 

542 assert not names, "invalid arg {!r}".format(arg) 

543 

544 seen_dirs = set() 

545 for path in argpath.visit( 

546 fil=self._visit_filter, rec=self._recurse, bf=True, sort=True 

547 ): 

548 dirpath = path.dirpath() 

549 if dirpath not in seen_dirs: 

550 # Collect packages first. 

551 seen_dirs.add(dirpath) 

552 pkginit = dirpath.join("__init__.py") 

553 if pkginit.exists(): 

554 for x in self._collectfile(pkginit): 

555 yield x 

556 if isinstance(x, Package): 

557 self._pkg_roots[dirpath] = x 

558 if dirpath in self._pkg_roots: 

559 # Do not collect packages here. 

560 continue 

561 

562 for x in self._collectfile(path): 

563 key = (type(x), x.fspath) 

564 if key in self._node_cache: 

565 yield self._node_cache[key] 

566 else: 

567 self._node_cache[key] = x 

568 yield x 

569 else: 

570 assert argpath.check(file=1) 

571 

572 if argpath in self._node_cache: 

573 col = self._node_cache[argpath] 

574 else: 

575 collect_root = self._pkg_roots.get(argpath.dirname, self) 

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

577 if col: 

578 self._node_cache[argpath] = col 

579 m = self.matchnodes(col, names) 

580 # If __init__.py was the only file requested, then the matched node will be 

581 # the corresponding Package, and the first yielded item will be the __init__ 

582 # Module itself, so just use that. If this special case isn't taken, then all 

583 # the files in the package will be yielded. 

584 if argpath.basename == "__init__.py": 

585 try: 

586 yield next(m[0].collect()) 

587 except StopIteration: 

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

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

590 # "python_files" option. 

591 pass 

592 return 

593 yield from m 

594 

595 def _collectfile(self, path, handle_dupes=True): 

596 assert ( 

597 path.isfile() 

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

599 path, path.isdir(), path.exists(), path.islink() 

600 ) 

601 ihook = self.gethookproxy(path) 

602 if not self.isinitpath(path): 

603 if ihook.pytest_ignore_collect(path=path, config=self.config): 

604 return () 

605 

606 if handle_dupes: 

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

608 if not keepduplicates: 

609 duplicate_paths = self.config.pluginmanager._duplicatepaths 

610 if path in duplicate_paths: 

611 return () 

612 else: 

613 duplicate_paths.add(path) 

614 

615 return ihook.pytest_collect_file(path=path, parent=self) 

616 

617 def _recurse(self, dirpath): 

618 if dirpath.basename == "__pycache__": 

619 return False 

620 ihook = self.gethookproxy(dirpath.dirpath()) 

621 if ihook.pytest_ignore_collect(path=dirpath, config=self.config): 

622 return False 

623 for pat in self._norecursepatterns: 

624 if dirpath.check(fnmatch=pat): 

625 return False 

626 ihook = self.gethookproxy(dirpath) 

627 ihook.pytest_collect_directory(path=dirpath, parent=self) 

628 return True 

629 

630 @staticmethod 

631 def _visit_filter(f): 

632 return f.check(file=1) 

633 

634 def _tryconvertpyarg(self, x): 

635 """Convert a dotted module name to path.""" 

636 try: 

637 spec = importlib.util.find_spec(x) 

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

639 # ImportError: module does not exist 

640 # ValueError: not a module name 

641 except (AttributeError, ImportError, ValueError): 

642 return x 

643 if spec is None or spec.origin in {None, "namespace"}: 

644 return x 

645 elif spec.submodule_search_locations: 

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

647 else: 

648 return spec.origin 

649 

650 def _parsearg(self, arg): 

651 """ return (fspath, names) tuple after checking the file exists. """ 

652 parts = str(arg).split("::") 

653 if self.config.option.pyargs: 

654 parts[0] = self._tryconvertpyarg(parts[0]) 

655 relpath = parts[0].replace("/", os.sep) 

656 path = self.config.invocation_dir.join(relpath, abs=True) 

657 if not path.check(): 

658 if self.config.option.pyargs: 

659 raise UsageError( 

660 "file or package not found: " + arg + " (missing __init__.py?)" 

661 ) 

662 raise UsageError("file not found: " + arg) 

663 parts[0] = path.realpath() 

664 return parts 

665 

666 def matchnodes(self, matching, names): 

667 self.trace("matchnodes", matching, names) 

668 self.trace.root.indent += 1 

669 nodes = self._matchnodes(matching, names) 

670 num = len(nodes) 

671 self.trace("matchnodes finished -> ", num, "nodes") 

672 self.trace.root.indent -= 1 

673 if num == 0: 

674 raise NoMatch(matching, names[:1]) 

675 return nodes 

676 

677 def _matchnodes(self, matching, names): 

678 if not matching or not names: 

679 return matching 

680 name = names[0] 

681 assert name 

682 nextnames = names[1:] 

683 resultnodes = [] 

684 for node in matching: 

685 if isinstance(node, nodes.Item): 

686 if not names: 

687 resultnodes.append(node) 

688 continue 

689 assert isinstance(node, nodes.Collector) 

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

691 if key in self._node_cache: 

692 rep = self._node_cache[key] 

693 else: 

694 rep = collect_one_node(node) 

695 self._node_cache[key] = rep 

696 if rep.passed: 

697 has_matched = False 

698 for x in rep.result: 

699 # TODO: remove parametrized workaround once collection structure contains parametrization 

700 if x.name == name or x.name.split("[")[0] == name: 

701 resultnodes.extend(self.matchnodes([x], nextnames)) 

702 has_matched = True 

703 # XXX accept IDs that don't have "()" for class instances 

704 if not has_matched and len(rep.result) == 1 and x.name == "()": 

705 nextnames.insert(0, name) 

706 resultnodes.extend(self.matchnodes([x], nextnames)) 

707 else: 

708 # report collection failures here to avoid failing to run some test 

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

710 # imported (#134) 

711 node.ihook.pytest_collectreport(report=rep) 

712 return resultnodes 

713 

714 def genitems(self, node): 

715 self.trace("genitems", node) 

716 if isinstance(node, nodes.Item): 

717 node.ihook.pytest_itemcollected(item=node) 

718 yield node 

719 else: 

720 assert isinstance(node, nodes.Collector) 

721 rep = collect_one_node(node) 

722 if rep.passed: 

723 for subnode in rep.result: 

724 yield from self.genitems(subnode) 

725 node.ihook.pytest_collectreport(report=rep)