Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/config/__init__.py: 58%
877 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
1"""Command line options, ini-file and conftest.py processing."""
2import argparse
3import collections.abc
4import copy
5import enum
6import glob
7import inspect
8import os
9import re
10import shlex
11import sys
12import types
13import warnings
14from functools import lru_cache
15from pathlib import Path
16from textwrap import dedent
17from types import FunctionType
18from types import TracebackType
19from typing import Any
20from typing import Callable
21from typing import cast
22from typing import Dict
23from typing import Generator
24from typing import IO
25from typing import Iterable
26from typing import Iterator
27from typing import List
28from typing import Optional
29from typing import Sequence
30from typing import Set
31from typing import TextIO
32from typing import Tuple
33from typing import Type
34from typing import TYPE_CHECKING
35from typing import Union
37import attr
38from pluggy import HookimplMarker
39from pluggy import HookspecMarker
40from pluggy import PluginManager
42import _pytest._code
43import _pytest.deprecated
44import _pytest.hookspec
45from .exceptions import PrintHelp as PrintHelp
46from .exceptions import UsageError as UsageError
47from .findpaths import determine_setup
48from _pytest._code import ExceptionInfo
49from _pytest._code import filter_traceback
50from _pytest._io import TerminalWriter
51from _pytest.compat import final
52from _pytest.compat import importlib_metadata
53from _pytest.outcomes import fail
54from _pytest.outcomes import Skipped
55from _pytest.pathlib import absolutepath
56from _pytest.pathlib import bestrelpath
57from _pytest.pathlib import import_path
58from _pytest.pathlib import ImportMode
59from _pytest.pathlib import resolve_package_path
60from _pytest.stash import Stash
61from _pytest.warning_types import PytestConfigWarning
62from _pytest.warning_types import warn_explicit_for
64if TYPE_CHECKING:
66 from _pytest._code.code import _TracebackStyle
67 from _pytest.terminal import TerminalReporter
68 from .argparsing import Argument
71_PluggyPlugin = object
72"""A type to represent plugin objects.
74Plugins can be any namespace, so we can't narrow it down much, but we use an
75alias to make the intent clear.
77Ideally this type would be provided by pluggy itself.
78"""
81hookimpl = HookimplMarker("pytest")
82hookspec = HookspecMarker("pytest")
85@final
86class ExitCode(enum.IntEnum):
87 """Encodes the valid exit codes by pytest.
89 Currently users and plugins may supply other exit codes as well.
91 .. versionadded:: 5.0
92 """
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
108class 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
118 def __str__(self) -> str:
119 return "{}: {} (from {})".format(
120 self.excinfo[0].__name__, self.excinfo[1], self.path
121 )
124def filter_traceback_for_conftest_import_failure(
125 entry: _pytest._code.TracebackEntry,
126) -> bool:
127 """Filter tracebacks entries which point to pytest internals or importlib.
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)
135def 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.
141 :param args: List of command line arguments.
142 :param plugins: List of plugin objects to be auto-registered during initialization.
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
183def console_main() -> int:
184 """The CLI entry point of pytest.
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
201class cmdline: # compatibility namespace
202 main = staticmethod(main)
205def filename_arg(path: str, optname: str) -> str:
206 """Argparse type validator for filename arguments.
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
216def directory_arg(path: str, optname: str) -> str:
217 """Argparse type validator for directory arguments.
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
227# Plugins that cannot be disabled via "-p no:X" currently.
228essential_plugins = (
229 "mark",
230 "main",
231 "runner",
232 "fixtures",
233 "helpconfig", # Provides -p.
234)
236default_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)
265builtin_plugins = set(default_plugins)
266builtin_plugins.add("pytester")
267builtin_plugins.add("pytester_assertions")
270def 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 )
285 if args is not None:
286 # Handle any "-p no:plugin" args.
287 pluginmanager.consider_preparse(args, exclude_only=True)
289 for spec in default_plugins:
290 pluginmanager.import_plugin(spec)
292 return config
295def get_plugin_manager() -> "PytestPluginManager":
296 """Obtain a new instance of the
297 :py:class:`pytest.PytestPluginManager`, with default plugins
298 already loaded.
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
306def _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)))
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
338def _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
346def _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
378@final
379class PytestPluginManager(PluginManager):
380 """A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
381 additional pytest-specific functionality:
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 """
388 def __init__(self) -> None:
389 import _pytest.assertion
391 super().__init__("pytest")
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
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)
410 self._duplicatepaths: Set[Path] = set()
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]] = []
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()
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
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
451 opts = super().parse_hookimpl_opts(plugin, name)
452 if opts is not None:
453 return opts
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 )
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
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 )
495 if isinstance(plugin, types.ModuleType):
496 self.consider_module(plugin)
497 return ret
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
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))
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
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".
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)
562 def _is_in_confcutdir(self, path: Path) -> bool:
563 """Whether a path is within the confcutdir.
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
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)
581 def _getconftestmodules(
582 self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
583 ) -> Sequence[types.ModuleType]:
584 if self._noconftest:
585 return []
587 directory = self._get_directory(path)
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
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
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)
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)
630 pkgpath = resolve_package_path(conftestpath)
631 if pkgpath is None:
632 _ensure_removed_sysmodule(conftestpath.stem)
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
641 self._check_non_top_pytest_plugins(mod, conftestpath)
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
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)
675 #
676 # API for bootstrapping plugin loading
677 #
678 #
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)
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)
711 # PR #4304: remove stepwise if cacheprovider is blocked.
712 if name == "cacheprovider":
713 self.set_blocked("stepwise")
714 self.set_blocked("pytest_stepwise")
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)
730 def consider_conftest(self, conftestmodule: types.ModuleType) -> None:
731 """:meta private:"""
732 self.register(conftestmodule, name=conftestmodule.__file__)
734 def consider_env(self) -> None:
735 """:meta private:"""
736 self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS"))
738 def consider_module(self, mod: types.ModuleType) -> None:
739 """:meta private:"""
740 self._import_plugin_specs(getattr(mod, "pytest_plugins", []))
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)
749 def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:
750 """Import a plugin with ``modname``.
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
765 importspec = "_pytest." + modname if modname in builtin_plugins else modname
766 self.rewrite_hook.mark_rewrite(importspec)
768 if consider_entry_points:
769 loaded = self.load_setuptools_entrypoints("pytest11", name=modname)
770 if loaded:
771 return
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
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)
787def _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 )
809def _ensure_removed_sysmodule(modname: str) -> None:
810 try:
811 del sys.modules[modname]
812 except KeyError:
813 pass
816class Notset:
817 def __repr__(self):
818 return "<NOTSET>"
821notset = Notset()
824def _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.
828 For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in
829 the assertion rewrite mechanism.
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).
834 Here are the file names as seen in a dist-info based distribution:
836 pytest_mock/__init__.py
837 pytest_mock/_version.py
838 pytest_mock/plugin.py
839 pytest_mock.egg-info/PKG-INFO
841 Here are the file names as seen in an egg based distribution:
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
850 We have to take in account those two distribution flavors in order to determine which
851 names should be considered for assertion rewriting.
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
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)
889def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:
890 return tuple(args)
893@final
894class Config:
895 """Access to configuration values, pluginmanager and plugin hooks.
897 :param PytestPluginManager pluginmanager:
898 A pytest PluginManager.
900 :param InvocationParams invocation_params:
901 Object containing parameters regarding the :func:`pytest.main`
902 invocation.
903 """
905 @final
906 @attr.s(frozen=True, auto_attribs=True)
907 class InvocationParams:
908 """Holds parameters passed during :func:`pytest.main`.
910 The object attributes are read-only.
912 .. versionadded:: 5.1
914 .. note::
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.
919 Plugins accessing ``InvocationParams`` must be aware of that.
920 """
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."""
929 class ArgsSource(enum.Enum):
930 """Indicates the source of the test arguments.
932 .. versionadded:: 7.2
933 """
935 #: Command line arguments.
936 ARGS = enum.auto()
937 #: Invocation directory.
938 INCOVATION_DIR = enum.auto()
939 #: 'testpaths' configuration value.
940 TESTPATHS = enum.auto()
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
950 if invocation_params is None:
951 invocation_params = self.InvocationParams(
952 args=(), plugins=None, dir=Path.cwd()
953 )
955 self.option = argparse.Namespace()
956 """Access to command line option as attributes.
958 :type: argparse.Namespace
959 """
961 self.invocation_params = invocation_params
962 """The parameters with which pytest was invoked.
964 :type: InvocationParams
965 """
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.
976 :type: PytestPluginManager
977 """
979 self.stash = Stash()
980 """A place where plugins can store information on the config for their
981 own use.
983 :type: Stash
984 """
985 # Deprecated alias. Was never public. Can be removed in a few releases.
986 self._store = self.stash
988 from .compat import PathAwareHookProxy
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] = []
1004 if TYPE_CHECKING:
1005 from _pytest.cacheprovider import Cache
1007 self.cache: Optional[Cache] = None
1009 @property
1010 def rootpath(self) -> Path:
1011 """The path to the :ref:`rootdir <rootdir>`.
1013 :type: pathlib.Path
1015 .. versionadded:: 6.1
1016 """
1017 return self._rootpath
1019 @property
1020 def inipath(self) -> Optional[Path]:
1021 """The path to the :ref:`configfile <configfiles>`.
1023 :type: Optional[pathlib.Path]
1025 .. versionadded:: 6.1
1026 """
1027 return self._inipath
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)
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))
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()
1050 def get_terminal_writer(self) -> TerminalWriter:
1051 terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
1052 "terminalreporter"
1053 )
1054 return terminalreporter._tw
1056 def pytest_cmdline_parse(
1057 self, pluginmanager: PytestPluginManager, args: List[str]
1058 ) -> "Config":
1059 try:
1060 self.parse(args)
1061 except UsageError:
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
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 )
1078 raise
1080 return self
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()
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
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
1117 def _processopt(self, opt: "Argument") -> None:
1118 for name in opt._short_opts + opt._long_opts:
1119 self._opt2dest[name] = opt.dest
1121 if hasattr(opt, "default"):
1122 if not hasattr(self.option, opt.dest):
1123 setattr(self.option, opt.dest, opt.default)
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 )
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 ()
1156 def _consider_importhook(self, args: Sequence[str]) -> None:
1157 """Install the PEP 302 import hook if using assertion rewriting.
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
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)
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
1182 if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
1183 # We don't autoload from setuptools entry points, no need to continue.
1184 return
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 )
1193 for name in _iter_rewritable_modules(package_files):
1194 hook.mark_rewrite(name)
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
1206 return args
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 )
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()
1234 self.known_args_namespace = self._parser.parse_known_args(
1235 args, namespace=copy.copy(self.known_args_namespace)
1236 )
1238 self._validate_plugins()
1239 self._warn_about_skipped_plugins()
1241 if self.known_args_namespace.strict:
1242 self.issue_config_time_warning(
1243 _pytest.deprecated.STRICT_OPTION, stacklevel=2
1244 )
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
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()
1271 def _checkversion(self) -> None:
1272 import pytest
1274 minver = self.inicfg.get("minversion", None)
1275 if minver:
1276 # Imported lazily to improve start-up time.
1277 from packaging.version import Version
1279 if not isinstance(minver, str):
1280 raise pytest.UsageError(
1281 "%s: 'minversion' must be a single value" % self.inipath
1282 )
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 )
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")
1298 def _validate_plugins(self) -> None:
1299 required_plugins = sorted(self.getini("required_plugins"))
1300 if not required_plugins:
1301 return
1303 # Imported lazily to improve start-up time.
1304 from packaging.version import Version
1305 from packaging.requirements import InvalidRequirement, Requirement
1307 plugin_info = self.pluginmanager.list_plugin_distinfo()
1308 plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}
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
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)
1325 if missing_plugins:
1326 raise UsageError(
1327 "Missing required plugins: {}".format(", ".join(missing_plugins)),
1328 )
1330 def _warn_or_fail_if_strict(self, message: str) -> None:
1331 if self.known_args_namespace.strict_config:
1332 raise UsageError(message)
1334 self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3)
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]
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
1375 def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None:
1376 """Issue and handle a warning during the "configure" stage.
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``.
1381 This function is mainly intended for plugins that need to issue warnings during
1382 ``pytest_configure`` (or similar stages).
1384 :param warning: The warning instance.
1385 :param stacklevel: stacklevel forwarded to warnings.warn.
1386 """
1387 if self.pluginmanager.is_blocked("warnings"):
1388 return
1390 cmdline_filters = self.known_args_namespace.pythonwarnings or []
1391 config_filters = self.getini("filterwarnings")
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)
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 )
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
1418 def getini(self, name: str):
1419 """Return configuration value from an :ref:`ini file <configfiles>`.
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
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
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)
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
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
1531 def getoption(self, name: str, default=notset, skip: bool = False):
1532 """Return command line option value.
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
1552 pytest.skip(f"no {name!r} option found")
1553 raise ValueError(f"no option named {name!r}") from e
1555 def getvalue(self, name: str, path=None):
1556 """Deprecated, use getoption() instead."""
1557 return self.getoption(name)
1559 def getvalueorskip(self, name: str, path=None):
1560 """Deprecated, use getoption(skip=True) instead."""
1561 return self.getoption(name, skip=True)
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 )
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 )
1592def _assertion_supported() -> bool:
1593 try:
1594 assert False
1595 except AssertionError:
1596 return True
1597 else:
1598 return False # type: ignore[unreachable]
1601def 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.
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)
1612 if config.option.color == "yes":
1613 tw.hasmarkup = True
1614 elif config.option.color == "no":
1615 tw.hasmarkup = False
1617 if config.option.code_highlight == "yes":
1618 tw.code_highlight = True
1619 elif config.option.code_highlight == "no":
1620 tw.code_highlight = False
1622 return tw
1625def _strtobool(val: str) -> bool:
1626 """Convert a string representation of truth to True or False.
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.
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}")
1643@lru_cache(maxsize=50)
1644def parse_warning_filter(
1645 arg: str, *, escape: bool
1646) -> Tuple["warnings._ActionKind", str, Type[Warning], str, int]:
1647 """Parse a warnings filter string.
1649 This is copied from warnings._setoption with the following changes:
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:
1660 {arg}
1662 This error occurred:
1664 {{error}}
1665 """
1666 )
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:
1677 action:message:category:module:line
1679 For more information please consult: {doc_url}
1680 """
1681 )
1682 raise UsageError(error_template.format(error=error))
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
1715def _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
1724 if "." not in category:
1725 import builtins as m
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)
1737def 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))
1746 for arg in cmdline_filters:
1747 warnings.filterwarnings(*parse_warning_filter(arg, escape=True))