1"""Command line options, ini-file and conftest.py processing."""

2 import argparse

3 import collections.abc

4 import copy

5 import enum

6 import glob

7 import inspect

8 import os

9 import re

10 import shlex

11 import sys

12 import types

13 import warnings

14 from functools import lru_cache

15 from pathlib import Path

16 from textwrap import dedent

17 from types import FunctionType

18 from types import TracebackType

19 from typing import Any

20 from typing import Callable

21 from typing import cast

22 from typing import Dict

23 from typing import Generator

24 from typing import IO

25 from typing import Iterable

26 from typing import Iterator

27 from typing import List

28 from typing import Optional

29 from typing import Sequence

30 from typing import Set

31 from typing import TextIO

32 from typing import Tuple

33 from typing import Type

34 from typing import TYPE_CHECKING

35 from typing import Union

36

37 import attr

38 from pluggy import HookimplMarker

39 from pluggy import HookspecMarker

40 from pluggy import PluginManager

41

42 import _pytest._code

43 import _pytest.deprecated

44 import _pytest.hookspec

45 from .exceptions import PrintHelp as PrintHelp

46 from .exceptions import UsageError as UsageError

47 from .findpaths import determine_setup

48 from _pytest._code import ExceptionInfo

49 from _pytest._code import filter_traceback

50 from _pytest._io import TerminalWriter

51 from _pytest.compat import final

52 from _pytest.compat import importlib_metadata

53 from _pytest.outcomes import fail

54 from _pytest.outcomes import Skipped

55 from _pytest.pathlib import absolutepath

56 from _pytest.pathlib import bestrelpath

57 from _pytest.pathlib import import_path

58 from _pytest.pathlib import ImportMode

59 from _pytest.pathlib import resolve_package_path

60 from _pytest.stash import Stash

61 from _pytest.warning_types import PytestConfigWarning

62 from _pytest.warning_types import warn_explicit_for

63

64 if TYPE_CHECKING:

65

66 from _pytest._code.code import _TracebackStyle

67 from _pytest.terminal import TerminalReporter

68 from .argparsing import Argument

69

70

71_PluggyPlugin = object

72"""A type to represent plugin objects.

73

74 Plugins can be any namespace, so we can't narrow it down much, but we use an

75 alias to make the intent clear.

76

77 Ideally this type would be provided by pluggy itself.

78"""

79

80

81 hookimpl = HookimplMarker("pytest")

82 hookspec = HookspecMarker("pytest")

83

84

85@final

86 class ExitCode(enum.IntEnum):

87 """Encodes the valid exit codes by pytest.

88

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

90

91 .. versionadded:: 5.0

92 """

93

94 #: Tests passed.

95 OK = 0

96 #: Tests failed.

97 TESTS_FAILED = 1

98 #: pytest was interrupted.

99 INTERRUPTED = 2

100 #: An internal error got in the way.

101 INTERNAL_ERROR = 3

102 #: pytest was misused.

103 USAGE_ERROR = 4

104 #: pytest couldn't find tests.

105 NO_TESTS_COLLECTED = 5

106

107

108 class ConftestImportFailure(Exception):

109 def __init__(

110 self,

111 path: Path,

112 excinfo: Tuple[Type[Exception], Exception, TracebackType],

113 ) -> None:

114 super().__init__(path, excinfo)

115 self.path = path

116 self.excinfo = excinfo

117

118 def __str__(self) -> str:

119 return "{}: {} (from {})".format(

120 self.excinfo[0].__name__, self.excinfo[1], self.path

121 )

122

123

124 def filter_traceback_for_conftest_import_failure(

125 entry: _pytest._code.TracebackEntry,

126) -> bool:

127 """Filter tracebacks entries which point to pytest internals or importlib.

128

129 Make a special case for importlib because we use it to import test modules and conftest files

130 in _pytest.pathlib.import_path.

131 """

132 return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep)

133

134

135 def main(

136 args: Optional[Union[List[str], "os.PathLike[str]"]] = None,

137 plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,

138) -> Union[int, ExitCode]:

139 """Perform an in-process test run.

140

141 :param args: List of command line arguments.

142 :param plugins: List of plugin objects to be auto-registered during initialization.

143

144 :returns: An exit code.

145 """

146 try:

147 try:

148 config = _prepareconfig(args, plugins)

149 except ConftestImportFailure as e:

150 exc_info = ExceptionInfo.from_exc_info(e.excinfo)

151 tw = TerminalWriter(sys.stderr)

152 tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)

153 exc_info.traceback = exc_info.traceback.filter(

154 filter_traceback_for_conftest_import_failure

155 )

156 exc_repr = (

157 exc_info.getrepr(style="short", chain=False)

158 if exc_info.traceback

159 else exc_info.exconly()

160 )

161 formatted_tb = str(exc_repr)

162 for line in formatted_tb.splitlines():

163 tw.line(line.rstrip(), red=True)

164 return ExitCode.USAGE_ERROR

165 else:

166 try:

167 ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main(

168 config=config

169 )

170 try:

171 return ExitCode(ret)

172 except ValueError:

173 return ret

174 finally:

175 config._ensure_unconfigure()

176 except UsageError as e:

177 tw = TerminalWriter(sys.stderr)

178 for msg in e.args:

179 tw.line(f"ERROR: {msg}\n", red=True)

180 return ExitCode.USAGE_ERROR

181

182

183 def console_main() -> int:

184 """The CLI entry point of pytest.

185

186 This function is not meant for programmable use; use `main()` instead.

187 """

188 # https://docs.python.org/3/library/signal.html#note-on-sigpipe

189 try:

190 code = main()

191 sys.stdout.flush()

192 return code

193 except BrokenPipeError:

194 # Python flushes standard streams on exit; redirect remaining output

195 # to devnull to avoid another BrokenPipeError at shutdown

196 devnull = os.open(os.devnull, os.O_WRONLY)

197 os.dup2(devnull, sys.stdout.fileno())

198 return 1 # Python exits with error code 1 on EPIPE

199

200

201 class cmdline: # compatibility namespace

202 main = staticmethod(main)

203

204

205 def filename_arg(path: str, optname: str) -> str:

206 """Argparse type validator for filename arguments.

207

208 :path: Path of filename.

209 :optname: Name of the option.

210 """

211 if os.path.isdir(path):

212 raise UsageError(f"{optname} must be a filename, given: {path}")

213 return path

214

215

216 def directory_arg(path: str, optname: str) -> str:

217 """Argparse type validator for directory arguments.

218

219 :path: Path of directory.

220 :optname: Name of the option.

221 """

222 if not os.path.isdir(path):

223 raise UsageError(f"{optname} must be a directory, given: {path}")

224 return path

225

226

227# Plugins that cannot be disabled via "-p no:X" currently.

228 essential_plugins = (

229 "mark",

230 "main",

231 "runner",

232 "fixtures",

233 "helpconfig", # Provides -p.

234)

235

236 default_plugins = essential_plugins + (

237 "python",

238 "terminal",

239 "debugging",

240 "unittest",

241 "capture",

242 "skipping",

243 "legacypath",

244 "tmpdir",

245 "monkeypatch",

246 "recwarn",

247 "pastebin",

248 "nose",

249 "assertion",

250 "junitxml",

251 "doctest",

252 "cacheprovider",

253 "freeze_support",

254 "setuponly",

255 "setupplan",

256 "stepwise",

257 "warnings",

258 "logging",

259 "reports",

260 "python_path",

261 *(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []),

262 "faulthandler",

263)

264

265 builtin_plugins = set(default_plugins)

266 builtin_plugins.add("pytester")

267 builtin_plugins.add("pytester_assertions")

268

269

270 def get_config(

271 args: Optional[List[str]] = None,

272 plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,

273) -> "Config":

274 # subsequent calls to main will create a fresh instance

275 pluginmanager = PytestPluginManager()

276 config = Config(

277 pluginmanager,

278 invocation_params=Config.InvocationParams(

279 args=args or (),

280 plugins=plugins,

281 dir=Path.cwd(),

282 ),

283 )

284

285 if args is not None:

286 # Handle any "-p no:plugin" args.

287 pluginmanager.consider_preparse(args, exclude_only=True)

288

289 for spec in default_plugins:

290 pluginmanager.import_plugin(spec)

291

292 return config

293

294

295 def get_plugin_manager() -> "PytestPluginManager":

296 """Obtain a new instance of the

297 :py:class:`pytest.PytestPluginManager`, with default plugins

298 already loaded.

299

300 This function can be used by integration with other tools, like hooking

301 into pytest to run tests into an IDE.

302 """

303 return get_config().pluginmanager

304

305

306 def _prepareconfig(

307 args: Optional[Union[List[str], "os.PathLike[str]"]] = None,

308 plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,

309) -> "Config":

310 if args is None:

311 args = sys.argv[1:]

312 elif isinstance(args, os.PathLike):

313 args = [os.fspath(args)]

314 elif not isinstance(args, list):

315 msg = ( # type:ignore[unreachable]

316 "`args` parameter expected to be a list of strings, got: {!r} (type: {})"

317 )

318 raise TypeError(msg.format(args, type(args)))

319

320 config = get_config(args, plugins)

321 pluginmanager = config.pluginmanager

322 try:

323 if plugins:

324 for plugin in plugins:

325 if isinstance(plugin, str):

326 pluginmanager.consider_pluginarg(plugin)

327 else:

328 pluginmanager.register(plugin)

329 config = pluginmanager.hook.pytest_cmdline_parse(

330 pluginmanager=pluginmanager, args=args

331 )

332 return config

333 except BaseException:

334 config._ensure_unconfigure()

335 raise

336

337

338 def _get_directory(path: Path) -> Path:

339 """Get the directory of a path - itself if already a directory."""

340 if path.is_file():

341 return path.parent

342 else:

343 return path

344

345

346 def _get_legacy_hook_marks(

347 method: Any,

348 hook_type: str,

349 opt_names: Tuple[str, ...],

350) -> Dict[str, bool]:

351 if TYPE_CHECKING:

352 # abuse typeguard from importlib to avoid massive method type union thats lacking a alias

353 assert inspect.isroutine(method)

354 known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])}

355 must_warn: list[str] = []

356 opts: dict[str, bool] = {}

357 for opt_name in opt_names:

358 opt_attr = getattr(method, opt_name, AttributeError)

359 if opt_attr is not AttributeError:

360 must_warn.append(f"{opt_name}={opt_attr}")

361 opts[opt_name] = True

362 elif opt_name in known_marks:

363 must_warn.append(f"{opt_name}=True")

364 opts[opt_name] = True

365 else:

366 opts[opt_name] = False

367 if must_warn:

368 hook_opts = ", ".join(must_warn)

369 message = _pytest.deprecated.HOOK_LEGACY_MARKING.format(

370 type=hook_type,

371 fullname=method.__qualname__,

372 hook_opts=hook_opts,

373 )

374 warn_explicit_for(cast(FunctionType, method), message)

375 return opts

376

377

378@final

379 class PytestPluginManager(PluginManager):

380 """A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with

381 additional pytest-specific functionality:

382

383 * Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and

384 ``pytest_plugins`` global variables found in plugins being loaded.

385 * ``conftest.py`` loading during start-up.

386 """

387

388 def __init__(self) -> None:

389 import _pytest.assertion

390

391 super().__init__("pytest")

392

393 # -- State related to local conftest plugins.

394 # All loaded conftest modules.

395 self._conftest_plugins: Set[types.ModuleType] = set()

396 # All conftest modules applicable for a directory.

397 # This includes the directory's own conftest modules as well

398 # as those of its parent directories.

399 self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {}

400 # Cutoff directory above which conftests are no longer discovered.

401 self._confcutdir: Optional[Path] = None

402 # If set, conftest loading is skipped.

403 self._noconftest = False

404

405 # _getconftestmodules()'s call to _get_directory() causes a stat

406 # storm when it's called potentially thousands of times in a test

407 # session (#9478), often with the same path, so cache it.

408 self._get_directory = lru_cache(256)(_get_directory)

409

410 self._duplicatepaths: Set[Path] = set()

411

412 # plugins that were explicitly skipped with pytest.skip

413 # list of (module name, skip reason)

414 # previously we would issue a warning when a plugin was skipped, but

415 # since we refactored warnings as first citizens of Config, they are

416 # just stored here to be used later.

417 self.skipped_plugins: List[Tuple[str, str]] = []

418

419 self.add_hookspecs(_pytest.hookspec)

420 self.register(self)

421 if os.environ.get("PYTEST_DEBUG"):

422 err: IO[str] = sys.stderr

423 encoding: str = getattr(err, "encoding", "utf8")

424 try:

425 err = open(

426 os.dup(err.fileno()),

427 mode=err.mode,

428 buffering=1,

429 encoding=encoding,

430 )

431 except Exception:

432 pass

433 self.trace.root.setwriter(err.write)

434 self.enable_tracing()

435

436 # Config._consider_importhook will set a real object if required.

437 self.rewrite_hook = _pytest.assertion.DummyRewriteHook()

438 # Used to know when we are importing conftests after the pytest_configure stage.

439 self._configured = False

440

441 def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):

442 # pytest hooks are always prefixed with "pytest_",

443 # so we avoid accessing possibly non-readable attributes

444 # (see issue #1073).

445 if not name.startswith("pytest_"):

446 return

447 # Ignore names which can not be hooks.

448 if name == "pytest_plugins":

449 return

450

451 opts = super().parse_hookimpl_opts(plugin, name)

452 if opts is not None:

453 return opts

454

455 method = getattr(plugin, name)

456 # Consider only actual functions for hooks (#3775).

457 if not inspect.isroutine(method):

458 return

459 # Collect unmarked hooks as long as they have the `pytest_' prefix.

460 return _get_legacy_hook_marks(

461 method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")

462 )

463

464 def parse_hookspec_opts(self, module_or_class, name: str):

465 opts = super().parse_hookspec_opts(module_or_class, name)

466 if opts is None:

467 method = getattr(module_or_class, name)

468 if name.startswith("pytest_"):

469 opts = _get_legacy_hook_marks(

470 method,

471 "spec",

472 ("firstresult", "historic"),

473 )

474 return opts

475

476 def register(

477 self, plugin: _PluggyPlugin, name: Optional[str] = None

478 ) -> Optional[str]:

479 if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS:

480 warnings.warn(

481 PytestConfigWarning(

482 "{} plugin has been merged into the core, "

483 "please remove it from your requirements.".format(

484 name.replace("_", "-")

485 )

486 )

487 )

488 return None

489 ret: Optional[str] = super().register(plugin, name)

490 if ret:

491 self.hook.pytest_plugin_registered.call_historic(

492 kwargs=dict(plugin=plugin, manager=self)

493 )

494

495 if isinstance(plugin, types.ModuleType):

496 self.consider_module(plugin)

497 return ret

498

499 def getplugin(self, name: str):

500 # Support deprecated naming because plugins (xdist e.g.) use it.

501 plugin: Optional[_PluggyPlugin] = self.get_plugin(name)

502 return plugin

503

504 def hasplugin(self, name: str) -> bool:

505 """Return whether a plugin with the given name is registered."""

506 return bool(self.get_plugin(name))

507

508 def pytest_configure(self, config: "Config") -> None:

509 """:meta private:"""

510 # XXX now that the pluginmanager exposes hookimpl(tryfirst...)

511 # we should remove tryfirst/trylast as markers.

512 config.addinivalue_line(

513 "markers",

514 "tryfirst: mark a hook implementation function such that the "

515 "plugin machinery will try to call it first/as early as possible. "

516 "DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.",

517 )

518 config.addinivalue_line(

519 "markers",

520 "trylast: mark a hook implementation function such that the "

521 "plugin machinery will try to call it last/as late as possible. "

522 "DEPRECATED, use @pytest.hookimpl(trylast=True) instead.",

523 )

524 self._configured = True

525

526 #

527 # Internal API for local conftest plugin handling.

528 #

529 def _set_initial_conftests(

530 self, namespace: argparse.Namespace, rootpath: Path

531 ) -> None:

532 """Load initial conftest files given a preparsed "namespace".

533

534 As conftest files may add their own command line options which have

535 arguments ('--my-opt somepath') we might get some false positives.

536 All builtin and 3rd party plugins will have been loaded, however, so

537 common options will not confuse our logic here.

538 """

539 current = Path.cwd()

540 self._confcutdir = (

541 absolutepath(current / namespace.confcutdir)

542 if namespace.confcutdir

543 else None

544 )

545 self._noconftest = namespace.noconftest

546 self._using_pyargs = namespace.pyargs

547 testpaths = namespace.file_or_dir

548 foundanchor = False

549 for testpath in testpaths:

550 path = str(testpath)

551 # remove node-id syntax

552 i = path.find("::")

553 if i != -1:

554 path = path[:i]

555 anchor = absolutepath(current / path)

556 if anchor.exists(): # we found some file object

557 self._try_load_conftest(anchor, namespace.importmode, rootpath)

558 foundanchor = True

559 if not foundanchor:

560 self._try_load_conftest(current, namespace.importmode, rootpath)

561

562 def _is_in_confcutdir(self, path: Path) -> bool:

563 """Whether a path is within the confcutdir.

564

565 When false, should not load conftest.

566 """

567 if self._confcutdir is None:

568 return True

569 return path not in self._confcutdir.parents

570

571 def _try_load_conftest(

572 self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path

573 ) -> None:

574 self._getconftestmodules(anchor, importmode, rootpath)

575 # let's also consider test* subdirs

576 if anchor.is_dir():

577 for x in anchor.glob("test*"):

578 if x.is_dir():

579 self._getconftestmodules(x, importmode, rootpath)

580

581 def _getconftestmodules(

582 self, path: Path, importmode: Union[str, ImportMode], rootpath: Path

583 ) -> Sequence[types.ModuleType]:

584 if self._noconftest:

585 return []

586

587 directory = self._get_directory(path)

588

589 # Optimization: avoid repeated searches in the same directory.

590 # Assumes always called with same importmode and rootpath.

591 existing_clist = self._dirpath2confmods.get(directory)

592 if existing_clist is not None:

593 return existing_clist

594

595 # XXX these days we may rather want to use config.rootpath

596 # and allow users to opt into looking into the rootdir parent

597 # directories instead of requiring to specify confcutdir.

598 clist = []

599 for parent in reversed((directory, *directory.parents)):

600 if self._is_in_confcutdir(parent):

601 conftestpath = parent / "conftest.py"

602 if conftestpath.is_file():

603 mod = self._importconftest(conftestpath, importmode, rootpath)

604 clist.append(mod)

605 self._dirpath2confmods[directory] = clist

606 return clist

607

608 def _rget_with_confmod(

609 self,

610 name: str,

611 path: Path,

612 importmode: Union[str, ImportMode],

613 rootpath: Path,

614 ) -> Tuple[types.ModuleType, Any]:

615 modules = self._getconftestmodules(path, importmode, rootpath=rootpath)

616 for mod in reversed(modules):

617 try:

618 return mod, getattr(mod, name)

619 except AttributeError:

620 continue

621 raise KeyError(name)

622

623 def _importconftest(

624 self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path

625 ) -> types.ModuleType:

626 existing = self.get_plugin(str(conftestpath))

627 if existing is not None:

628 return cast(types.ModuleType, existing)

629

630 pkgpath = resolve_package_path(conftestpath)

631 if pkgpath is None:

632 _ensure_removed_sysmodule(conftestpath.stem)

633

634 try:

635 mod = import_path(conftestpath, mode=importmode, root=rootpath)

636 except Exception as e:

637 assert e.__traceback__ is not None

638 exc_info = (type(e), e, e.__traceback__)

639 raise ConftestImportFailure(conftestpath, exc_info) from e

640

641 self._check_non_top_pytest_plugins(mod, conftestpath)

642

643 self._conftest_plugins.add(mod)

644 dirpath = conftestpath.parent

645 if dirpath in self._dirpath2confmods:

646 for path, mods in self._dirpath2confmods.items():

647 if dirpath in path.parents or path == dirpath:

648 assert mod not in mods

649 mods.append(mod)

650 self.trace(f"loading conftestmodule {mod!r}")

651 self.consider_conftest(mod)

652 return mod

653

654 def _check_non_top_pytest_plugins(

655 self,

656 mod: types.ModuleType,

657 conftestpath: Path,

658 ) -> None:

659 if (

660 hasattr(mod, "pytest_plugins")

661 and self._configured

662 and not self._using_pyargs

663 ):

664 msg = (

665 "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n"

666 "It affects the entire test suite instead of just below the conftest as expected.\n"

667 " {}\n"

668 "Please move it to a top level conftest file at the rootdir:\n"

669 " {}\n"

670 "For more information, visit:\n"

671 " https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files"

672 )

673 fail(msg.format(conftestpath, self._confcutdir), pytrace=False)

674

675 #

676 # API for bootstrapping plugin loading

677 #

678 #

679

680 def consider_preparse(

681 self, args: Sequence[str], *, exclude_only: bool = False

682 ) -> None:

683 """:meta private:"""

684 i = 0

685 n = len(args)

686 while i < n:

687 opt = args[i]

688 i += 1

689 if isinstance(opt, str):

690 if opt == "-p":

691 try:

692 parg = args[i]

693 except IndexError:

694 return

695 i += 1

696 elif opt.startswith("-p"):

697 parg = opt[2:]

698 else:

699 continue

700 if exclude_only and not parg.startswith("no:"):

701 continue

702 self.consider_pluginarg(parg)

703

704 def consider_pluginarg(self, arg: str) -> None:

705 """:meta private:"""

706 if arg.startswith("no:"):

707 name = arg[3:]

708 if name in essential_plugins:

709 raise UsageError("plugin %s cannot be disabled" % name)

710

711 # PR #4304: remove stepwise if cacheprovider is blocked.

712 if name == "cacheprovider":

713 self.set_blocked("stepwise")

714 self.set_blocked("pytest_stepwise")

715

716 self.set_blocked(name)

717 if not name.startswith("pytest_"):

718 self.set_blocked("pytest_" + name)

719 else:

720 name = arg

721 # Unblock the plugin. None indicates that it has been blocked.

722 # There is no interface with pluggy for this.

723 if self._name2plugin.get(name, -1) is None:

724 del self._name2plugin[name]

725 if not name.startswith("pytest_"):

726 if self._name2plugin.get("pytest_" + name, -1) is None:

727 del self._name2plugin["pytest_" + name]

728 self.import_plugin(arg, consider_entry_points=True)

729

730 def consider_conftest(self, conftestmodule: types.ModuleType) -> None:

731 """:meta private:"""

732 self.register(conftestmodule, name=conftestmodule.__file__)

733

734 def consider_env(self) -> None:

735 """:meta private:"""

736 self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS"))

737

738 def consider_module(self, mod: types.ModuleType) -> None:

739 """:meta private:"""

740 self._import_plugin_specs(getattr(mod, "pytest_plugins", []))

741

742 def _import_plugin_specs(

743 self, spec: Union[None, types.ModuleType, str, Sequence[str]]

744 ) -> None:

745 plugins = _get_plugin_specs_as_list(spec)

746 for import_spec in plugins:

747 self.import_plugin(import_spec)

748

749 def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:

750 """Import a plugin with ``modname``.

751

752 If ``consider_entry_points`` is True, entry point names are also

753 considered to find a plugin.

754 """

755 # Most often modname refers to builtin modules, e.g. "pytester",

756 # "terminal" or "capture". Those plugins are registered under their

757 # basename for historic purposes but must be imported with the

758 # _pytest prefix.

759 assert isinstance(modname, str), (

760 "module name as text required, got %r" % modname

761 )

762 if self.is_blocked(modname) or self.get_plugin(modname) is not None:

763 return

764

765 importspec = "_pytest." + modname if modname in builtin_plugins else modname

766 self.rewrite_hook.mark_rewrite(importspec)

767

768 if consider_entry_points:

769 loaded = self.load_setuptools_entrypoints("pytest11", name=modname)

770 if loaded:

771 return

772

773 try:

774 __import__(importspec)

775 except ImportError as e:

776 raise ImportError(

777 f'Error importing plugin "{modname}": {e.args[0]}'

778 ).with_traceback(e.__traceback__) from e

779

780 except Skipped as e:

781 self.skipped_plugins.append((modname, e.msg or ""))

782 else:

783 mod = sys.modules[importspec]

784 self.register(mod, modname)

785

786

787 def _get_plugin_specs_as_list(

788 specs: Union[None, types.ModuleType, str, Sequence[str]]

789) -> List[str]:

790 """Parse a plugins specification into a list of plugin names."""

791 # None means empty.

792 if specs is None:

793 return []

794 # Workaround for #3899 - a submodule which happens to be called "pytest_plugins".

795 if isinstance(specs, types.ModuleType):

796 return []

797 # Comma-separated list.

798 if isinstance(specs, str):

799 return specs.split(",") if specs else []

800 # Direct specification.

801 if isinstance(specs, collections.abc.Sequence):

802 return list(specs)

803 raise UsageError(

804 "Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %r"

805 % specs

806 )

807

808

809 def _ensure_removed_sysmodule(modname: str) -> None:

810 try:

811 del sys.modules[modname]

812 except KeyError:

813 pass

814

815

816 class Notset:

817 def __repr__(self):

818 return "<NOTSET>"

819

820

821 notset = Notset()

822

823

824 def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:

825 """Given an iterable of file names in a source distribution, return the "names" that should

826 be marked for assertion rewrite.

827

828 For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in

829 the assertion rewrite mechanism.

830

831 This function has to deal with dist-info based distributions and egg based distributions

832 (which are still very much in use for "editable" installs).

833

834 Here are the file names as seen in a dist-info based distribution:

835

836 pytest_mock/__init__.py

837 pytest_mock/_version.py

838 pytest_mock/plugin.py

839 pytest_mock.egg-info/PKG-INFO

840

841 Here are the file names as seen in an egg based distribution:

842

843 src/pytest_mock/__init__.py

844 src/pytest_mock/_version.py

845 src/pytest_mock/plugin.py

846 src/pytest_mock.egg-info/PKG-INFO

847 LICENSE

848 setup.py

849

850 We have to take in account those two distribution flavors in order to determine which

851 names should be considered for assertion rewriting.

852

853 More information:

854 https://github.com/pytest-dev/pytest-mock/issues/167

855 """

856 package_files = list(package_files)

857 seen_some = False

858 for fn in package_files:

859 is_simple_module = "/" not in fn and fn.endswith(".py")

860 is_package = fn.count("/") == 1 and fn.endswith("__init__.py")

861 if is_simple_module:

862 module_name, _ = os.path.splitext(fn)

863 # we ignore "setup.py" at the root of the distribution

864 # as well as editable installation finder modules made by setuptools

865 if module_name != "setup" and not module_name.startswith("__editable__"):

866 seen_some = True

867 yield module_name

868 elif is_package:

869 package_name = os.path.dirname(fn)

870 seen_some = True

871 yield package_name

872

873 if not seen_some:

874 # At this point we did not find any packages or modules suitable for assertion

875 # rewriting, so we try again by stripping the first path component (to account for

876 # "src" based source trees for example).

877 # This approach lets us have the common case continue to be fast, as egg-distributions

878 # are rarer.

879 new_package_files = []

880 for fn in package_files:

881 parts = fn.split("/")

882 new_fn = "/".join(parts[1:])

883 if new_fn:

884 new_package_files.append(new_fn)

885 if new_package_files:

886 yield from _iter_rewritable_modules(new_package_files)

887

888

889 def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:

890 return tuple(args)

891

892

893@final

894 class Config:

895 """Access to configuration values, pluginmanager and plugin hooks.

896

897 :param PytestPluginManager pluginmanager:

898 A pytest PluginManager.

899

900 :param InvocationParams invocation_params:

901 Object containing parameters regarding the :func:`pytest.main`

902 invocation.

903 """

904

905 @final

906 @attr.s(frozen=True, auto_attribs=True)

907 class InvocationParams:

908 """Holds parameters passed during :func:`pytest.main`.

909

910 The object attributes are read-only.

911

912 .. versionadded:: 5.1

913

914 .. note::

915

916 Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts``

917 ini option are handled by pytest, not being included in the ``args`` attribute.

918

919 Plugins accessing ``InvocationParams`` must be aware of that.

920 """

921

922 args: Tuple[str, ...] = attr.ib(converter=_args_converter)

923 """The command-line arguments as passed to :func:`pytest.main`."""

924 plugins: Optional[Sequence[Union[str, _PluggyPlugin]]]

925 """Extra plugins, might be `None`."""

926 dir: Path

927 """The directory from which :func:`pytest.main` was invoked."""

928

929 class ArgsSource(enum.Enum):

930 """Indicates the source of the test arguments.

931

932 .. versionadded:: 7.2

933 """

934

935 #: Command line arguments.

936 ARGS = enum.auto()

937 #: Invocation directory.

938 INCOVATION_DIR = enum.auto()

939 #: 'testpaths' configuration value.

940 TESTPATHS = enum.auto()

941

942 def __init__(

943 self,

944 pluginmanager: PytestPluginManager,

945 *,

946 invocation_params: Optional[InvocationParams] = None,

947 ) -> None:

948 from .argparsing import Parser, FILE_OR_DIR

949

950 if invocation_params is None:

951 invocation_params = self.InvocationParams(

952 args=(), plugins=None, dir=Path.cwd()

953 )

954

955 self.option = argparse.Namespace()

956 """Access to command line option as attributes.

957

958 :type: argparse.Namespace

959 """

960

961 self.invocation_params = invocation_params

962 """The parameters with which pytest was invoked.

963

964 :type: InvocationParams

965 """

966

967 _a = FILE_OR_DIR

968 self._parser = Parser(

969 usage=f"%(prog)s [options] [{_a}] [{_a}] [...]",

970 processopt=self._processopt,

971 _ispytest=True,

972 )

973 self.pluginmanager = pluginmanager

974 """The plugin manager handles plugin registration and hook invocation.

975

976 :type: PytestPluginManager

977 """

978

979 self.stash = Stash()

980 """A place where plugins can store information on the config for their

981 own use.

982

983 :type: Stash

984 """

985 # Deprecated alias. Was never public. Can be removed in a few releases.

986 self._store = self.stash

987

988 from .compat import PathAwareHookProxy

989

990 self.trace = self.pluginmanager.trace.root.get("config")

991 self.hook = PathAwareHookProxy(self.pluginmanager.hook)

992 self._inicache: Dict[str, Any] = {}

993 self._override_ini: Sequence[str] = ()

994 self._opt2dest: Dict[str, str] = {}

995 self._cleanup: List[Callable[[], None]] = []

996 self.pluginmanager.register(self, "pytestconfig")

997 self._configured = False

998 self.hook.pytest_addoption.call_historic(

999 kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)

1000 )

1001 self.args_source = Config.ArgsSource.ARGS

1002 self.args: List[str] = []

1003

1004 if TYPE_CHECKING:

1005 from _pytest.cacheprovider import Cache

1006

1007 self.cache: Optional[Cache] = None

1008

1009 @property

1010 def rootpath(self) -> Path:

1011 """The path to the :ref:`rootdir <rootdir>`.

1012

1013 :type: pathlib.Path

1014

1015 .. versionadded:: 6.1

1016 """

1017 return self._rootpath

1018

1019 @property

1020 def inipath(self) -> Optional[Path]:

1021 """The path to the :ref:`configfile <configfiles>`.

1022

1023 :type: Optional[pathlib.Path]

1024

1025 .. versionadded:: 6.1

1026 """

1027 return self._inipath

1028

1029 def add_cleanup(self, func: Callable[[], None]) -> None:

1030 """Add a function to be called when the config object gets out of

1031 use (usually coinciding with pytest_unconfigure)."""

1032 self._cleanup.append(func)

1033

1034 def _do_configure(self) -> None:

1035 assert not self._configured

1036 self._configured = True

1037 with warnings.catch_warnings():

1038 warnings.simplefilter("default")

1039 self.hook.pytest_configure.call_historic(kwargs=dict(config=self))

1040

1041 def _ensure_unconfigure(self) -> None:

1042 if self._configured:

1043 self._configured = False

1044 self.hook.pytest_unconfigure(config=self)

1045 self.hook.pytest_configure._call_history = []

1046 while self._cleanup:

1047 fin = self._cleanup.pop()

1048 fin()

1049

1050 def get_terminal_writer(self) -> TerminalWriter:

1051 terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(

1052 "terminalreporter"

1053 )

1054 return terminalreporter._tw

1055

1056 def pytest_cmdline_parse(

1057 self, pluginmanager: PytestPluginManager, args: List[str]

1058 ) -> "Config":

1059 try:

1060 self.parse(args)

1061 except UsageError:

1062

1063 # Handle --version and --help here in a minimal fashion.

1064 # This gets done via helpconfig normally, but its

1065 # pytest_cmdline_main is not called in case of errors.

1066 if getattr(self.option, "version", False) or "--version" in args:

1067 from _pytest.helpconfig import showversion

1068

1069 showversion(self)

1070 elif (

1071 getattr(self.option, "help", False) or "--help" in args or "-h" in args

1072 ):

1073 self._parser._getparser().print_help()

1074 sys.stdout.write(

1075 "\nNOTE: displaying only minimal help due to UsageError.\n\n"

1076 )

1077

1078 raise

1079

1080 return self

1081

1082 def notify_exception(

1083 self,

1084 excinfo: ExceptionInfo[BaseException],

1085 option: Optional[argparse.Namespace] = None,

1086 ) -> None:

1087 if option and getattr(option, "fulltrace", False):

1088 style: _TracebackStyle = "long"

1089 else:

1090 style = "native"

1091 excrepr = excinfo.getrepr(

1092 funcargs=True, showlocals=getattr(option, "showlocals", False), style=style

1093 )

1094 res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo)

1095 if not any(res):

1096 for line in str(excrepr).split("\n"):

1097 sys.stderr.write("INTERNALERROR> %s\n" % line)

1098 sys.stderr.flush()

1099

1100 def cwd_relative_nodeid(self, nodeid: str) -> str:

1101 # nodeid's are relative to the rootpath, compute relative to cwd.

1102 if self.invocation_params.dir != self.rootpath:

1103 fullpath = self.rootpath / nodeid

1104 nodeid = bestrelpath(self.invocation_params.dir, fullpath)

1105 return nodeid

1106

1107 @classmethod

1108 def fromdictargs(cls, option_dict, args) -> "Config":

1109 """Constructor usable for subprocesses."""

1110 config = get_config(args)

1111 config.option.__dict__.update(option_dict)

1112 config.parse(args, addopts=False)

1113 for x in config.option.plugins:

1114 config.pluginmanager.consider_pluginarg(x)

1115 return config

1116

1117 def _processopt(self, opt: "Argument") -> None:

1118 for name in opt._short_opts + opt._long_opts:

1119 self._opt2dest[name] = opt.dest

1120

1121 if hasattr(opt, "default"):

1122 if not hasattr(self.option, opt.dest):

1123 setattr(self.option, opt.dest, opt.default)

1124

1125 @hookimpl(trylast=True)

1126 def pytest_load_initial_conftests(self, early_config: "Config") -> None:

1127 self.pluginmanager._set_initial_conftests(

1128 early_config.known_args_namespace, rootpath=early_config.rootpath

1129 )

1130

1131 def _initini(self, args: Sequence[str]) -> None:

1132 ns, unknown_args = self._parser.parse_known_and_unknown_args(

1133 args, namespace=copy.copy(self.option)

1134 )

1135 rootpath, inipath, inicfg = determine_setup(

1136 ns.inifilename,

1137 ns.file_or_dir + unknown_args,

1138 rootdir_cmd_arg=ns.rootdir or None,

1139 config=self,

1140 )

1141 self._rootpath = rootpath

1142 self._inipath = inipath

1143 self.inicfg = inicfg

1144 self._parser.extra_info["rootdir"] = str(self.rootpath)

1145 self._parser.extra_info["inifile"] = str(self.inipath)

1146 self._parser.addini("addopts", "Extra command line options", "args")

1147 self._parser.addini("minversion", "Minimally required pytest version")

1148 self._parser.addini(

1149 "required_plugins",

1150 "Plugins that must be present for pytest to run",

1151 type="args",

1152 default=[],

1153 )

1154 self._override_ini = ns.override_ini or ()

1155

1156 def _consider_importhook(self, args: Sequence[str]) -> None:

1157 """Install the PEP 302 import hook if using assertion rewriting.

1158

1159 Needs to parse the --assert=<mode> option from the commandline

1160 and find all the installed plugins to mark them for rewriting

1161 by the importhook.

1162 """

1163 ns, unknown_args = self._parser.parse_known_and_unknown_args(args)

1164 mode = getattr(ns, "assertmode", "plain")

1165 if mode == "rewrite":

1166 import _pytest.assertion

1167

1168 try:

1169 hook = _pytest.assertion.install_importhook(self)

1170 except SystemError:

1171 mode = "plain"

1172 else:

1173 self._mark_plugins_for_rewrite(hook)

1174 self._warn_about_missing_assertion(mode)

1175

1176 def _mark_plugins_for_rewrite(self, hook) -> None:

1177 """Given an importhook, mark for rewrite any top-level

1178 modules or packages in the distribution package for

1179 all pytest plugins."""

1180 self.pluginmanager.rewrite_hook = hook

1181

1182 if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):

1183 # We don't autoload from setuptools entry points, no need to continue.

1184 return

1185

1186 package_files = (

1187 str(file)

1188 for dist in importlib_metadata.distributions()

1189 if any(ep.group == "pytest11" for ep in dist.entry_points)

1190 for file in dist.files or []

1191 )

1192

1193 for name in _iter_rewritable_modules(package_files):

1194 hook.mark_rewrite(name)

1195

1196 def _validate_args(self, args: List[str], via: str) -> List[str]:

1197 """Validate known args."""

1198 self._parser._config_source_hint = via # type: ignore

1199 try:

1200 self._parser.parse_known_and_unknown_args(

1201 args, namespace=copy.copy(self.option)

1202 )

1203 finally:

1204 del self._parser._config_source_hint # type: ignore

1205

1206 return args

1207

1208 def _preparse(self, args: List[str], addopts: bool = True) -> None:

1209 if addopts:

1210 env_addopts = os.environ.get("PYTEST_ADDOPTS", "")

1211 if len(env_addopts):

1212 args[:] = (

1213 self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")

1214 + args

1215 )

1216 self._initini(args)

1217 if addopts:

1218 args[:] = (

1219 self._validate_args(self.getini("addopts"), "via addopts config") + args

1220 )

1221

1222 self.known_args_namespace = self._parser.parse_known_args(

1223 args, namespace=copy.copy(self.option)

1224 )

1225 self._checkversion()

1226 self._consider_importhook(args)

1227 self.pluginmanager.consider_preparse(args, exclude_only=False)

1228 if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):

1229 # Don't autoload from setuptools entry point. Only explicitly specified

1230 # plugins are going to be loaded.

1231 self.pluginmanager.load_setuptools_entrypoints("pytest11")

1232 self.pluginmanager.consider_env()

1233

1234 self.known_args_namespace = self._parser.parse_known_args(

1235 args, namespace=copy.copy(self.known_args_namespace)

1236 )

1237

1238 self._validate_plugins()

1239 self._warn_about_skipped_plugins()

1240

1241 if self.known_args_namespace.strict:

1242 self.issue_config_time_warning(

1243 _pytest.deprecated.STRICT_OPTION, stacklevel=2

1244 )

1245

1246 if self.known_args_namespace.confcutdir is None and self.inipath is not None:

1247 confcutdir = str(self.inipath.parent)

1248 self.known_args_namespace.confcutdir = confcutdir

1249 try:

1250 self.hook.pytest_load_initial_conftests(

1251 early_config=self, args=args, parser=self._parser

1252 )

1253 except ConftestImportFailure as e:

1254 if self.known_args_namespace.help or self.known_args_namespace.version:

1255 # we don't want to prevent --help/--version to work

1256 # so just let is pass and print a warning at the end

1257 self.issue_config_time_warning(

1258 PytestConfigWarning(f"could not load initial conftests: {e.path}"),

1259 stacklevel=2,

1260 )

1261 else:

1262 raise

1263

1264 @hookimpl(hookwrapper=True)

1265 def pytest_collection(self) -> Generator[None, None, None]:

1266 # Validate invalid ini keys after collection is done so we take in account

1267 # options added by late-loading conftest files.

1268 yield

1269 self._validate_config_options()

1270

1271 def _checkversion(self) -> None:

1272 import pytest

1273

1274 minver = self.inicfg.get("minversion", None)

1275 if minver:

1276 # Imported lazily to improve start-up time.

1277 from packaging.version import Version

1278

1279 if not isinstance(minver, str):

1280 raise pytest.UsageError(

1281 "%s: 'minversion' must be a single value" % self.inipath

1282 )

1283

1284 if Version(minver) > Version(pytest.__version__):

1285 raise pytest.UsageError(

1286 "%s: 'minversion' requires pytest-%s, actual pytest-%s'"

1287 % (

1288 self.inipath,

1289 minver,

1290 pytest.__version__,

1291 )

1292 )

1293

1294 def _validate_config_options(self) -> None:

1295 for key in sorted(self._get_unknown_ini_keys()):

1296 self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")

1297

1298 def _validate_plugins(self) -> None:

1299 required_plugins = sorted(self.getini("required_plugins"))

1300 if not required_plugins:

1301 return

1302

1303 # Imported lazily to improve start-up time.

1304 from packaging.version import Version

1305 from packaging.requirements import InvalidRequirement, Requirement

1306

1307 plugin_info = self.pluginmanager.list_plugin_distinfo()

1308 plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}

1309

1310 missing_plugins = []

1311 for required_plugin in required_plugins:

1312 try:

1313 req = Requirement(required_plugin)

1314 except InvalidRequirement:

1315 missing_plugins.append(required_plugin)

1316 continue

1317

1318 if req.name not in plugin_dist_info:

1319 missing_plugins.append(required_plugin)

1320 elif not req.specifier.contains(

1321 Version(plugin_dist_info[req.name]), prereleases=True

1322 ):

1323 missing_plugins.append(required_plugin)

1324

1325 if missing_plugins:

1326 raise UsageError(

1327 "Missing required plugins: {}".format(", ".join(missing_plugins)),

1328 )

1329

1330 def _warn_or_fail_if_strict(self, message: str) -> None:

1331 if self.known_args_namespace.strict_config:

1332 raise UsageError(message)

1333

1334 self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3)

1335

1336 def _get_unknown_ini_keys(self) -> List[str]:

1337 parser_inicfg = self._parser._inidict

1338 return [name for name in self.inicfg if name not in parser_inicfg]

1339

1340 def parse(self, args: List[str], addopts: bool = True) -> None:

1341 # Parse given cmdline arguments into this config object.

1342 assert (

1343 self.args == []

1344 ), "can only parse cmdline args at most once per Config object"

1345 self.hook.pytest_addhooks.call_historic(

1346 kwargs=dict(pluginmanager=self.pluginmanager)

1347 )

1348 self._preparse(args, addopts=addopts)

1349 # XXX deprecated hook:

1350 self.hook.pytest_cmdline_preparse(config=self, args=args)

1351 self._parser.after_preparse = True # type: ignore

1352 try:

1353 source = Config.ArgsSource.ARGS

1354 args = self._parser.parse_setoption(

1355 args, self.option, namespace=self.option

1356 )

1357 if not args:

1358 if self.invocation_params.dir == self.rootpath:

1359 source = Config.ArgsSource.TESTPATHS

1360 testpaths: List[str] = self.getini("testpaths")

1361 if self.known_args_namespace.pyargs:

1362 args = testpaths

1363 else:

1364 args = []

1365 for path in testpaths:

1366 args.extend(sorted(glob.iglob(path, recursive=True)))

1367 if not args:

1368 source = Config.ArgsSource.INCOVATION_DIR

1369 args = [str(self.invocation_params.dir)]

1370 self.args = args

1371 self.args_source = source

1372 except PrintHelp:

1373 pass

1374

1375 def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None:

1376 """Issue and handle a warning during the "configure" stage.

1377

1378 During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``

1379 function because it is not possible to have hookwrappers around ``pytest_configure``.

1380

1381 This function is mainly intended for plugins that need to issue warnings during

1382 ``pytest_configure`` (or similar stages).

1383

1384 :param warning: The warning instance.

1385 :param stacklevel: stacklevel forwarded to warnings.warn.

1386 """

1387 if self.pluginmanager.is_blocked("warnings"):

1388 return

1389

1390 cmdline_filters = self.known_args_namespace.pythonwarnings or []

1391 config_filters = self.getini("filterwarnings")

1392

1393 with warnings.catch_warnings(record=True) as records:

1394 warnings.simplefilter("always", type(warning))

1395 apply_warning_filters(config_filters, cmdline_filters)

1396 warnings.warn(warning, stacklevel=stacklevel)

1397

1398 if records:

1399 frame = sys._getframe(stacklevel - 1)

1400 location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name

1401 self.hook.pytest_warning_recorded.call_historic(

1402 kwargs=dict(

1403 warning_message=records[0],

1404 when="config",

1405 nodeid="",

1406 location=location,

1407 )

1408 )

1409

1410 def addinivalue_line(self, name: str, line: str) -> None:

1411 """Add a line to an ini-file option. The option must have been

1412 declared but might not yet be set in which case the line becomes

1413 the first line in its value."""

1414 x = self.getini(name)

1415 assert isinstance(x, list)

1416 x.append(line) # modifies the cached list inline

1417

1418 def getini(self, name: str):

1419 """Return configuration value from an :ref:`ini file <configfiles>`.

1420

1421 If the specified name hasn't been registered through a prior

1422 :func:`parser.addini <pytest.Parser.addini>` call (usually from a

1423 plugin), a ValueError is raised.

1424 """

1425 try:

1426 return self._inicache[name]

1427 except KeyError:

1428 self._inicache[name] = val = self._getini(name)

1429 return val

1430

1431 # Meant for easy monkeypatching by legacypath plugin.

1432 # Can be inlined back (with no cover removed) once legacypath is gone.

1433 def _getini_unknown_type(self, name: str, type: str, value: Union[str, List[str]]):

1434 msg = f"unknown configuration type: {type}"

1435 raise ValueError(msg, value) # pragma: no cover

1436

1437 def _getini(self, name: str):

1438 try:

1439 description, type, default = self._parser._inidict[name]

1440 except KeyError as e:

1441 raise ValueError(f"unknown configuration value: {name!r}") from e

1442 override_value = self._get_override_ini_value(name)

1443 if override_value is None:

1444 try:

1445 value = self.inicfg[name]

1446 except KeyError:

1447 if default is not None:

1448 return default

1449 if type is None:

1450 return ""

1451 return []

1452 else:

1453 value = override_value

1454 # Coerce the values based on types.

1455 #

1456 # Note: some coercions are only required if we are reading from .ini files, because

1457 # the file format doesn't contain type information, but when reading from toml we will

1458 # get either str or list of str values (see _parse_ini_config_from_pyproject_toml).

1459 # For example:

1460 #

1461 # ini:

1462 # a_line_list = "tests acceptance"

1463 # in this case, we need to split the string to obtain a list of strings.

1464 #

1465 # toml:

1466 # a_line_list = ["tests", "acceptance"]

1467 # in this case, we already have a list ready to use.

1468 #

1469 if type == "paths":

1470 # TODO: This assert is probably not valid in all cases.

1471 assert self.inipath is not None

1472 dp = self.inipath.parent

1473 input_values = shlex.split(value) if isinstance(value, str) else value

1474 return [dp / x for x in input_values]

1475 elif type == "args":

1476 return shlex.split(value) if isinstance(value, str) else value

1477 elif type == "linelist":

1478 if isinstance(value, str):

1479 return [t for t in map(lambda x: x.strip(), value.split("\n")) if t]

1480 else:

1481 return value

1482 elif type == "bool":

1483 return _strtobool(str(value).strip())

1484 elif type == "string":

1485 return value

1486 elif type is None:

1487 return value

1488 else:

1489 return self._getini_unknown_type(name, type, value)

1490

1491 def _getconftest_pathlist(

1492 self, name: str, path: Path, rootpath: Path

1493 ) -> Optional[List[Path]]:

1494 try:

1495 mod, relroots = self.pluginmanager._rget_with_confmod(

1496 name, path, self.getoption("importmode"), rootpath

1497 )

1498 except KeyError:

1499 return None

1500 assert mod.__file__ is not None

1501 modpath = Path(mod.__file__).parent

1502 values: List[Path] = []

1503 for relroot in relroots:

1504 if isinstance(relroot, os.PathLike):

1505 relroot = Path(relroot)

1506 else:

1507 relroot = relroot.replace("/", os.sep)

1508 relroot = absolutepath(modpath / relroot)

1509 values.append(relroot)

1510 return values

1511

1512 def _get_override_ini_value(self, name: str) -> Optional[str]:

1513 value = None

1514 # override_ini is a list of "ini=value" options.

1515 # Always use the last item if multiple values are set for same ini-name,

1516 # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2.

1517 for ini_config in self._override_ini:

1518 try:

1519 key, user_ini_value = ini_config.split("=", 1)

1520 except ValueError as e:

1521 raise UsageError(

1522 "-o/--override-ini expects option=value style (got: {!r}).".format(

1523 ini_config

1524 )

1525 ) from e

1526 else:

1527 if key == name:

1528 value = user_ini_value

1529 return value

1530

1531 def getoption(self, name: str, default=notset, skip: bool = False):

1532 """Return command line option value.

1533

1534 :param name: Name of the option. You may also specify

1535 the literal ``--OPT`` option instead of the "dest" option name.

1536 :param default: Default value if no option of that name exists.

1537 :param skip: If True, raise pytest.skip if option does not exists

1538 or has a None value.

1539 """

1540 name = self._opt2dest.get(name, name)

1541 try:

1542 val = getattr(self.option, name)

1543 if val is None and skip:

1544 raise AttributeError(name)

1545 return val

1546 except AttributeError as e:

1547 if default is not notset:

1548 return default

1549 if skip:

1550 import pytest

1551

1552 pytest.skip(f"no {name!r} option found")

1553 raise ValueError(f"no option named {name!r}") from e

1554

1555 def getvalue(self, name: str, path=None):

1556 """Deprecated, use getoption() instead."""

1557 return self.getoption(name)

1558

1559 def getvalueorskip(self, name: str, path=None):

1560 """Deprecated, use getoption(skip=True) instead."""

1561 return self.getoption(name, skip=True)

1562

1563 def _warn_about_missing_assertion(self, mode: str) -> None:

1564 if not _assertion_supported():

1565 if mode == "plain":

1566 warning_text = (

1567 "ASSERTIONS ARE NOT EXECUTED"

1568 " and FAILING TESTS WILL PASS. Are you"

1569 " using python -O?"

1570 )

1571 else:

1572 warning_text = (

1573 "assertions not in test modules or"

1574 " plugins will be ignored"

1575 " because assert statements are not executed "

1576 "by the underlying Python interpreter "

1577 "(are you using python -O?)\n"

1578 )

1579 self.issue_config_time_warning(

1580 PytestConfigWarning(warning_text),

1581 stacklevel=3,

1582 )

1583

1584 def _warn_about_skipped_plugins(self) -> None:

1585 for module_name, msg in self.pluginmanager.skipped_plugins:

1586 self.issue_config_time_warning(

1587 PytestConfigWarning(f"skipped plugin {module_name!r}: {msg}"),

1588 stacklevel=2,

1589 )

1590

1591

1592 def _assertion_supported() -> bool:

1593 try:

1594 assert False

1595 except AssertionError:

1596 return True

1597 else:

1598 return False # type: ignore[unreachable]

1599

1600

1601 def create_terminal_writer(

1602 config: Config, file: Optional[TextIO] = None

1603) -> TerminalWriter:

1604 """Create a TerminalWriter instance configured according to the options

1605 in the config object.

1606

1607 Every code which requires a TerminalWriter object and has access to a

1608 config object should use this function.

1609 """

1610 tw = TerminalWriter(file=file)

1611

1612 if config.option.color == "yes":

1613 tw.hasmarkup = True

1614 elif config.option.color == "no":

1615 tw.hasmarkup = False

1616

1617 if config.option.code_highlight == "yes":

1618 tw.code_highlight = True

1619 elif config.option.code_highlight == "no":

1620 tw.code_highlight = False

1621

1622 return tw

1623

1624

1625 def _strtobool(val: str) -> bool:

1626 """Convert a string representation of truth to True or False.

1627

1628 True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values

1629 are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if

1630 'val' is anything else.

1631

1632 .. note:: Copied from distutils.util.

1633 """

1634 val = val.lower()

1635 if val in ("y", "yes", "t", "true", "on", "1"):

1636 return True

1637 elif val in ("n", "no", "f", "false", "off", "0"):

1638 return False

1639 else:

1640 raise ValueError(f"invalid truth value {val!r}")

1641

1642

1643@lru_cache(maxsize=50)

1644 def parse_warning_filter(

1645 arg: str, *, escape: bool

1646) -> Tuple["warnings._ActionKind", str, Type[Warning], str, int]:

1647 """Parse a warnings filter string.

1648

1649 This is copied from warnings._setoption with the following changes:

1650

1651 * Does not apply the filter.

1652 * Escaping is optional.

1653 * Raises UsageError so we get nice error messages on failure.

1654 """

1655 __tracebackhide__ = True

1656 error_template = dedent(

1657 f"""\

1658 while parsing the following warning configuration:

1659

1660 {arg}

1661

1662 This error occurred:

1663

1664 {{error}}

1665 """

1666 )

1667

1668 parts = arg.split(":")

1669 if len(parts) > 5:

1670 doc_url = (

1671 "https://docs.python.org/3/library/warnings.html#describing-warning-filters"

1672 )

1673 error = dedent(

1674 f"""\

1675 Too many fields ({len(parts)}), expected at most 5 separated by colons:

1676

1677 action:message:category:module:line

1678

1679 For more information please consult: {doc_url}

1680 """

1681 )

1682 raise UsageError(error_template.format(error=error))

1683

1684 while len(parts) < 5:

1685 parts.append("")

1686 action_, message, category_, module, lineno_ = (s.strip() for s in parts)

1687 try:

1688 action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined]

1689 except warnings._OptionError as e:

1690 raise UsageError(error_template.format(error=str(e)))

1691 try:

1692 category: Type[Warning] = _resolve_warning_category(category_)

1693 except Exception:

1694 exc_info = ExceptionInfo.from_current()

1695 exception_text = exc_info.getrepr(style="native")

1696 raise UsageError(error_template.format(error=exception_text))

1697 if message and escape:

1698 message = re.escape(message)

1699 if module and escape:

1700 module = re.escape(module) + r"\Z"

1701 if lineno_:

1702 try:

1703 lineno = int(lineno_)

1704 if lineno < 0:

1705 raise ValueError("number is negative")

1706 except ValueError as e:

1707 raise UsageError(

1708 error_template.format(error=f"invalid lineno {lineno_!r}: {e}")

1709 )

1710 else:

1711 lineno = 0

1712 return action, message, category, module, lineno

1713

1714

1715 def _resolve_warning_category(category: str) -> Type[Warning]:

1716 """

1717 Copied from warnings._getcategory, but changed so it lets exceptions (specially ImportErrors)

1718 propagate so we can get access to their tracebacks (#9218).

1719 """

1720 __tracebackhide__ = True

1721 if not category:

1722 return Warning

1723

1724 if "." not in category:

1725 import builtins as m

1726

1727 klass = category

1728 else:

1729 module, _, klass = category.rpartition(".")

1730 m = __import__(module, None, None, [klass])

1731 cat = getattr(m, klass)

1732 if not issubclass(cat, Warning):

1733 raise UsageError(f"{cat} is not a Warning subclass")

1734 return cast(Type[Warning], cat)

1735

1736

1737 def apply_warning_filters(

1738 config_filters: Iterable[str], cmdline_filters: Iterable[str]

1739) -> None:

1740 """Applies pytest-configured filters to the warnings module"""

1741 # Filters should have this precedence: cmdline options, config.

1742 # Filters should be applied in the inverse order of precedence.

1743 for arg in config_filters:

1744 warnings.filterwarnings(*parse_warning_filter(arg, escape=False))

1745

1746 for arg in cmdline_filters:

1747 warnings.filterwarnings(*parse_warning_filter(arg, escape=True))