Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/_pytest/config/__init__.py : 20%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1""" command line options, ini-file and conftest.py processing. """
2import argparse
3import collections.abc
4import contextlib
5import copy
6import enum
7import inspect
8import os
9import shlex
10import sys
11import types
12import warnings
13from functools import lru_cache
14from types import TracebackType
15from typing import Any
16from typing import Callable
17from typing import Dict
18from typing import IO
19from typing import Iterable
20from typing import Iterator
21from typing import List
22from typing import Optional
23from typing import Sequence
24from typing import Set
25from typing import TextIO
26from typing import Tuple
27from typing import Union
29import attr
30import py
31from pluggy import HookimplMarker
32from pluggy import HookspecMarker
33from pluggy import PluginManager
35import _pytest._code
36import _pytest.deprecated
37import _pytest.hookspec # the extension point definitions
38from .exceptions import PrintHelp
39from .exceptions import UsageError
40from .findpaths import determine_setup
41from _pytest._code import ExceptionInfo
42from _pytest._code import filter_traceback
43from _pytest._io import TerminalWriter
44from _pytest.compat import importlib_metadata
45from _pytest.compat import TYPE_CHECKING
46from _pytest.outcomes import fail
47from _pytest.outcomes import Skipped
48from _pytest.pathlib import import_path
49from _pytest.pathlib import ImportMode
50from _pytest.pathlib import Path
51from _pytest.store import Store
52from _pytest.warning_types import PytestConfigWarning
54if TYPE_CHECKING:
55 from typing import Type
57 from _pytest._code.code import _TracebackStyle
58 from _pytest.terminal import TerminalReporter
59 from .argparsing import Argument
62_PluggyPlugin = object
63"""A type to represent plugin objects.
64Plugins can be any namespace, so we can't narrow it down much, but we use an
65alias to make the intent clear.
66Ideally this type would be provided by pluggy itself."""
69hookimpl = HookimplMarker("pytest")
70hookspec = HookspecMarker("pytest")
73class ExitCode(enum.IntEnum):
74 """
75 .. versionadded:: 5.0
77 Encodes the valid exit codes by pytest.
79 Currently users and plugins may supply other exit codes as well.
80 """
82 #: tests passed
83 OK = 0
84 #: tests failed
85 TESTS_FAILED = 1
86 #: pytest was interrupted
87 INTERRUPTED = 2
88 #: an internal error got in the way
89 INTERNAL_ERROR = 3
90 #: pytest was misused
91 USAGE_ERROR = 4
92 #: pytest couldn't find tests
93 NO_TESTS_COLLECTED = 5
96class ConftestImportFailure(Exception):
97 def __init__(
98 self,
99 path: py.path.local,
100 excinfo: Tuple["Type[Exception]", Exception, TracebackType],
101 ) -> None:
102 super().__init__(path, excinfo)
103 self.path = path
104 self.excinfo = excinfo
106 def __str__(self) -> str:
107 return "{}: {} (from {})".format(
108 self.excinfo[0].__name__, self.excinfo[1], self.path
109 )
112def filter_traceback_for_conftest_import_failure(
113 entry: _pytest._code.TracebackEntry,
114) -> bool:
115 """filters tracebacks entries which point to pytest internals or importlib.
117 Make a special case for importlib because we use it to import test modules and conftest files
118 in _pytest.pathlib.import_path.
119 """
120 return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep)
123def main(
124 args: Optional[List[str]] = None,
125 plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
126) -> Union[int, ExitCode]:
127 """ return exit code, after performing an in-process test run.
129 :arg args: list of command line arguments.
131 :arg plugins: list of plugin objects to be auto-registered during
132 initialization.
133 """
134 try:
135 try:
136 config = _prepareconfig(args, plugins)
137 except ConftestImportFailure as e:
138 exc_info = ExceptionInfo(e.excinfo)
139 tw = TerminalWriter(sys.stderr)
140 tw.line(
141 "ImportError while loading conftest '{e.path}'.".format(e=e), red=True
142 )
143 exc_info.traceback = exc_info.traceback.filter(
144 filter_traceback_for_conftest_import_failure
145 )
146 exc_repr = (
147 exc_info.getrepr(style="short", chain=False)
148 if exc_info.traceback
149 else exc_info.exconly()
150 )
151 formatted_tb = str(exc_repr)
152 for line in formatted_tb.splitlines():
153 tw.line(line.rstrip(), red=True)
154 return ExitCode.USAGE_ERROR
155 else:
156 try:
157 ret = config.hook.pytest_cmdline_main(
158 config=config
159 ) # type: Union[ExitCode, int]
160 try:
161 return ExitCode(ret)
162 except ValueError:
163 return ret
164 finally:
165 config._ensure_unconfigure()
166 except UsageError as e:
167 tw = TerminalWriter(sys.stderr)
168 for msg in e.args:
169 tw.line("ERROR: {}\n".format(msg), red=True)
170 return ExitCode.USAGE_ERROR
173def console_main() -> int:
174 """pytest's CLI entry point.
176 This function is not meant for programmable use; use `main()` instead.
177 """
178 # https://docs.python.org/3/library/signal.html#note-on-sigpipe
179 try:
180 code = main()
181 sys.stdout.flush()
182 return code
183 except BrokenPipeError:
184 # Python flushes standard streams on exit; redirect remaining output
185 # to devnull to avoid another BrokenPipeError at shutdown
186 devnull = os.open(os.devnull, os.O_WRONLY)
187 os.dup2(devnull, sys.stdout.fileno())
188 return 1 # Python exits with error code 1 on EPIPE
191class cmdline: # compatibility namespace
192 main = staticmethod(main)
195def filename_arg(path: str, optname: str) -> str:
196 """ Argparse type validator for filename arguments.
198 :path: path of filename
199 :optname: name of the option
200 """
201 if os.path.isdir(path):
202 raise UsageError("{} must be a filename, given: {}".format(optname, path))
203 return path
206def directory_arg(path: str, optname: str) -> str:
207 """Argparse type validator for directory arguments.
209 :path: path of directory
210 :optname: name of the option
211 """
212 if not os.path.isdir(path):
213 raise UsageError("{} must be a directory, given: {}".format(optname, path))
214 return path
217# Plugins that cannot be disabled via "-p no:X" currently.
218essential_plugins = (
219 "mark",
220 "main",
221 "runner",
222 "fixtures",
223 "helpconfig", # Provides -p.
224)
226default_plugins = essential_plugins + (
227 "python",
228 "terminal",
229 "debugging",
230 "unittest",
231 "capture",
232 "skipping",
233 "tmpdir",
234 "monkeypatch",
235 "recwarn",
236 "pastebin",
237 "nose",
238 "assertion",
239 "junitxml",
240 "resultlog",
241 "doctest",
242 "cacheprovider",
243 "freeze_support",
244 "setuponly",
245 "setupplan",
246 "stepwise",
247 "warnings",
248 "logging",
249 "reports",
250 "faulthandler",
251)
253builtin_plugins = set(default_plugins)
254builtin_plugins.add("pytester")
257def get_config(
258 args: Optional[List[str]] = None,
259 plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
260) -> "Config":
261 # subsequent calls to main will create a fresh instance
262 pluginmanager = PytestPluginManager()
263 config = Config(
264 pluginmanager,
265 invocation_params=Config.InvocationParams(
266 args=args or (), plugins=plugins, dir=Path.cwd(),
267 ),
268 )
270 if args is not None:
271 # Handle any "-p no:plugin" args.
272 pluginmanager.consider_preparse(args, exclude_only=True)
274 for spec in default_plugins:
275 pluginmanager.import_plugin(spec)
277 return config
280def get_plugin_manager() -> "PytestPluginManager":
281 """
282 Obtain a new instance of the
283 :py:class:`_pytest.config.PytestPluginManager`, with default plugins
284 already loaded.
286 This function can be used by integration with other tools, like hooking
287 into pytest to run tests into an IDE.
288 """
289 return get_config().pluginmanager
292def _prepareconfig(
293 args: Optional[Union[py.path.local, List[str]]] = None,
294 plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
295) -> "Config":
296 if args is None:
297 args = sys.argv[1:]
298 elif isinstance(args, py.path.local):
299 args = [str(args)]
300 elif not isinstance(args, list):
301 msg = "`args` parameter expected to be a list of strings, got: {!r} (type: {})"
302 raise TypeError(msg.format(args, type(args)))
304 config = get_config(args, plugins)
305 pluginmanager = config.pluginmanager
306 try:
307 if plugins:
308 for plugin in plugins:
309 if isinstance(plugin, str):
310 pluginmanager.consider_pluginarg(plugin)
311 else:
312 pluginmanager.register(plugin)
313 config = pluginmanager.hook.pytest_cmdline_parse(
314 pluginmanager=pluginmanager, args=args
315 )
316 return config
317 except BaseException:
318 config._ensure_unconfigure()
319 raise
322class PytestPluginManager(PluginManager):
323 """
324 Overwrites :py:class:`pluggy.PluginManager <pluggy.PluginManager>` to add pytest-specific
325 functionality:
327 * loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and
328 ``pytest_plugins`` global variables found in plugins being loaded;
329 * ``conftest.py`` loading during start-up;
330 """
332 def __init__(self) -> None:
333 import _pytest.assertion
335 super().__init__("pytest")
336 # The objects are module objects, only used generically.
337 self._conftest_plugins = set() # type: Set[types.ModuleType]
339 # State related to local conftest plugins.
340 self._dirpath2confmods = {} # type: Dict[py.path.local, List[types.ModuleType]]
341 self._conftestpath2mod = {} # type: Dict[Path, types.ModuleType]
342 self._confcutdir = None # type: Optional[py.path.local]
343 self._noconftest = False
344 self._duplicatepaths = set() # type: Set[py.path.local]
346 self.add_hookspecs(_pytest.hookspec)
347 self.register(self)
348 if os.environ.get("PYTEST_DEBUG"):
349 err = sys.stderr # type: IO[str]
350 encoding = getattr(err, "encoding", "utf8") # type: str
351 try:
352 err = open(
353 os.dup(err.fileno()), mode=err.mode, buffering=1, encoding=encoding,
354 )
355 except Exception:
356 pass
357 self.trace.root.setwriter(err.write)
358 self.enable_tracing()
360 # Config._consider_importhook will set a real object if required.
361 self.rewrite_hook = _pytest.assertion.DummyRewriteHook()
362 # Used to know when we are importing conftests after the pytest_configure stage
363 self._configured = False
365 def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):
366 # pytest hooks are always prefixed with pytest_
367 # so we avoid accessing possibly non-readable attributes
368 # (see issue #1073)
369 if not name.startswith("pytest_"):
370 return
371 # ignore names which can not be hooks
372 if name == "pytest_plugins":
373 return
375 method = getattr(plugin, name)
376 opts = super().parse_hookimpl_opts(plugin, name)
378 # consider only actual functions for hooks (#3775)
379 if not inspect.isroutine(method):
380 return
382 # collect unmarked hooks as long as they have the `pytest_' prefix
383 if opts is None and name.startswith("pytest_"):
384 opts = {}
385 if opts is not None:
386 # TODO: DeprecationWarning, people should use hookimpl
387 # https://github.com/pytest-dev/pytest/issues/4562
388 known_marks = {m.name for m in getattr(method, "pytestmark", [])}
390 for name in ("tryfirst", "trylast", "optionalhook", "hookwrapper"):
391 opts.setdefault(name, hasattr(method, name) or name in known_marks)
392 return opts
394 def parse_hookspec_opts(self, module_or_class, name: str):
395 opts = super().parse_hookspec_opts(module_or_class, name)
396 if opts is None:
397 method = getattr(module_or_class, name)
399 if name.startswith("pytest_"):
400 # todo: deprecate hookspec hacks
401 # https://github.com/pytest-dev/pytest/issues/4562
402 known_marks = {m.name for m in getattr(method, "pytestmark", [])}
403 opts = {
404 "firstresult": hasattr(method, "firstresult")
405 or "firstresult" in known_marks,
406 "historic": hasattr(method, "historic")
407 or "historic" in known_marks,
408 }
409 return opts
411 def register(
412 self, plugin: _PluggyPlugin, name: Optional[str] = None
413 ) -> Optional[str]:
414 if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS:
415 warnings.warn(
416 PytestConfigWarning(
417 "{} plugin has been merged into the core, "
418 "please remove it from your requirements.".format(
419 name.replace("_", "-")
420 )
421 )
422 )
423 return None
424 ret = super().register(plugin, name) # type: Optional[str]
425 if ret:
426 self.hook.pytest_plugin_registered.call_historic(
427 kwargs=dict(plugin=plugin, manager=self)
428 )
430 if isinstance(plugin, types.ModuleType):
431 self.consider_module(plugin)
432 return ret
434 def getplugin(self, name: str):
435 # support deprecated naming because plugins (xdist e.g.) use it
436 plugin = self.get_plugin(name) # type: Optional[_PluggyPlugin]
437 return plugin
439 def hasplugin(self, name: str) -> bool:
440 """Return True if the plugin with the given name is registered."""
441 return bool(self.get_plugin(name))
443 def pytest_configure(self, config: "Config") -> None:
444 # XXX now that the pluginmanager exposes hookimpl(tryfirst...)
445 # we should remove tryfirst/trylast as markers
446 config.addinivalue_line(
447 "markers",
448 "tryfirst: mark a hook implementation function such that the "
449 "plugin machinery will try to call it first/as early as possible.",
450 )
451 config.addinivalue_line(
452 "markers",
453 "trylast: mark a hook implementation function such that the "
454 "plugin machinery will try to call it last/as late as possible.",
455 )
456 self._configured = True
458 #
459 # internal API for local conftest plugin handling
460 #
461 def _set_initial_conftests(self, namespace: argparse.Namespace) -> None:
462 """ load initial conftest files given a preparsed "namespace".
463 As conftest files may add their own command line options
464 which have arguments ('--my-opt somepath') we might get some
465 false positives. All builtin and 3rd party plugins will have
466 been loaded, however, so common options will not confuse our logic
467 here.
468 """
469 current = py.path.local()
470 self._confcutdir = (
471 current.join(namespace.confcutdir, abs=True)
472 if namespace.confcutdir
473 else None
474 )
475 self._noconftest = namespace.noconftest
476 self._using_pyargs = namespace.pyargs
477 testpaths = namespace.file_or_dir
478 foundanchor = False
479 for testpath in testpaths:
480 path = str(testpath)
481 # remove node-id syntax
482 i = path.find("::")
483 if i != -1:
484 path = path[:i]
485 anchor = current.join(path, abs=1)
486 if anchor.exists(): # we found some file object
487 self._try_load_conftest(anchor, namespace.importmode)
488 foundanchor = True
489 if not foundanchor:
490 self._try_load_conftest(current, namespace.importmode)
492 def _try_load_conftest(
493 self, anchor: py.path.local, importmode: Union[str, ImportMode]
494 ) -> None:
495 self._getconftestmodules(anchor, importmode)
496 # let's also consider test* subdirs
497 if anchor.check(dir=1):
498 for x in anchor.listdir("test*"):
499 if x.check(dir=1):
500 self._getconftestmodules(x, importmode)
502 @lru_cache(maxsize=128)
503 def _getconftestmodules(
504 self, path: py.path.local, importmode: Union[str, ImportMode],
505 ) -> List[types.ModuleType]:
506 if self._noconftest:
507 return []
509 if path.isfile():
510 directory = path.dirpath()
511 else:
512 directory = path
514 # XXX these days we may rather want to use config.rootdir
515 # and allow users to opt into looking into the rootdir parent
516 # directories instead of requiring to specify confcutdir
517 clist = []
518 for parent in directory.parts():
519 if self._confcutdir and self._confcutdir.relto(parent):
520 continue
521 conftestpath = parent.join("conftest.py")
522 if conftestpath.isfile():
523 mod = self._importconftest(conftestpath, importmode)
524 clist.append(mod)
525 self._dirpath2confmods[directory] = clist
526 return clist
528 def _rget_with_confmod(
529 self, name: str, path: py.path.local, importmode: Union[str, ImportMode],
530 ) -> Tuple[types.ModuleType, Any]:
531 modules = self._getconftestmodules(path, importmode)
532 for mod in reversed(modules):
533 try:
534 return mod, getattr(mod, name)
535 except AttributeError:
536 continue
537 raise KeyError(name)
539 def _importconftest(
540 self, conftestpath: py.path.local, importmode: Union[str, ImportMode],
541 ) -> types.ModuleType:
542 # Use a resolved Path object as key to avoid loading the same conftest twice
543 # with build systems that create build directories containing
544 # symlinks to actual files.
545 # Using Path().resolve() is better than py.path.realpath because
546 # it resolves to the correct path/drive in case-insensitive file systems (#5792)
547 key = Path(str(conftestpath)).resolve()
549 with contextlib.suppress(KeyError):
550 return self._conftestpath2mod[key]
552 pkgpath = conftestpath.pypkgpath()
553 if pkgpath is None:
554 _ensure_removed_sysmodule(conftestpath.purebasename)
556 try:
557 mod = import_path(conftestpath, mode=importmode)
558 except Exception as e:
559 assert e.__traceback__ is not None
560 exc_info = (type(e), e, e.__traceback__)
561 raise ConftestImportFailure(conftestpath, exc_info) from e
563 self._check_non_top_pytest_plugins(mod, conftestpath)
565 self._conftest_plugins.add(mod)
566 self._conftestpath2mod[key] = mod
567 dirpath = conftestpath.dirpath()
568 if dirpath in self._dirpath2confmods:
569 for path, mods in self._dirpath2confmods.items():
570 if path and path.relto(dirpath) or path == dirpath:
571 assert mod not in mods
572 mods.append(mod)
573 self.trace("loading conftestmodule {!r}".format(mod))
574 self.consider_conftest(mod)
575 return mod
577 def _check_non_top_pytest_plugins(
578 self, mod: types.ModuleType, conftestpath: py.path.local,
579 ) -> None:
580 if (
581 hasattr(mod, "pytest_plugins")
582 and self._configured
583 and not self._using_pyargs
584 ):
585 msg = (
586 "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n"
587 "It affects the entire test suite instead of just below the conftest as expected.\n"
588 " {}\n"
589 "Please move it to a top level conftest file at the rootdir:\n"
590 " {}\n"
591 "For more information, visit:\n"
592 " https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files"
593 )
594 fail(msg.format(conftestpath, self._confcutdir), pytrace=False)
596 #
597 # API for bootstrapping plugin loading
598 #
599 #
601 def consider_preparse(
602 self, args: Sequence[str], *, exclude_only: bool = False
603 ) -> None:
604 i = 0
605 n = len(args)
606 while i < n:
607 opt = args[i]
608 i += 1
609 if isinstance(opt, str):
610 if opt == "-p":
611 try:
612 parg = args[i]
613 except IndexError:
614 return
615 i += 1
616 elif opt.startswith("-p"):
617 parg = opt[2:]
618 else:
619 continue
620 if exclude_only and not parg.startswith("no:"):
621 continue
622 self.consider_pluginarg(parg)
624 def consider_pluginarg(self, arg: str) -> None:
625 if arg.startswith("no:"):
626 name = arg[3:]
627 if name in essential_plugins:
628 raise UsageError("plugin %s cannot be disabled" % name)
630 # PR #4304 : remove stepwise if cacheprovider is blocked
631 if name == "cacheprovider":
632 self.set_blocked("stepwise")
633 self.set_blocked("pytest_stepwise")
635 self.set_blocked(name)
636 if not name.startswith("pytest_"):
637 self.set_blocked("pytest_" + name)
638 else:
639 name = arg
640 # Unblock the plugin. None indicates that it has been blocked.
641 # There is no interface with pluggy for this.
642 if self._name2plugin.get(name, -1) is None:
643 del self._name2plugin[name]
644 if not name.startswith("pytest_"):
645 if self._name2plugin.get("pytest_" + name, -1) is None:
646 del self._name2plugin["pytest_" + name]
647 self.import_plugin(arg, consider_entry_points=True)
649 def consider_conftest(self, conftestmodule: types.ModuleType) -> None:
650 self.register(conftestmodule, name=conftestmodule.__file__)
652 def consider_env(self) -> None:
653 self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS"))
655 def consider_module(self, mod: types.ModuleType) -> None:
656 self._import_plugin_specs(getattr(mod, "pytest_plugins", []))
658 def _import_plugin_specs(
659 self, spec: Union[None, types.ModuleType, str, Sequence[str]]
660 ) -> None:
661 plugins = _get_plugin_specs_as_list(spec)
662 for import_spec in plugins:
663 self.import_plugin(import_spec)
665 def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:
666 """
667 Imports a plugin with ``modname``. If ``consider_entry_points`` is True, entry point
668 names are also considered to find a plugin.
669 """
670 # most often modname refers to builtin modules, e.g. "pytester",
671 # "terminal" or "capture". Those plugins are registered under their
672 # basename for historic purposes but must be imported with the
673 # _pytest prefix.
674 assert isinstance(modname, str), (
675 "module name as text required, got %r" % modname
676 )
677 if self.is_blocked(modname) or self.get_plugin(modname) is not None:
678 return
680 importspec = "_pytest." + modname if modname in builtin_plugins else modname
681 self.rewrite_hook.mark_rewrite(importspec)
683 if consider_entry_points:
684 loaded = self.load_setuptools_entrypoints("pytest11", name=modname)
685 if loaded:
686 return
688 try:
689 __import__(importspec)
690 except ImportError as e:
691 raise ImportError(
692 'Error importing plugin "{}": {}'.format(modname, str(e.args[0]))
693 ).with_traceback(e.__traceback__) from e
695 except Skipped as e:
696 from _pytest.warnings import _issue_warning_captured
698 _issue_warning_captured(
699 PytestConfigWarning("skipped plugin {!r}: {}".format(modname, e.msg)),
700 self.hook,
701 stacklevel=2,
702 )
703 else:
704 mod = sys.modules[importspec]
705 self.register(mod, modname)
708def _get_plugin_specs_as_list(
709 specs: Union[None, types.ModuleType, str, Sequence[str]]
710) -> List[str]:
711 """Parse a plugins specification into a list of plugin names."""
712 # None means empty.
713 if specs is None:
714 return []
715 # Workaround for #3899 - a submodule which happens to be called "pytest_plugins".
716 if isinstance(specs, types.ModuleType):
717 return []
718 # Comma-separated list.
719 if isinstance(specs, str):
720 return specs.split(",") if specs else []
721 # Direct specification.
722 if isinstance(specs, collections.abc.Sequence):
723 return list(specs)
724 raise UsageError(
725 "Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %r"
726 % specs
727 )
730def _ensure_removed_sysmodule(modname: str) -> None:
731 try:
732 del sys.modules[modname]
733 except KeyError:
734 pass
737class Notset:
738 def __repr__(self):
739 return "<NOTSET>"
742notset = Notset()
745def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
746 """
747 Given an iterable of file names in a source distribution, return the "names" that should
748 be marked for assertion rewrite (for example the package "pytest_mock/__init__.py" should
749 be added as "pytest_mock" in the assertion rewrite mechanism.
751 This function has to deal with dist-info based distributions and egg based distributions
752 (which are still very much in use for "editable" installs).
754 Here are the file names as seen in a dist-info based distribution:
756 pytest_mock/__init__.py
757 pytest_mock/_version.py
758 pytest_mock/plugin.py
759 pytest_mock.egg-info/PKG-INFO
761 Here are the file names as seen in an egg based distribution:
763 src/pytest_mock/__init__.py
764 src/pytest_mock/_version.py
765 src/pytest_mock/plugin.py
766 src/pytest_mock.egg-info/PKG-INFO
767 LICENSE
768 setup.py
770 We have to take in account those two distribution flavors in order to determine which
771 names should be considered for assertion rewriting.
773 More information:
774 https://github.com/pytest-dev/pytest-mock/issues/167
775 """
776 package_files = list(package_files)
777 seen_some = False
778 for fn in package_files:
779 is_simple_module = "/" not in fn and fn.endswith(".py")
780 is_package = fn.count("/") == 1 and fn.endswith("__init__.py")
781 if is_simple_module:
782 module_name, _ = os.path.splitext(fn)
783 # we ignore "setup.py" at the root of the distribution
784 if module_name != "setup":
785 seen_some = True
786 yield module_name
787 elif is_package:
788 package_name = os.path.dirname(fn)
789 seen_some = True
790 yield package_name
792 if not seen_some:
793 # at this point we did not find any packages or modules suitable for assertion
794 # rewriting, so we try again by stripping the first path component (to account for
795 # "src" based source trees for example)
796 # this approach lets us have the common case continue to be fast, as egg-distributions
797 # are rarer
798 new_package_files = []
799 for fn in package_files:
800 parts = fn.split("/")
801 new_fn = "/".join(parts[1:])
802 if new_fn:
803 new_package_files.append(new_fn)
804 if new_package_files:
805 yield from _iter_rewritable_modules(new_package_files)
808def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:
809 return tuple(args)
812class Config:
813 """
814 Access to configuration values, pluginmanager and plugin hooks.
816 :param PytestPluginManager pluginmanager:
818 :param InvocationParams invocation_params:
819 Object containing the parameters regarding the ``pytest.main``
820 invocation.
821 """
823 @attr.s(frozen=True)
824 class InvocationParams:
825 """Holds parameters passed during ``pytest.main()``
827 The object attributes are read-only.
829 .. versionadded:: 5.1
831 .. note::
833 Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts``
834 ini option are handled by pytest, not being included in the ``args`` attribute.
836 Plugins accessing ``InvocationParams`` must be aware of that.
837 """
839 args = attr.ib(type=Tuple[str, ...], converter=_args_converter)
840 """tuple of command-line arguments as passed to ``pytest.main()``."""
841 plugins = attr.ib(type=Optional[Sequence[Union[str, _PluggyPlugin]]])
842 """list of extra plugins, might be `None`."""
843 dir = attr.ib(type=Path)
844 """directory where ``pytest.main()`` was invoked from."""
846 def __init__(
847 self,
848 pluginmanager: PytestPluginManager,
849 *,
850 invocation_params: Optional[InvocationParams] = None
851 ) -> None:
852 from .argparsing import Parser, FILE_OR_DIR
854 if invocation_params is None:
855 invocation_params = self.InvocationParams(
856 args=(), plugins=None, dir=Path.cwd()
857 )
859 self.option = argparse.Namespace()
860 """access to command line option as attributes.
862 :type: argparse.Namespace"""
864 self.invocation_params = invocation_params
866 _a = FILE_OR_DIR
867 self._parser = Parser(
868 usage="%(prog)s [options] [{}] [{}] [...]".format(_a, _a),
869 processopt=self._processopt,
870 )
871 self.pluginmanager = pluginmanager
872 """the plugin manager handles plugin registration and hook invocation.
874 :type: PytestPluginManager"""
876 self.trace = self.pluginmanager.trace.root.get("config")
877 self.hook = self.pluginmanager.hook
878 self._inicache = {} # type: Dict[str, Any]
879 self._override_ini = () # type: Sequence[str]
880 self._opt2dest = {} # type: Dict[str, str]
881 self._cleanup = [] # type: List[Callable[[], None]]
882 # A place where plugins can store information on the config for their
883 # own use. Currently only intended for internal plugins.
884 self._store = Store()
885 self.pluginmanager.register(self, "pytestconfig")
886 self._configured = False
887 self.hook.pytest_addoption.call_historic(
888 kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
889 )
891 if TYPE_CHECKING:
892 from _pytest.cacheprovider import Cache
894 self.cache = None # type: Optional[Cache]
896 @property
897 def invocation_dir(self) -> py.path.local:
898 """Backward compatibility"""
899 return py.path.local(str(self.invocation_params.dir))
901 def add_cleanup(self, func: Callable[[], None]) -> None:
902 """ Add a function to be called when the config object gets out of
903 use (usually coninciding with pytest_unconfigure)."""
904 self._cleanup.append(func)
906 def _do_configure(self) -> None:
907 assert not self._configured
908 self._configured = True
909 with warnings.catch_warnings():
910 warnings.simplefilter("default")
911 self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
913 def _ensure_unconfigure(self) -> None:
914 if self._configured:
915 self._configured = False
916 self.hook.pytest_unconfigure(config=self)
917 self.hook.pytest_configure._call_history = []
918 while self._cleanup:
919 fin = self._cleanup.pop()
920 fin()
922 def get_terminal_writer(self) -> TerminalWriter:
923 terminalreporter = self.pluginmanager.get_plugin(
924 "terminalreporter"
925 ) # type: TerminalReporter
926 return terminalreporter._tw
928 def pytest_cmdline_parse(
929 self, pluginmanager: PytestPluginManager, args: List[str]
930 ) -> "Config":
931 try:
932 self.parse(args)
933 except UsageError:
935 # Handle --version and --help here in a minimal fashion.
936 # This gets done via helpconfig normally, but its
937 # pytest_cmdline_main is not called in case of errors.
938 if getattr(self.option, "version", False) or "--version" in args:
939 from _pytest.helpconfig import showversion
941 showversion(self)
942 elif (
943 getattr(self.option, "help", False) or "--help" in args or "-h" in args
944 ):
945 self._parser._getparser().print_help()
946 sys.stdout.write(
947 "\nNOTE: displaying only minimal help due to UsageError.\n\n"
948 )
950 raise
952 return self
954 def notify_exception(
955 self,
956 excinfo: ExceptionInfo[BaseException],
957 option: Optional[argparse.Namespace] = None,
958 ) -> None:
959 if option and getattr(option, "fulltrace", False):
960 style = "long" # type: _TracebackStyle
961 else:
962 style = "native"
963 excrepr = excinfo.getrepr(
964 funcargs=True, showlocals=getattr(option, "showlocals", False), style=style
965 )
966 res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo)
967 if not any(res):
968 for line in str(excrepr).split("\n"):
969 sys.stderr.write("INTERNALERROR> %s\n" % line)
970 sys.stderr.flush()
972 def cwd_relative_nodeid(self, nodeid: str) -> str:
973 # nodeid's are relative to the rootpath, compute relative to cwd
974 if self.invocation_dir != self.rootdir:
975 fullpath = self.rootdir.join(nodeid)
976 nodeid = self.invocation_dir.bestrelpath(fullpath)
977 return nodeid
979 @classmethod
980 def fromdictargs(cls, option_dict, args) -> "Config":
981 """ constructor usable for subprocesses. """
982 config = get_config(args)
983 config.option.__dict__.update(option_dict)
984 config.parse(args, addopts=False)
985 for x in config.option.plugins:
986 config.pluginmanager.consider_pluginarg(x)
987 return config
989 def _processopt(self, opt: "Argument") -> None:
990 for name in opt._short_opts + opt._long_opts:
991 self._opt2dest[name] = opt.dest
993 if hasattr(opt, "default"):
994 if not hasattr(self.option, opt.dest):
995 setattr(self.option, opt.dest, opt.default)
997 @hookimpl(trylast=True)
998 def pytest_load_initial_conftests(self, early_config: "Config") -> None:
999 self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
1001 def _initini(self, args: Sequence[str]) -> None:
1002 ns, unknown_args = self._parser.parse_known_and_unknown_args(
1003 args, namespace=copy.copy(self.option)
1004 )
1005 self.rootdir, self.inifile, self.inicfg = determine_setup(
1006 ns.inifilename,
1007 ns.file_or_dir + unknown_args,
1008 rootdir_cmd_arg=ns.rootdir or None,
1009 config=self,
1010 )
1011 self._parser.extra_info["rootdir"] = self.rootdir
1012 self._parser.extra_info["inifile"] = self.inifile
1013 self._parser.addini("addopts", "extra command line options", "args")
1014 self._parser.addini("minversion", "minimally required pytest version")
1015 self._parser.addini(
1016 "required_plugins",
1017 "plugins that must be present for pytest to run",
1018 type="args",
1019 default=[],
1020 )
1021 self._override_ini = ns.override_ini or ()
1023 def _consider_importhook(self, args: Sequence[str]) -> None:
1024 """Install the PEP 302 import hook if using assertion rewriting.
1026 Needs to parse the --assert=<mode> option from the commandline
1027 and find all the installed plugins to mark them for rewriting
1028 by the importhook.
1029 """
1030 ns, unknown_args = self._parser.parse_known_and_unknown_args(args)
1031 mode = getattr(ns, "assertmode", "plain")
1032 if mode == "rewrite":
1033 import _pytest.assertion
1035 try:
1036 hook = _pytest.assertion.install_importhook(self)
1037 except SystemError:
1038 mode = "plain"
1039 else:
1040 self._mark_plugins_for_rewrite(hook)
1041 self._warn_about_missing_assertion(mode)
1043 def _mark_plugins_for_rewrite(self, hook) -> None:
1044 """
1045 Given an importhook, mark for rewrite any top-level
1046 modules or packages in the distribution package for
1047 all pytest plugins.
1048 """
1049 self.pluginmanager.rewrite_hook = hook
1051 if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
1052 # We don't autoload from setuptools entry points, no need to continue.
1053 return
1055 package_files = (
1056 str(file)
1057 for dist in importlib_metadata.distributions()
1058 if any(ep.group == "pytest11" for ep in dist.entry_points)
1059 for file in dist.files or []
1060 )
1062 for name in _iter_rewritable_modules(package_files):
1063 hook.mark_rewrite(name)
1065 def _validate_args(self, args: List[str], via: str) -> List[str]:
1066 """Validate known args."""
1067 self._parser._config_source_hint = via # type: ignore
1068 try:
1069 self._parser.parse_known_and_unknown_args(
1070 args, namespace=copy.copy(self.option)
1071 )
1072 finally:
1073 del self._parser._config_source_hint # type: ignore
1075 return args
1077 def _preparse(self, args: List[str], addopts: bool = True) -> None:
1078 if addopts:
1079 env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
1080 if len(env_addopts):
1081 args[:] = (
1082 self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")
1083 + args
1084 )
1085 self._initini(args)
1086 if addopts:
1087 args[:] = (
1088 self._validate_args(self.getini("addopts"), "via addopts config") + args
1089 )
1091 self._checkversion()
1092 self._consider_importhook(args)
1093 self.pluginmanager.consider_preparse(args, exclude_only=False)
1094 if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
1095 # Don't autoload from setuptools entry point. Only explicitly specified
1096 # plugins are going to be loaded.
1097 self.pluginmanager.load_setuptools_entrypoints("pytest11")
1098 self.pluginmanager.consider_env()
1099 self.known_args_namespace = ns = self._parser.parse_known_args(
1100 args, namespace=copy.copy(self.option)
1101 )
1102 self._validate_plugins()
1103 if self.known_args_namespace.confcutdir is None and self.inifile:
1104 confcutdir = py.path.local(self.inifile).dirname
1105 self.known_args_namespace.confcutdir = confcutdir
1106 try:
1107 self.hook.pytest_load_initial_conftests(
1108 early_config=self, args=args, parser=self._parser
1109 )
1110 except ConftestImportFailure as e:
1111 if ns.help or ns.version:
1112 # we don't want to prevent --help/--version to work
1113 # so just let is pass and print a warning at the end
1114 from _pytest.warnings import _issue_warning_captured
1116 _issue_warning_captured(
1117 PytestConfigWarning(
1118 "could not load initial conftests: {}".format(e.path)
1119 ),
1120 self.hook,
1121 stacklevel=2,
1122 )
1123 else:
1124 raise
1125 self._validate_keys()
1127 def _checkversion(self) -> None:
1128 import pytest
1130 minver = self.inicfg.get("minversion", None)
1131 if minver:
1132 # Imported lazily to improve start-up time.
1133 from packaging.version import Version
1135 if not isinstance(minver, str):
1136 raise pytest.UsageError(
1137 "%s: 'minversion' must be a single value" % self.inifile
1138 )
1140 if Version(minver) > Version(pytest.__version__):
1141 raise pytest.UsageError(
1142 "%s: 'minversion' requires pytest-%s, actual pytest-%s'"
1143 % (self.inifile, minver, pytest.__version__,)
1144 )
1146 def _validate_keys(self) -> None:
1147 for key in sorted(self._get_unknown_ini_keys()):
1148 self._warn_or_fail_if_strict("Unknown config ini key: {}\n".format(key))
1150 def _validate_plugins(self) -> None:
1151 required_plugins = sorted(self.getini("required_plugins"))
1152 if not required_plugins:
1153 return
1155 # Imported lazily to improve start-up time.
1156 from packaging.version import Version
1157 from packaging.requirements import InvalidRequirement, Requirement
1159 plugin_info = self.pluginmanager.list_plugin_distinfo()
1160 plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}
1162 missing_plugins = []
1163 for required_plugin in required_plugins:
1164 spec = None
1165 try:
1166 spec = Requirement(required_plugin)
1167 except InvalidRequirement:
1168 missing_plugins.append(required_plugin)
1169 continue
1171 if spec.name not in plugin_dist_info:
1172 missing_plugins.append(required_plugin)
1173 elif Version(plugin_dist_info[spec.name]) not in spec.specifier:
1174 missing_plugins.append(required_plugin)
1176 if missing_plugins:
1177 fail(
1178 "Missing required plugins: {}".format(", ".join(missing_plugins)),
1179 pytrace=False,
1180 )
1182 def _warn_or_fail_if_strict(self, message: str) -> None:
1183 if self.known_args_namespace.strict_config:
1184 fail(message, pytrace=False)
1186 from _pytest.warnings import _issue_warning_captured
1188 _issue_warning_captured(
1189 PytestConfigWarning(message), self.hook, stacklevel=3,
1190 )
1192 def _get_unknown_ini_keys(self) -> List[str]:
1193 parser_inicfg = self._parser._inidict
1194 return [name for name in self.inicfg if name not in parser_inicfg]
1196 def parse(self, args: List[str], addopts: bool = True) -> None:
1197 # parse given cmdline arguments into this config object.
1198 assert not hasattr(
1199 self, "args"
1200 ), "can only parse cmdline args at most once per Config object"
1201 self.hook.pytest_addhooks.call_historic(
1202 kwargs=dict(pluginmanager=self.pluginmanager)
1203 )
1204 self._preparse(args, addopts=addopts)
1205 # XXX deprecated hook:
1206 self.hook.pytest_cmdline_preparse(config=self, args=args)
1207 self._parser.after_preparse = True # type: ignore
1208 try:
1209 args = self._parser.parse_setoption(
1210 args, self.option, namespace=self.option
1211 )
1212 if not args:
1213 if self.invocation_dir == self.rootdir:
1214 args = self.getini("testpaths")
1215 if not args:
1216 args = [str(self.invocation_dir)]
1217 self.args = args
1218 except PrintHelp:
1219 pass
1221 def addinivalue_line(self, name: str, line: str) -> None:
1222 """ add a line to an ini-file option. The option must have been
1223 declared but might not yet be set in which case the line becomes the
1224 the first line in its value. """
1225 x = self.getini(name)
1226 assert isinstance(x, list)
1227 x.append(line) # modifies the cached list inline
1229 def getini(self, name: str):
1230 """ return configuration value from an :ref:`ini file <configfiles>`. If the
1231 specified name hasn't been registered through a prior
1232 :py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>`
1233 call (usually from a plugin), a ValueError is raised. """
1234 try:
1235 return self._inicache[name]
1236 except KeyError:
1237 self._inicache[name] = val = self._getini(name)
1238 return val
1240 def _getini(self, name: str):
1241 try:
1242 description, type, default = self._parser._inidict[name]
1243 except KeyError as e:
1244 raise ValueError("unknown configuration value: {!r}".format(name)) from e
1245 override_value = self._get_override_ini_value(name)
1246 if override_value is None:
1247 try:
1248 value = self.inicfg[name]
1249 except KeyError:
1250 if default is not None:
1251 return default
1252 if type is None:
1253 return ""
1254 return []
1255 else:
1256 value = override_value
1257 # coerce the values based on types
1258 # note: some coercions are only required if we are reading from .ini files, because
1259 # the file format doesn't contain type information, but when reading from toml we will
1260 # get either str or list of str values (see _parse_ini_config_from_pyproject_toml).
1261 # for example:
1262 #
1263 # ini:
1264 # a_line_list = "tests acceptance"
1265 # in this case, we need to split the string to obtain a list of strings
1266 #
1267 # toml:
1268 # a_line_list = ["tests", "acceptance"]
1269 # in this case, we already have a list ready to use
1270 #
1271 if type == "pathlist":
1272 # TODO: This assert is probably not valid in all cases.
1273 assert self.inifile is not None
1274 dp = py.path.local(self.inifile).dirpath()
1275 input_values = shlex.split(value) if isinstance(value, str) else value
1276 return [dp.join(x, abs=True) for x in input_values]
1277 elif type == "args":
1278 return shlex.split(value) if isinstance(value, str) else value
1279 elif type == "linelist":
1280 if isinstance(value, str):
1281 return [t for t in map(lambda x: x.strip(), value.split("\n")) if t]
1282 else:
1283 return value
1284 elif type == "bool":
1285 return _strtobool(str(value).strip())
1286 else:
1287 assert type is None
1288 return value
1290 def _getconftest_pathlist(
1291 self, name: str, path: py.path.local
1292 ) -> Optional[List[py.path.local]]:
1293 try:
1294 mod, relroots = self.pluginmanager._rget_with_confmod(
1295 name, path, self.getoption("importmode")
1296 )
1297 except KeyError:
1298 return None
1299 modpath = py.path.local(mod.__file__).dirpath()
1300 values = [] # type: List[py.path.local]
1301 for relroot in relroots:
1302 if not isinstance(relroot, py.path.local):
1303 relroot = relroot.replace("/", py.path.local.sep)
1304 relroot = modpath.join(relroot, abs=True)
1305 values.append(relroot)
1306 return values
1308 def _get_override_ini_value(self, name: str) -> Optional[str]:
1309 value = None
1310 # override_ini is a list of "ini=value" options
1311 # always use the last item if multiple values are set for same ini-name,
1312 # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2
1313 for ini_config in self._override_ini:
1314 try:
1315 key, user_ini_value = ini_config.split("=", 1)
1316 except ValueError as e:
1317 raise UsageError(
1318 "-o/--override-ini expects option=value style (got: {!r}).".format(
1319 ini_config
1320 )
1321 ) from e
1322 else:
1323 if key == name:
1324 value = user_ini_value
1325 return value
1327 def getoption(self, name: str, default=notset, skip: bool = False):
1328 """ return command line option value.
1330 :arg name: name of the option. You may also specify
1331 the literal ``--OPT`` option instead of the "dest" option name.
1332 :arg default: default value if no option of that name exists.
1333 :arg skip: if True raise pytest.skip if option does not exists
1334 or has a None value.
1335 """
1336 name = self._opt2dest.get(name, name)
1337 try:
1338 val = getattr(self.option, name)
1339 if val is None and skip:
1340 raise AttributeError(name)
1341 return val
1342 except AttributeError as e:
1343 if default is not notset:
1344 return default
1345 if skip:
1346 import pytest
1348 pytest.skip("no {!r} option found".format(name))
1349 raise ValueError("no option named {!r}".format(name)) from e
1351 def getvalue(self, name: str, path=None):
1352 """ (deprecated, use getoption()) """
1353 return self.getoption(name)
1355 def getvalueorskip(self, name: str, path=None):
1356 """ (deprecated, use getoption(skip=True)) """
1357 return self.getoption(name, skip=True)
1359 def _warn_about_missing_assertion(self, mode: str) -> None:
1360 if not _assertion_supported():
1361 from _pytest.warnings import _issue_warning_captured
1363 if mode == "plain":
1364 warning_text = (
1365 "ASSERTIONS ARE NOT EXECUTED"
1366 " and FAILING TESTS WILL PASS. Are you"
1367 " using python -O?"
1368 )
1369 else:
1370 warning_text = (
1371 "assertions not in test modules or"
1372 " plugins will be ignored"
1373 " because assert statements are not executed "
1374 "by the underlying Python interpreter "
1375 "(are you using python -O?)\n"
1376 )
1377 _issue_warning_captured(
1378 PytestConfigWarning(warning_text), self.hook, stacklevel=3,
1379 )
1382def _assertion_supported() -> bool:
1383 try:
1384 assert False
1385 except AssertionError:
1386 return True
1387 else:
1388 return False
1391def create_terminal_writer(
1392 config: Config, file: Optional[TextIO] = None
1393) -> TerminalWriter:
1394 """Create a TerminalWriter instance configured according to the options
1395 in the config object. Every code which requires a TerminalWriter object
1396 and has access to a config object should use this function.
1397 """
1398 tw = TerminalWriter(file=file)
1399 if config.option.color == "yes":
1400 tw.hasmarkup = True
1401 elif config.option.color == "no":
1402 tw.hasmarkup = False
1404 if config.option.code_highlight == "yes":
1405 tw.code_highlight = True
1406 elif config.option.code_highlight == "no":
1407 tw.code_highlight = False
1408 return tw
1411def _strtobool(val: str) -> bool:
1412 """Convert a string representation of truth to True or False.
1414 True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
1415 are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
1416 'val' is anything else.
1418 .. note:: copied from distutils.util
1419 """
1420 val = val.lower()
1421 if val in ("y", "yes", "t", "true", "on", "1"):
1422 return True
1423 elif val in ("n", "no", "f", "false", "off", "0"):
1424 return False
1425 else:
1426 raise ValueError("invalid truth value {!r}".format(val))