diff options
author | shadchin <shadchin@yandex-team.ru> | 2022-02-10 16:44:30 +0300 |
---|---|---|
committer | Daniil Cherednik <dcherednik@yandex-team.ru> | 2022-02-10 16:44:30 +0300 |
commit | 2598ef1d0aee359b4b6d5fdd1758916d5907d04f (patch) | |
tree | 012bb94d777798f1f56ac1cec429509766d05181 /contrib/python/pytest/py3/_pytest/config | |
parent | 6751af0b0c1b952fede40b19b71da8025b5d8bcf (diff) | |
download | ydb-2598ef1d0aee359b4b6d5fdd1758916d5907d04f.tar.gz |
Restoring authorship annotation for <shadchin@yandex-team.ru>. Commit 1 of 2.
Diffstat (limited to 'contrib/python/pytest/py3/_pytest/config')
-rw-r--r-- | contrib/python/pytest/py3/_pytest/config/__init__.py | 2110 | ||||
-rw-r--r-- | contrib/python/pytest/py3/_pytest/config/argparsing.py | 528 | ||||
-rw-r--r-- | contrib/python/pytest/py3/_pytest/config/exceptions.py | 12 | ||||
-rw-r--r-- | contrib/python/pytest/py3/_pytest/config/findpaths.py | 318 |
4 files changed, 1484 insertions, 1484 deletions
diff --git a/contrib/python/pytest/py3/_pytest/config/__init__.py b/contrib/python/pytest/py3/_pytest/config/__init__.py index bd9e2883f9..fc04943216 100644 --- a/contrib/python/pytest/py3/_pytest/config/__init__.py +++ b/contrib/python/pytest/py3/_pytest/config/__init__.py @@ -1,236 +1,236 @@ -"""Command line options, ini-file and conftest.py processing.""" +"""Command line options, ini-file and conftest.py processing.""" import argparse -import collections.abc -import contextlib +import collections.abc +import contextlib import copy -import enum +import enum import inspect import os -import re +import re import shlex import sys import types import warnings -from functools import lru_cache -from pathlib import Path -from types import TracebackType -from typing import Any -from typing import Callable -from typing import Dict -from typing import Generator -from typing import IO -from typing import Iterable -from typing import Iterator -from typing import List -from typing import Optional -from typing import Sequence -from typing import Set -from typing import TextIO -from typing import Tuple -from typing import Type -from typing import TYPE_CHECKING -from typing import Union - -import attr +from functools import lru_cache +from pathlib import Path +from types import TracebackType +from typing import Any +from typing import Callable +from typing import Dict +from typing import Generator +from typing import IO +from typing import Iterable +from typing import Iterator +from typing import List +from typing import Optional +from typing import Sequence +from typing import Set +from typing import TextIO +from typing import Tuple +from typing import Type +from typing import TYPE_CHECKING +from typing import Union + +import attr import py from pluggy import HookimplMarker from pluggy import HookspecMarker from pluggy import PluginManager import _pytest._code -import _pytest.deprecated -import _pytest.hookspec -from .exceptions import PrintHelp as PrintHelp -from .exceptions import UsageError as UsageError +import _pytest.deprecated +import _pytest.hookspec +from .exceptions import PrintHelp as PrintHelp +from .exceptions import UsageError as UsageError from .findpaths import determine_setup from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback -from _pytest._io import TerminalWriter -from _pytest.compat import final -from _pytest.compat import importlib_metadata -from _pytest.outcomes import fail +from _pytest._io import TerminalWriter +from _pytest.compat import final +from _pytest.compat import importlib_metadata +from _pytest.outcomes import fail from _pytest.outcomes import Skipped -from _pytest.pathlib import bestrelpath -from _pytest.pathlib import import_path -from _pytest.pathlib import ImportMode -from _pytest.store import Store -from _pytest.warning_types import PytestConfigWarning - -if TYPE_CHECKING: - - from _pytest._code.code import _TracebackStyle - from _pytest.terminal import TerminalReporter - from .argparsing import Argument - - -_PluggyPlugin = object -"""A type to represent plugin objects. - -Plugins can be any namespace, so we can't narrow it down much, but we use an -alias to make the intent clear. - -Ideally this type would be provided by pluggy itself. -""" - - +from _pytest.pathlib import bestrelpath +from _pytest.pathlib import import_path +from _pytest.pathlib import ImportMode +from _pytest.store import Store +from _pytest.warning_types import PytestConfigWarning + +if TYPE_CHECKING: + + from _pytest._code.code import _TracebackStyle + from _pytest.terminal import TerminalReporter + from .argparsing import Argument + + +_PluggyPlugin = object +"""A type to represent plugin objects. + +Plugins can be any namespace, so we can't narrow it down much, but we use an +alias to make the intent clear. + +Ideally this type would be provided by pluggy itself. +""" + + hookimpl = HookimplMarker("pytest") hookspec = HookspecMarker("pytest") -@final -class ExitCode(enum.IntEnum): - """Encodes the valid exit codes by pytest. - - Currently users and plugins may supply other exit codes as well. - - .. versionadded:: 5.0 - """ - - #: Tests passed. - OK = 0 - #: Tests failed. - TESTS_FAILED = 1 - #: pytest was interrupted. - INTERRUPTED = 2 - #: An internal error got in the way. - INTERNAL_ERROR = 3 - #: pytest was misused. - USAGE_ERROR = 4 - #: pytest couldn't find tests. - NO_TESTS_COLLECTED = 5 - - +@final +class ExitCode(enum.IntEnum): + """Encodes the valid exit codes by pytest. + + Currently users and plugins may supply other exit codes as well. + + .. versionadded:: 5.0 + """ + + #: Tests passed. + OK = 0 + #: Tests failed. + TESTS_FAILED = 1 + #: pytest was interrupted. + INTERRUPTED = 2 + #: An internal error got in the way. + INTERNAL_ERROR = 3 + #: pytest was misused. + USAGE_ERROR = 4 + #: pytest couldn't find tests. + NO_TESTS_COLLECTED = 5 + + class ConftestImportFailure(Exception): - def __init__( - self, - path: py.path.local, - excinfo: Tuple[Type[Exception], Exception, TracebackType], - ) -> None: - super().__init__(path, excinfo) + def __init__( + self, + path: py.path.local, + excinfo: Tuple[Type[Exception], Exception, TracebackType], + ) -> None: + super().__init__(path, excinfo) self.path = path - self.excinfo = excinfo - - def __str__(self) -> str: - return "{}: {} (from {})".format( - self.excinfo[0].__name__, self.excinfo[1], self.path - ) - - -def filter_traceback_for_conftest_import_failure( - entry: _pytest._code.TracebackEntry, -) -> bool: - """Filter tracebacks entries which point to pytest internals or importlib. - - Make a special case for importlib because we use it to import test modules and conftest files - in _pytest.pathlib.import_path. - """ - return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep) + self.excinfo = excinfo + def __str__(self) -> str: + return "{}: {} (from {})".format( + self.excinfo[0].__name__, self.excinfo[1], self.path + ) -def main( - args: Optional[Union[List[str], py.path.local]] = None, - plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, -) -> Union[int, ExitCode]: - """Perform an in-process test run. - :param args: List of command line arguments. - :param plugins: List of plugin objects to be auto-registered during initialization. +def filter_traceback_for_conftest_import_failure( + entry: _pytest._code.TracebackEntry, +) -> bool: + """Filter tracebacks entries which point to pytest internals or importlib. - :returns: An exit code. + Make a special case for importlib because we use it to import test modules and conftest files + in _pytest.pathlib.import_path. """ + return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep) + + +def main( + args: Optional[Union[List[str], py.path.local]] = None, + plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, +) -> Union[int, ExitCode]: + """Perform an in-process test run. + + :param args: List of command line arguments. + :param plugins: List of plugin objects to be auto-registered during initialization. + + :returns: An exit code. + """ try: try: config = _prepareconfig(args, plugins) except ConftestImportFailure as e: exc_info = ExceptionInfo(e.excinfo) - tw = TerminalWriter(sys.stderr) - tw.line(f"ImportError while loading conftest '{e.path}'.", red=True) - exc_info.traceback = exc_info.traceback.filter( - filter_traceback_for_conftest_import_failure + tw = TerminalWriter(sys.stderr) + tw.line(f"ImportError while loading conftest '{e.path}'.", red=True) + exc_info.traceback = exc_info.traceback.filter( + filter_traceback_for_conftest_import_failure ) exc_repr = ( exc_info.getrepr(style="short", chain=False) if exc_info.traceback else exc_info.exconly() ) - formatted_tb = str(exc_repr) + formatted_tb = str(exc_repr) for line in formatted_tb.splitlines(): tw.line(line.rstrip(), red=True) - return ExitCode.USAGE_ERROR + return ExitCode.USAGE_ERROR else: try: - ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main( - config=config - ) - try: - return ExitCode(ret) - except ValueError: - return ret + ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main( + config=config + ) + try: + return ExitCode(ret) + except ValueError: + return ret finally: config._ensure_unconfigure() except UsageError as e: - tw = TerminalWriter(sys.stderr) + tw = TerminalWriter(sys.stderr) for msg in e.args: - tw.line(f"ERROR: {msg}\n", red=True) - return ExitCode.USAGE_ERROR - - -def console_main() -> int: - """The CLI entry point of pytest. - - This function is not meant for programmable use; use `main()` instead. - """ - # https://docs.python.org/3/library/signal.html#note-on-sigpipe - try: - code = main() - sys.stdout.flush() - return code - except BrokenPipeError: - # Python flushes standard streams on exit; redirect remaining output - # to devnull to avoid another BrokenPipeError at shutdown - devnull = os.open(os.devnull, os.O_WRONLY) - os.dup2(devnull, sys.stdout.fileno()) - return 1 # Python exits with error code 1 on EPIPE - - -class cmdline: # compatibility namespace + tw.line(f"ERROR: {msg}\n", red=True) + return ExitCode.USAGE_ERROR + + +def console_main() -> int: + """The CLI entry point of pytest. + + This function is not meant for programmable use; use `main()` instead. + """ + # https://docs.python.org/3/library/signal.html#note-on-sigpipe + try: + code = main() + sys.stdout.flush() + return code + except BrokenPipeError: + # Python flushes standard streams on exit; redirect remaining output + # to devnull to avoid another BrokenPipeError at shutdown + devnull = os.open(os.devnull, os.O_WRONLY) + os.dup2(devnull, sys.stdout.fileno()) + return 1 # Python exits with error code 1 on EPIPE + + +class cmdline: # compatibility namespace main = staticmethod(main) -def filename_arg(path: str, optname: str) -> str: - """Argparse type validator for filename arguments. +def filename_arg(path: str, optname: str) -> str: + """Argparse type validator for filename arguments. - :path: Path of filename. - :optname: Name of the option. + :path: Path of filename. + :optname: Name of the option. """ if os.path.isdir(path): - raise UsageError(f"{optname} must be a filename, given: {path}") + raise UsageError(f"{optname} must be a filename, given: {path}") return path -def directory_arg(path: str, optname: str) -> str: +def directory_arg(path: str, optname: str) -> str: """Argparse type validator for directory arguments. - :path: Path of directory. - :optname: Name of the option. + :path: Path of directory. + :optname: Name of the option. """ if not os.path.isdir(path): - raise UsageError(f"{optname} must be a directory, given: {path}") + raise UsageError(f"{optname} must be a directory, given: {path}") return path -# Plugins that cannot be disabled via "-p no:X" currently. -essential_plugins = ( +# Plugins that cannot be disabled via "-p no:X" currently. +essential_plugins = ( "mark", "main", "runner", - "fixtures", - "helpconfig", # Provides -p. -) - -default_plugins = essential_plugins + ( + "fixtures", + "helpconfig", # Provides -p. +) + +default_plugins = essential_plugins + ( "python", - "terminal", + "terminal", "debugging", "unittest", "capture", @@ -250,41 +250,41 @@ default_plugins = essential_plugins + ( "stepwise", "warnings", "logging", - "reports", - *(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []), - "faulthandler", + "reports", + *(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []), + "faulthandler", ) builtin_plugins = set(default_plugins) builtin_plugins.add("pytester") -builtin_plugins.add("pytester_assertions") +builtin_plugins.add("pytester_assertions") -def get_config( - args: Optional[List[str]] = None, - plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, -) -> "Config": +def get_config( + args: Optional[List[str]] = None, + plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, +) -> "Config": # subsequent calls to main will create a fresh instance pluginmanager = PytestPluginManager() - config = Config( - pluginmanager, - invocation_params=Config.InvocationParams( - args=args or (), plugins=plugins, dir=Path.cwd(), - ), - ) - - if args is not None: - # Handle any "-p no:plugin" args. - pluginmanager.consider_preparse(args, exclude_only=True) - + config = Config( + pluginmanager, + invocation_params=Config.InvocationParams( + args=args or (), plugins=plugins, dir=Path.cwd(), + ), + ) + + if args is not None: + # Handle any "-p no:plugin" args. + pluginmanager.consider_preparse(args, exclude_only=True) + for spec in default_plugins: pluginmanager.import_plugin(spec) - + return config -def get_plugin_manager() -> "PytestPluginManager": - """Obtain a new instance of the +def get_plugin_manager() -> "PytestPluginManager": + """Obtain a new instance of the :py:class:`_pytest.config.PytestPluginManager`, with default plugins already loaded. @@ -294,76 +294,76 @@ def get_plugin_manager() -> "PytestPluginManager": return get_config().pluginmanager -def _prepareconfig( - args: Optional[Union[py.path.local, List[str]]] = None, - plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, -) -> "Config": +def _prepareconfig( + args: Optional[Union[py.path.local, List[str]]] = None, + plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, +) -> "Config": if args is None: args = sys.argv[1:] elif isinstance(args, py.path.local): args = [str(args)] - elif not isinstance(args, list): - msg = "`args` parameter expected to be a list of strings, got: {!r} (type: {})" - raise TypeError(msg.format(args, type(args))) + elif not isinstance(args, list): + msg = "`args` parameter expected to be a list of strings, got: {!r} (type: {})" + raise TypeError(msg.format(args, type(args))) - config = get_config(args, plugins) + config = get_config(args, plugins) pluginmanager = config.pluginmanager try: if plugins: for plugin in plugins: - if isinstance(plugin, str): + if isinstance(plugin, str): pluginmanager.consider_pluginarg(plugin) else: pluginmanager.register(plugin) - config = pluginmanager.hook.pytest_cmdline_parse( + config = pluginmanager.hook.pytest_cmdline_parse( pluginmanager=pluginmanager, args=args ) - return config + return config except BaseException: config._ensure_unconfigure() raise -@final +@final class PytestPluginManager(PluginManager): - """A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with - additional pytest-specific functionality: + """A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with + additional pytest-specific functionality: - * Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and - ``pytest_plugins`` global variables found in plugins being loaded. - * ``conftest.py`` loading during start-up. + * Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and + ``pytest_plugins`` global variables found in plugins being loaded. + * ``conftest.py`` loading during start-up. """ - def __init__(self) -> None: - import _pytest.assertion - - super().__init__("pytest") - # The objects are module objects, only used generically. - self._conftest_plugins: Set[types.ModuleType] = set() - - # State related to local conftest plugins. - self._dirpath2confmods: Dict[py.path.local, List[types.ModuleType]] = {} - self._conftestpath2mod: Dict[Path, types.ModuleType] = {} - self._confcutdir: Optional[py.path.local] = None + def __init__(self) -> None: + import _pytest.assertion + + super().__init__("pytest") + # The objects are module objects, only used generically. + self._conftest_plugins: Set[types.ModuleType] = set() + + # State related to local conftest plugins. + self._dirpath2confmods: Dict[py.path.local, List[types.ModuleType]] = {} + self._conftestpath2mod: Dict[Path, types.ModuleType] = {} + self._confcutdir: Optional[py.path.local] = None self._noconftest = False - self._duplicatepaths: Set[py.path.local] = set() - - # plugins that were explicitly skipped with pytest.skip - # list of (module name, skip reason) - # previously we would issue a warning when a plugin was skipped, but - # since we refactored warnings as first citizens of Config, they are - # just stored here to be used later. - self.skipped_plugins: List[Tuple[str, str]] = [] - + self._duplicatepaths: Set[py.path.local] = set() + + # plugins that were explicitly skipped with pytest.skip + # list of (module name, skip reason) + # previously we would issue a warning when a plugin was skipped, but + # since we refactored warnings as first citizens of Config, they are + # just stored here to be used later. + self.skipped_plugins: List[Tuple[str, str]] = [] + self.add_hookspecs(_pytest.hookspec) self.register(self) if os.environ.get("PYTEST_DEBUG"): - err: IO[str] = sys.stderr - encoding: str = getattr(err, "encoding", "utf8") + err: IO[str] = sys.stderr + encoding: str = getattr(err, "encoding", "utf8") try: - err = open( - os.dup(err.fileno()), mode=err.mode, buffering=1, encoding=encoding, - ) + err = open( + os.dup(err.fileno()), mode=err.mode, buffering=1, encoding=encoding, + ) except Exception: pass self.trace.root.setwriter(err.write) @@ -371,69 +371,69 @@ class PytestPluginManager(PluginManager): # Config._consider_importhook will set a real object if required. self.rewrite_hook = _pytest.assertion.DummyRewriteHook() - # Used to know when we are importing conftests after the pytest_configure stage. + # Used to know when we are importing conftests after the pytest_configure stage. self._configured = False - def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str): - # pytest hooks are always prefixed with "pytest_", + def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str): + # pytest hooks are always prefixed with "pytest_", # so we avoid accessing possibly non-readable attributes - # (see issue #1073). + # (see issue #1073). if not name.startswith("pytest_"): return - # Ignore names which can not be hooks. - if name == "pytest_plugins": + # Ignore names which can not be hooks. + if name == "pytest_plugins": return method = getattr(plugin, name) - opts = super().parse_hookimpl_opts(plugin, name) + opts = super().parse_hookimpl_opts(plugin, name) - # Consider only actual functions for hooks (#3775). + # Consider only actual functions for hooks (#3775). if not inspect.isroutine(method): return - # Collect unmarked hooks as long as they have the `pytest_' prefix. + # Collect unmarked hooks as long as they have the `pytest_' prefix. if opts is None and name.startswith("pytest_"): opts = {} - if opts is not None: - # TODO: DeprecationWarning, people should use hookimpl - # https://github.com/pytest-dev/pytest/issues/4562 - known_marks = {m.name for m in getattr(method, "pytestmark", [])} + if opts is not None: + # TODO: DeprecationWarning, people should use hookimpl + # https://github.com/pytest-dev/pytest/issues/4562 + known_marks = {m.name for m in getattr(method, "pytestmark", [])} for name in ("tryfirst", "trylast", "optionalhook", "hookwrapper"): - opts.setdefault(name, hasattr(method, name) or name in known_marks) + opts.setdefault(name, hasattr(method, name) or name in known_marks) return opts - def parse_hookspec_opts(self, module_or_class, name: str): - opts = super().parse_hookspec_opts(module_or_class, name) + def parse_hookspec_opts(self, module_or_class, name: str): + opts = super().parse_hookspec_opts(module_or_class, name) if opts is None: method = getattr(module_or_class, name) - + if name.startswith("pytest_"): - # todo: deprecate hookspec hacks - # https://github.com/pytest-dev/pytest/issues/4562 - known_marks = {m.name for m in getattr(method, "pytestmark", [])} + # todo: deprecate hookspec hacks + # https://github.com/pytest-dev/pytest/issues/4562 + known_marks = {m.name for m in getattr(method, "pytestmark", [])} opts = { - "firstresult": hasattr(method, "firstresult") - or "firstresult" in known_marks, - "historic": hasattr(method, "historic") - or "historic" in known_marks, + "firstresult": hasattr(method, "firstresult") + or "firstresult" in known_marks, + "historic": hasattr(method, "historic") + or "historic" in known_marks, } return opts - def register( - self, plugin: _PluggyPlugin, name: Optional[str] = None - ) -> Optional[str]: - if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS: - warnings.warn( - PytestConfigWarning( - "{} plugin has been merged into the core, " - "please remove it from your requirements.".format( - name.replace("_", "-") - ) + def register( + self, plugin: _PluggyPlugin, name: Optional[str] = None + ) -> Optional[str]: + if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS: + warnings.warn( + PytestConfigWarning( + "{} plugin has been merged into the core, " + "please remove it from your requirements.".format( + name.replace("_", "-") + ) ) ) - return None - ret: Optional[str] = super().register(plugin, name) + return None + ret: Optional[str] = super().register(plugin, name) if ret: self.hook.pytest_plugin_registered.call_historic( kwargs=dict(plugin=plugin, manager=self) @@ -443,19 +443,19 @@ class PytestPluginManager(PluginManager): self.consider_module(plugin) return ret - def getplugin(self, name: str): - # Support deprecated naming because plugins (xdist e.g.) use it. - plugin: Optional[_PluggyPlugin] = self.get_plugin(name) - return plugin + def getplugin(self, name: str): + # Support deprecated naming because plugins (xdist e.g.) use it. + plugin: Optional[_PluggyPlugin] = self.get_plugin(name) + return plugin - def hasplugin(self, name: str) -> bool: - """Return whether a plugin with the given name is registered.""" + def hasplugin(self, name: str) -> bool: + """Return whether a plugin with the given name is registered.""" return bool(self.get_plugin(name)) - def pytest_configure(self, config: "Config") -> None: - """:meta private:""" + def pytest_configure(self, config: "Config") -> None: + """:meta private:""" # XXX now that the pluginmanager exposes hookimpl(tryfirst...) - # we should remove tryfirst/trylast as markers. + # we should remove tryfirst/trylast as markers. config.addinivalue_line( "markers", "tryfirst: mark a hook implementation function such that the " @@ -469,15 +469,15 @@ class PytestPluginManager(PluginManager): self._configured = True # - # Internal API for local conftest plugin handling. + # Internal API for local conftest plugin handling. # - def _set_initial_conftests(self, namespace: argparse.Namespace) -> None: - """Load initial conftest files given a preparsed "namespace". - - As conftest files may add their own command line options which have - arguments ('--my-opt somepath') we might get some false positives. - All builtin and 3rd party plugins will have been loaded, however, so - common options will not confuse our logic here. + def _set_initial_conftests(self, namespace: argparse.Namespace) -> None: + """Load initial conftest files given a preparsed "namespace". + + As conftest files may add their own command line options which have + arguments ('--my-opt somepath') we might get some false positives. + All builtin and 3rd party plugins will have been loaded, however, so + common options will not confuse our logic here. """ current = py.path.local() self._confcutdir = ( @@ -489,33 +489,33 @@ class PytestPluginManager(PluginManager): self._using_pyargs = namespace.pyargs testpaths = namespace.file_or_dir foundanchor = False - for testpath in testpaths: - path = str(testpath) + for testpath in testpaths: + path = str(testpath) # remove node-id syntax i = path.find("::") if i != -1: path = path[:i] anchor = current.join(path, abs=1) - if anchor.exists(): # we found some file object - self._try_load_conftest(anchor, namespace.importmode) + if anchor.exists(): # we found some file object + self._try_load_conftest(anchor, namespace.importmode) foundanchor = True if not foundanchor: - self._try_load_conftest(current, namespace.importmode) + self._try_load_conftest(current, namespace.importmode) - def _try_load_conftest( - self, anchor: py.path.local, importmode: Union[str, ImportMode] - ) -> None: - self._getconftestmodules(anchor, importmode) + def _try_load_conftest( + self, anchor: py.path.local, importmode: Union[str, ImportMode] + ) -> None: + self._getconftestmodules(anchor, importmode) # let's also consider test* subdirs if anchor.check(dir=1): for x in anchor.listdir("test*"): if x.check(dir=1): - self._getconftestmodules(x, importmode) + self._getconftestmodules(x, importmode) @lru_cache(maxsize=128) - def _getconftestmodules( - self, path: py.path.local, importmode: Union[str, ImportMode], - ) -> List[types.ModuleType]: + def _getconftestmodules( + self, path: py.path.local, importmode: Union[str, ImportMode], + ) -> List[types.ModuleType]: if self._noconftest: return [] @@ -524,24 +524,24 @@ class PytestPluginManager(PluginManager): else: directory = path - # XXX these days we may rather want to use config.rootpath + # XXX these days we may rather want to use config.rootpath # and allow users to opt into looking into the rootdir parent - # directories instead of requiring to specify confcutdir. + # directories instead of requiring to specify confcutdir. clist = [] - for parent in directory.parts(): + for parent in directory.parts(): if self._confcutdir and self._confcutdir.relto(parent): continue conftestpath = parent.join("conftest.py") if conftestpath.isfile(): - mod = self._importconftest(conftestpath, importmode) + mod = self._importconftest(conftestpath, importmode) clist.append(mod) self._dirpath2confmods[directory] = clist return clist - def _rget_with_confmod( - self, name: str, path: py.path.local, importmode: Union[str, ImportMode], - ) -> Tuple[types.ModuleType, Any]: - modules = self._getconftestmodules(path, importmode) + def _rget_with_confmod( + self, name: str, path: py.path.local, importmode: Union[str, ImportMode], + ) -> Tuple[types.ModuleType, Any]: + modules = self._getconftestmodules(path, importmode) for mod in reversed(modules): try: return mod, getattr(mod, name) @@ -549,98 +549,98 @@ class PytestPluginManager(PluginManager): continue raise KeyError(name) - def _importconftest( - self, conftestpath: py.path.local, importmode: Union[str, ImportMode], - ) -> types.ModuleType: - # Use a resolved Path object as key to avoid loading the same conftest - # twice with build systems that create build directories containing - # symlinks to actual files. - # Using Path().resolve() is better than py.path.realpath because - # it resolves to the correct path/drive in case-insensitive file systems (#5792) - key = Path(str(conftestpath)).resolve() - - with contextlib.suppress(KeyError): - return self._conftestpath2mod[key] - - pkgpath = conftestpath.pypkgpath() - if pkgpath is None: - _ensure_removed_sysmodule(conftestpath.purebasename) - - try: - mod = import_path(conftestpath, mode=importmode) - except Exception as e: - assert e.__traceback__ is not None - exc_info = (type(e), e, e.__traceback__) - raise ConftestImportFailure(conftestpath, exc_info) from e - - self._check_non_top_pytest_plugins(mod, conftestpath) - - self._conftest_plugins.add(mod) - self._conftestpath2mod[key] = mod - dirpath = conftestpath.dirpath() - if dirpath in self._dirpath2confmods: - for path, mods in self._dirpath2confmods.items(): - if path and path.relto(dirpath) or path == dirpath: - assert mod not in mods - mods.append(mod) - self.trace(f"loading conftestmodule {mod!r}") - self.consider_conftest(mod) - return mod - - def _check_non_top_pytest_plugins( - self, mod: types.ModuleType, conftestpath: py.path.local, - ) -> None: - if ( - hasattr(mod, "pytest_plugins") - and self._configured - and not self._using_pyargs - ): - msg = ( - "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n" - "It affects the entire test suite instead of just below the conftest as expected.\n" - " {}\n" - "Please move it to a top level conftest file at the rootdir:\n" - " {}\n" - "For more information, visit:\n" - " https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files" - ) - fail(msg.format(conftestpath, self._confcutdir), pytrace=False) - + def _importconftest( + self, conftestpath: py.path.local, importmode: Union[str, ImportMode], + ) -> types.ModuleType: + # Use a resolved Path object as key to avoid loading the same conftest + # twice with build systems that create build directories containing + # symlinks to actual files. + # Using Path().resolve() is better than py.path.realpath because + # it resolves to the correct path/drive in case-insensitive file systems (#5792) + key = Path(str(conftestpath)).resolve() + + with contextlib.suppress(KeyError): + return self._conftestpath2mod[key] + + pkgpath = conftestpath.pypkgpath() + if pkgpath is None: + _ensure_removed_sysmodule(conftestpath.purebasename) + + try: + mod = import_path(conftestpath, mode=importmode) + except Exception as e: + assert e.__traceback__ is not None + exc_info = (type(e), e, e.__traceback__) + raise ConftestImportFailure(conftestpath, exc_info) from e + + self._check_non_top_pytest_plugins(mod, conftestpath) + + self._conftest_plugins.add(mod) + self._conftestpath2mod[key] = mod + dirpath = conftestpath.dirpath() + if dirpath in self._dirpath2confmods: + for path, mods in self._dirpath2confmods.items(): + if path and path.relto(dirpath) or path == dirpath: + assert mod not in mods + mods.append(mod) + self.trace(f"loading conftestmodule {mod!r}") + self.consider_conftest(mod) + return mod + + def _check_non_top_pytest_plugins( + self, mod: types.ModuleType, conftestpath: py.path.local, + ) -> None: + if ( + hasattr(mod, "pytest_plugins") + and self._configured + and not self._using_pyargs + ): + msg = ( + "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n" + "It affects the entire test suite instead of just below the conftest as expected.\n" + " {}\n" + "Please move it to a top level conftest file at the rootdir:\n" + " {}\n" + "For more information, visit:\n" + " https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files" + ) + fail(msg.format(conftestpath, self._confcutdir), pytrace=False) + # # API for bootstrapping plugin loading # # - def consider_preparse( - self, args: Sequence[str], *, exclude_only: bool = False - ) -> None: - i = 0 - n = len(args) - while i < n: - opt = args[i] - i += 1 - if isinstance(opt, str): - if opt == "-p": - try: - parg = args[i] - except IndexError: - return - i += 1 - elif opt.startswith("-p"): - parg = opt[2:] - else: - continue - if exclude_only and not parg.startswith("no:"): - continue - self.consider_pluginarg(parg) - - def consider_pluginarg(self, arg: str) -> None: + def consider_preparse( + self, args: Sequence[str], *, exclude_only: bool = False + ) -> None: + i = 0 + n = len(args) + while i < n: + opt = args[i] + i += 1 + if isinstance(opt, str): + if opt == "-p": + try: + parg = args[i] + except IndexError: + return + i += 1 + elif opt.startswith("-p"): + parg = opt[2:] + else: + continue + if exclude_only and not parg.startswith("no:"): + continue + self.consider_pluginarg(parg) + + def consider_pluginarg(self, arg: str) -> None: if arg.startswith("no:"): name = arg[3:] - if name in essential_plugins: - raise UsageError("plugin %s cannot be disabled" % name) - - # PR #4304: remove stepwise if cacheprovider is blocked. + if name in essential_plugins: + raise UsageError("plugin %s cannot be disabled" % name) + + # PR #4304: remove stepwise if cacheprovider is blocked. if name == "cacheprovider": self.set_blocked("stepwise") self.set_blocked("pytest_stepwise") @@ -649,100 +649,100 @@ class PytestPluginManager(PluginManager): if not name.startswith("pytest_"): self.set_blocked("pytest_" + name) else: - name = arg - # Unblock the plugin. None indicates that it has been blocked. - # There is no interface with pluggy for this. - if self._name2plugin.get(name, -1) is None: - del self._name2plugin[name] - if not name.startswith("pytest_"): - if self._name2plugin.get("pytest_" + name, -1) is None: - del self._name2plugin["pytest_" + name] - self.import_plugin(arg, consider_entry_points=True) - - def consider_conftest(self, conftestmodule: types.ModuleType) -> None: + name = arg + # Unblock the plugin. None indicates that it has been blocked. + # There is no interface with pluggy for this. + if self._name2plugin.get(name, -1) is None: + del self._name2plugin[name] + if not name.startswith("pytest_"): + if self._name2plugin.get("pytest_" + name, -1) is None: + del self._name2plugin["pytest_" + name] + self.import_plugin(arg, consider_entry_points=True) + + def consider_conftest(self, conftestmodule: types.ModuleType) -> None: self.register(conftestmodule, name=conftestmodule.__file__) - def consider_env(self) -> None: + def consider_env(self) -> None: self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS")) - def consider_module(self, mod: types.ModuleType) -> None: + def consider_module(self, mod: types.ModuleType) -> None: self._import_plugin_specs(getattr(mod, "pytest_plugins", [])) - def _import_plugin_specs( - self, spec: Union[None, types.ModuleType, str, Sequence[str]] - ) -> None: + def _import_plugin_specs( + self, spec: Union[None, types.ModuleType, str, Sequence[str]] + ) -> None: plugins = _get_plugin_specs_as_list(spec) for import_spec in plugins: self.import_plugin(import_spec) - def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None: - """Import a plugin with ``modname``. - - If ``consider_entry_points`` is True, entry point names are also - considered to find a plugin. - """ - # Most often modname refers to builtin modules, e.g. "pytester", + def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None: + """Import a plugin with ``modname``. + + If ``consider_entry_points`` is True, entry point names are also + considered to find a plugin. + """ + # Most often modname refers to builtin modules, e.g. "pytester", # "terminal" or "capture". Those plugins are registered under their # basename for historic purposes but must be imported with the # _pytest prefix. - assert isinstance(modname, str), ( + assert isinstance(modname, str), ( "module name as text required, got %r" % modname ) if self.is_blocked(modname) or self.get_plugin(modname) is not None: return - - importspec = "_pytest." + modname if modname in builtin_plugins else modname + + importspec = "_pytest." + modname if modname in builtin_plugins else modname self.rewrite_hook.mark_rewrite(importspec) - - if consider_entry_points: - loaded = self.load_setuptools_entrypoints("pytest11", name=modname) - if loaded: - return - + + if consider_entry_points: + loaded = self.load_setuptools_entrypoints("pytest11", name=modname) + if loaded: + return + try: __import__(importspec) except ImportError as e: - raise ImportError( - 'Error importing plugin "{}": {}'.format(modname, str(e.args[0])) - ).with_traceback(e.__traceback__) from e + raise ImportError( + 'Error importing plugin "{}": {}'.format(modname, str(e.args[0])) + ).with_traceback(e.__traceback__) from e except Skipped as e: - self.skipped_plugins.append((modname, e.msg or "")) + self.skipped_plugins.append((modname, e.msg or "")) else: mod = sys.modules[importspec] self.register(mod, modname) -def _get_plugin_specs_as_list( - specs: Union[None, types.ModuleType, str, Sequence[str]] -) -> List[str]: - """Parse a plugins specification into a list of plugin names.""" - # None means empty. - if specs is None: - return [] - # Workaround for #3899 - a submodule which happens to be called "pytest_plugins". - if isinstance(specs, types.ModuleType): - return [] - # Comma-separated list. - if isinstance(specs, str): - return specs.split(",") if specs else [] - # Direct specification. - if isinstance(specs, collections.abc.Sequence): +def _get_plugin_specs_as_list( + specs: Union[None, types.ModuleType, str, Sequence[str]] +) -> List[str]: + """Parse a plugins specification into a list of plugin names.""" + # None means empty. + if specs is None: + return [] + # Workaround for #3899 - a submodule which happens to be called "pytest_plugins". + if isinstance(specs, types.ModuleType): + return [] + # Comma-separated list. + if isinstance(specs, str): + return specs.split(",") if specs else [] + # Direct specification. + if isinstance(specs, collections.abc.Sequence): return list(specs) - raise UsageError( - "Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %r" - % specs - ) + raise UsageError( + "Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %r" + % specs + ) -def _ensure_removed_sysmodule(modname: str) -> None: +def _ensure_removed_sysmodule(modname: str) -> None: try: del sys.modules[modname] except KeyError: pass -class Notset: +class Notset: def __repr__(self): return "<NOTSET>" @@ -750,238 +750,238 @@ class Notset: notset = Notset() -def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]: - """Given an iterable of file names in a source distribution, return the "names" that should - be marked for assertion rewrite. - - For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in - the assertion rewrite mechanism. - - This function has to deal with dist-info based distributions and egg based distributions - (which are still very much in use for "editable" installs). - - Here are the file names as seen in a dist-info based distribution: - - pytest_mock/__init__.py - pytest_mock/_version.py - pytest_mock/plugin.py - pytest_mock.egg-info/PKG-INFO - - Here are the file names as seen in an egg based distribution: - - src/pytest_mock/__init__.py - src/pytest_mock/_version.py - src/pytest_mock/plugin.py - src/pytest_mock.egg-info/PKG-INFO - LICENSE - setup.py - - We have to take in account those two distribution flavors in order to determine which - names should be considered for assertion rewriting. - - More information: - https://github.com/pytest-dev/pytest-mock/issues/167 - """ - package_files = list(package_files) - seen_some = False +def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]: + """Given an iterable of file names in a source distribution, return the "names" that should + be marked for assertion rewrite. + + For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in + the assertion rewrite mechanism. + + This function has to deal with dist-info based distributions and egg based distributions + (which are still very much in use for "editable" installs). + + Here are the file names as seen in a dist-info based distribution: + + pytest_mock/__init__.py + pytest_mock/_version.py + pytest_mock/plugin.py + pytest_mock.egg-info/PKG-INFO + + Here are the file names as seen in an egg based distribution: + + src/pytest_mock/__init__.py + src/pytest_mock/_version.py + src/pytest_mock/plugin.py + src/pytest_mock.egg-info/PKG-INFO + LICENSE + setup.py + + We have to take in account those two distribution flavors in order to determine which + names should be considered for assertion rewriting. + + More information: + https://github.com/pytest-dev/pytest-mock/issues/167 + """ + package_files = list(package_files) + seen_some = False for fn in package_files: is_simple_module = "/" not in fn and fn.endswith(".py") is_package = fn.count("/") == 1 and fn.endswith("__init__.py") if is_simple_module: module_name, _ = os.path.splitext(fn) - # we ignore "setup.py" at the root of the distribution - if module_name != "setup": - seen_some = True - yield module_name + # we ignore "setup.py" at the root of the distribution + if module_name != "setup": + seen_some = True + yield module_name elif is_package: package_name = os.path.dirname(fn) - seen_some = True + seen_some = True yield package_name - if not seen_some: - # At this point we did not find any packages or modules suitable for assertion - # rewriting, so we try again by stripping the first path component (to account for - # "src" based source trees for example). - # This approach lets us have the common case continue to be fast, as egg-distributions - # are rarer. - new_package_files = [] - for fn in package_files: - parts = fn.split("/") - new_fn = "/".join(parts[1:]) - if new_fn: - new_package_files.append(new_fn) - if new_package_files: - yield from _iter_rewritable_modules(new_package_files) - - -def _args_converter(args: Iterable[str]) -> Tuple[str, ...]: - return tuple(args) - - -@final -class Config: - """Access to configuration values, pluginmanager and plugin hooks. - - :param PytestPluginManager pluginmanager: - - :param InvocationParams invocation_params: - Object containing parameters regarding the :func:`pytest.main` - invocation. - """ - - @final - @attr.s(frozen=True) - class InvocationParams: - """Holds parameters passed during :func:`pytest.main`. - - The object attributes are read-only. - - .. versionadded:: 5.1 - - .. note:: - - Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts`` - ini option are handled by pytest, not being included in the ``args`` attribute. - - Plugins accessing ``InvocationParams`` must be aware of that. - """ - - args = attr.ib(type=Tuple[str, ...], converter=_args_converter) - """The command-line arguments as passed to :func:`pytest.main`. - - :type: Tuple[str, ...] - """ - plugins = attr.ib(type=Optional[Sequence[Union[str, _PluggyPlugin]]]) - """Extra plugins, might be `None`. - - :type: Optional[Sequence[Union[str, plugin]]] - """ - dir = attr.ib(type=Path) - """The directory from which :func:`pytest.main` was invoked. - - :type: pathlib.Path - """ - - def __init__( - self, - pluginmanager: PytestPluginManager, - *, - invocation_params: Optional[InvocationParams] = None, - ) -> None: - from .argparsing import Parser, FILE_OR_DIR - - if invocation_params is None: - invocation_params = self.InvocationParams( - args=(), plugins=None, dir=Path.cwd() - ) - + if not seen_some: + # At this point we did not find any packages or modules suitable for assertion + # rewriting, so we try again by stripping the first path component (to account for + # "src" based source trees for example). + # This approach lets us have the common case continue to be fast, as egg-distributions + # are rarer. + new_package_files = [] + for fn in package_files: + parts = fn.split("/") + new_fn = "/".join(parts[1:]) + if new_fn: + new_package_files.append(new_fn) + if new_package_files: + yield from _iter_rewritable_modules(new_package_files) + + +def _args_converter(args: Iterable[str]) -> Tuple[str, ...]: + return tuple(args) + + +@final +class Config: + """Access to configuration values, pluginmanager and plugin hooks. + + :param PytestPluginManager pluginmanager: + + :param InvocationParams invocation_params: + Object containing parameters regarding the :func:`pytest.main` + invocation. + """ + + @final + @attr.s(frozen=True) + class InvocationParams: + """Holds parameters passed during :func:`pytest.main`. + + The object attributes are read-only. + + .. versionadded:: 5.1 + + .. note:: + + Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts`` + ini option are handled by pytest, not being included in the ``args`` attribute. + + Plugins accessing ``InvocationParams`` must be aware of that. + """ + + args = attr.ib(type=Tuple[str, ...], converter=_args_converter) + """The command-line arguments as passed to :func:`pytest.main`. + + :type: Tuple[str, ...] + """ + plugins = attr.ib(type=Optional[Sequence[Union[str, _PluggyPlugin]]]) + """Extra plugins, might be `None`. + + :type: Optional[Sequence[Union[str, plugin]]] + """ + dir = attr.ib(type=Path) + """The directory from which :func:`pytest.main` was invoked. + + :type: pathlib.Path + """ + + def __init__( + self, + pluginmanager: PytestPluginManager, + *, + invocation_params: Optional[InvocationParams] = None, + ) -> None: + from .argparsing import Parser, FILE_OR_DIR + + if invocation_params is None: + invocation_params = self.InvocationParams( + args=(), plugins=None, dir=Path.cwd() + ) + self.option = argparse.Namespace() - """Access to command line option as attributes. - - :type: argparse.Namespace - """ - - self.invocation_params = invocation_params - """The parameters with which pytest was invoked. - - :type: InvocationParams - """ - + """Access to command line option as attributes. + + :type: argparse.Namespace + """ + + self.invocation_params = invocation_params + """The parameters with which pytest was invoked. + + :type: InvocationParams + """ + _a = FILE_OR_DIR self._parser = Parser( - usage=f"%(prog)s [options] [{_a}] [{_a}] [...]", + usage=f"%(prog)s [options] [{_a}] [{_a}] [...]", processopt=self._processopt, ) self.pluginmanager = pluginmanager - """The plugin manager handles plugin registration and hook invocation. - - :type: PytestPluginManager - """ - + """The plugin manager handles plugin registration and hook invocation. + + :type: PytestPluginManager + """ + self.trace = self.pluginmanager.trace.root.get("config") self.hook = self.pluginmanager.hook - self._inicache: Dict[str, Any] = {} - self._override_ini: Sequence[str] = () - self._opt2dest: Dict[str, str] = {} - self._cleanup: List[Callable[[], None]] = [] - # A place where plugins can store information on the config for their - # own use. Currently only intended for internal plugins. - self._store = Store() + self._inicache: Dict[str, Any] = {} + self._override_ini: Sequence[str] = () + self._opt2dest: Dict[str, str] = {} + self._cleanup: List[Callable[[], None]] = [] + # A place where plugins can store information on the config for their + # own use. Currently only intended for internal plugins. + self._store = Store() self.pluginmanager.register(self, "pytestconfig") self._configured = False - self.hook.pytest_addoption.call_historic( - kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager) - ) - - if TYPE_CHECKING: - from _pytest.cacheprovider import Cache - - self.cache: Optional[Cache] = None - - @property - def invocation_dir(self) -> py.path.local: - """The directory from which pytest was invoked. - - Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`, - which is a :class:`pathlib.Path`. - - :type: py.path.local - """ - return py.path.local(str(self.invocation_params.dir)) - - @property - def rootpath(self) -> Path: - """The path to the :ref:`rootdir <rootdir>`. - - :type: pathlib.Path - - .. versionadded:: 6.1 - """ - return self._rootpath - - @property - def rootdir(self) -> py.path.local: - """The path to the :ref:`rootdir <rootdir>`. - - Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`. - - :type: py.path.local - """ - return py.path.local(str(self.rootpath)) - - @property - def inipath(self) -> Optional[Path]: - """The path to the :ref:`configfile <configfiles>`. - - :type: Optional[pathlib.Path] - - .. versionadded:: 6.1 - """ - return self._inipath - - @property - def inifile(self) -> Optional[py.path.local]: - """The path to the :ref:`configfile <configfiles>`. - - Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`. - - :type: Optional[py.path.local] - """ - return py.path.local(str(self.inipath)) if self.inipath else None - - def add_cleanup(self, func: Callable[[], None]) -> None: - """Add a function to be called when the config object gets out of + self.hook.pytest_addoption.call_historic( + kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager) + ) + + if TYPE_CHECKING: + from _pytest.cacheprovider import Cache + + self.cache: Optional[Cache] = None + + @property + def invocation_dir(self) -> py.path.local: + """The directory from which pytest was invoked. + + Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`, + which is a :class:`pathlib.Path`. + + :type: py.path.local + """ + return py.path.local(str(self.invocation_params.dir)) + + @property + def rootpath(self) -> Path: + """The path to the :ref:`rootdir <rootdir>`. + + :type: pathlib.Path + + .. versionadded:: 6.1 + """ + return self._rootpath + + @property + def rootdir(self) -> py.path.local: + """The path to the :ref:`rootdir <rootdir>`. + + Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`. + + :type: py.path.local + """ + return py.path.local(str(self.rootpath)) + + @property + def inipath(self) -> Optional[Path]: + """The path to the :ref:`configfile <configfiles>`. + + :type: Optional[pathlib.Path] + + .. versionadded:: 6.1 + """ + return self._inipath + + @property + def inifile(self) -> Optional[py.path.local]: + """The path to the :ref:`configfile <configfiles>`. + + Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`. + + :type: Optional[py.path.local] + """ + return py.path.local(str(self.inipath)) if self.inipath else None + + def add_cleanup(self, func: Callable[[], None]) -> None: + """Add a function to be called when the config object gets out of use (usually coninciding with pytest_unconfigure).""" self._cleanup.append(func) - def _do_configure(self) -> None: + def _do_configure(self) -> None: assert not self._configured self._configured = True - with warnings.catch_warnings(): - warnings.simplefilter("default") - self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) + with warnings.catch_warnings(): + warnings.simplefilter("default") + self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) - def _ensure_unconfigure(self) -> None: + def _ensure_unconfigure(self) -> None: if self._configured: self._configured = False self.hook.pytest_unconfigure(config=self) @@ -990,45 +990,45 @@ class Config: fin = self._cleanup.pop() fin() - def get_terminal_writer(self) -> TerminalWriter: - terminalreporter: TerminalReporter = self.pluginmanager.get_plugin( - "terminalreporter" - ) - return terminalreporter._tw - - def pytest_cmdline_parse( - self, pluginmanager: PytestPluginManager, args: List[str] - ) -> "Config": - try: - self.parse(args) - except UsageError: - - # Handle --version and --help here in a minimal fashion. - # This gets done via helpconfig normally, but its - # pytest_cmdline_main is not called in case of errors. - if getattr(self.option, "version", False) or "--version" in args: - from _pytest.helpconfig import showversion - - showversion(self) - elif ( - getattr(self.option, "help", False) or "--help" in args or "-h" in args - ): - self._parser._getparser().print_help() - sys.stdout.write( - "\nNOTE: displaying only minimal help due to UsageError.\n\n" - ) - - raise + def get_terminal_writer(self) -> TerminalWriter: + terminalreporter: TerminalReporter = self.pluginmanager.get_plugin( + "terminalreporter" + ) + return terminalreporter._tw + + def pytest_cmdline_parse( + self, pluginmanager: PytestPluginManager, args: List[str] + ) -> "Config": + try: + self.parse(args) + except UsageError: + + # Handle --version and --help here in a minimal fashion. + # This gets done via helpconfig normally, but its + # pytest_cmdline_main is not called in case of errors. + if getattr(self.option, "version", False) or "--version" in args: + from _pytest.helpconfig import showversion + + showversion(self) + elif ( + getattr(self.option, "help", False) or "--help" in args or "-h" in args + ): + self._parser._getparser().print_help() + sys.stdout.write( + "\nNOTE: displaying only minimal help due to UsageError.\n\n" + ) + + raise return self - def notify_exception( - self, - excinfo: ExceptionInfo[BaseException], - option: Optional[argparse.Namespace] = None, - ) -> None: - if option and getattr(option, "fulltrace", False): - style: _TracebackStyle = "long" + def notify_exception( + self, + excinfo: ExceptionInfo[BaseException], + option: Optional[argparse.Namespace] = None, + ) -> None: + if option and getattr(option, "fulltrace", False): + style: _TracebackStyle = "long" else: style = "native" excrepr = excinfo.getrepr( @@ -1040,61 +1040,61 @@ class Config: sys.stderr.write("INTERNALERROR> %s\n" % line) sys.stderr.flush() - def cwd_relative_nodeid(self, nodeid: str) -> str: - # nodeid's are relative to the rootpath, compute relative to cwd. - if self.invocation_params.dir != self.rootpath: - fullpath = self.rootpath / nodeid - nodeid = bestrelpath(self.invocation_params.dir, fullpath) + def cwd_relative_nodeid(self, nodeid: str) -> str: + # nodeid's are relative to the rootpath, compute relative to cwd. + if self.invocation_params.dir != self.rootpath: + fullpath = self.rootpath / nodeid + nodeid = bestrelpath(self.invocation_params.dir, fullpath) return nodeid @classmethod - def fromdictargs(cls, option_dict, args) -> "Config": - """Constructor usable for subprocesses.""" - config = get_config(args) + def fromdictargs(cls, option_dict, args) -> "Config": + """Constructor usable for subprocesses.""" + config = get_config(args) config.option.__dict__.update(option_dict) config.parse(args, addopts=False) for x in config.option.plugins: config.pluginmanager.consider_pluginarg(x) return config - def _processopt(self, opt: "Argument") -> None: + def _processopt(self, opt: "Argument") -> None: for name in opt._short_opts + opt._long_opts: self._opt2dest[name] = opt.dest - if hasattr(opt, "default"): + if hasattr(opt, "default"): if not hasattr(self.option, opt.dest): setattr(self.option, opt.dest, opt.default) @hookimpl(trylast=True) - def pytest_load_initial_conftests(self, early_config: "Config") -> None: + def pytest_load_initial_conftests(self, early_config: "Config") -> None: self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) - def _initini(self, args: Sequence[str]) -> None: + def _initini(self, args: Sequence[str]) -> None: ns, unknown_args = self._parser.parse_known_and_unknown_args( args, namespace=copy.copy(self.option) ) - rootpath, inipath, inicfg = determine_setup( + rootpath, inipath, inicfg = determine_setup( ns.inifilename, ns.file_or_dir + unknown_args, rootdir_cmd_arg=ns.rootdir or None, config=self, ) - self._rootpath = rootpath - self._inipath = inipath - self.inicfg = inicfg - self._parser.extra_info["rootdir"] = str(self.rootpath) - self._parser.extra_info["inifile"] = str(self.inipath) + self._rootpath = rootpath + self._inipath = inipath + self.inicfg = inicfg + self._parser.extra_info["rootdir"] = str(self.rootpath) + self._parser.extra_info["inifile"] = str(self.inipath) self._parser.addini("addopts", "extra command line options", "args") self._parser.addini("minversion", "minimally required pytest version") - self._parser.addini( - "required_plugins", - "plugins that must be present for pytest to run", - type="args", - default=[], - ) + self._parser.addini( + "required_plugins", + "plugins that must be present for pytest to run", + type="args", + default=[], + ) self._override_ini = ns.override_ini or () - def _consider_importhook(self, args: Sequence[str]) -> None: + def _consider_importhook(self, args: Sequence[str]) -> None: """Install the PEP 302 import hook if using assertion rewriting. Needs to parse the --assert=<mode> option from the commandline @@ -1102,22 +1102,22 @@ class Config: by the importhook. """ ns, unknown_args = self._parser.parse_known_and_unknown_args(args) - mode = getattr(ns, "assertmode", "plain") + mode = getattr(ns, "assertmode", "plain") if mode == "rewrite": - import _pytest.assertion - + import _pytest.assertion + try: hook = _pytest.assertion.install_importhook(self) except SystemError: mode = "plain" else: self._mark_plugins_for_rewrite(hook) - self._warn_about_missing_assertion(mode) + self._warn_about_missing_assertion(mode) - def _mark_plugins_for_rewrite(self, hook) -> None: - """Given an importhook, mark for rewrite any top-level + def _mark_plugins_for_rewrite(self, hook) -> None: + """Given an importhook, mark for rewrite any top-level modules or packages in the distribution package for - all pytest plugins.""" + all pytest plugins.""" self.pluginmanager.rewrite_hook = hook if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): @@ -1125,155 +1125,155 @@ class Config: return package_files = ( - str(file) - for dist in importlib_metadata.distributions() - if any(ep.group == "pytest11" for ep in dist.entry_points) - for file in dist.files or [] + str(file) + for dist in importlib_metadata.distributions() + if any(ep.group == "pytest11" for ep in dist.entry_points) + for file in dist.files or [] ) for name in _iter_rewritable_modules(package_files): hook.mark_rewrite(name) - def _validate_args(self, args: List[str], via: str) -> List[str]: - """Validate known args.""" - self._parser._config_source_hint = via # type: ignore - try: - self._parser.parse_known_and_unknown_args( - args, namespace=copy.copy(self.option) - ) - finally: - del self._parser._config_source_hint # type: ignore - - return args - - def _preparse(self, args: List[str], addopts: bool = True) -> None: + def _validate_args(self, args: List[str], via: str) -> List[str]: + """Validate known args.""" + self._parser._config_source_hint = via # type: ignore + try: + self._parser.parse_known_and_unknown_args( + args, namespace=copy.copy(self.option) + ) + finally: + del self._parser._config_source_hint # type: ignore + + return args + + def _preparse(self, args: List[str], addopts: bool = True) -> None: if addopts: - env_addopts = os.environ.get("PYTEST_ADDOPTS", "") - if len(env_addopts): - args[:] = ( - self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS") - + args - ) + env_addopts = os.environ.get("PYTEST_ADDOPTS", "") + if len(env_addopts): + args[:] = ( + self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS") + + args + ) self._initini(args) if addopts: - args[:] = ( - self._validate_args(self.getini("addopts"), "via addopts config") + args - ) - - self.known_args_namespace = self._parser.parse_known_args( - args, namespace=copy.copy(self.option) - ) + args[:] = ( + self._validate_args(self.getini("addopts"), "via addopts config") + args + ) + + self.known_args_namespace = self._parser.parse_known_args( + args, namespace=copy.copy(self.option) + ) self._checkversion() self._consider_importhook(args) - self.pluginmanager.consider_preparse(args, exclude_only=False) + self.pluginmanager.consider_preparse(args, exclude_only=False) if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): # Don't autoload from setuptools entry point. Only explicitly specified # plugins are going to be loaded. self.pluginmanager.load_setuptools_entrypoints("pytest11") self.pluginmanager.consider_env() - - self.known_args_namespace = self._parser.parse_known_args( - args, namespace=copy.copy(self.known_args_namespace) + + self.known_args_namespace = self._parser.parse_known_args( + args, namespace=copy.copy(self.known_args_namespace) ) - - self._validate_plugins() - self._warn_about_skipped_plugins() - - if self.known_args_namespace.strict: - self.issue_config_time_warning( - _pytest.deprecated.STRICT_OPTION, stacklevel=2 - ) - - if self.known_args_namespace.confcutdir is None and self.inipath is not None: - confcutdir = str(self.inipath.parent) + + self._validate_plugins() + self._warn_about_skipped_plugins() + + if self.known_args_namespace.strict: + self.issue_config_time_warning( + _pytest.deprecated.STRICT_OPTION, stacklevel=2 + ) + + if self.known_args_namespace.confcutdir is None and self.inipath is not None: + confcutdir = str(self.inipath.parent) self.known_args_namespace.confcutdir = confcutdir try: self.hook.pytest_load_initial_conftests( early_config=self, args=args, parser=self._parser ) - except ConftestImportFailure as e: - if self.known_args_namespace.help or self.known_args_namespace.version: + except ConftestImportFailure as e: + if self.known_args_namespace.help or self.known_args_namespace.version: # we don't want to prevent --help/--version to work # so just let is pass and print a warning at the end - self.issue_config_time_warning( - PytestConfigWarning(f"could not load initial conftests: {e.path}"), - stacklevel=2, - ) + self.issue_config_time_warning( + PytestConfigWarning(f"could not load initial conftests: {e.path}"), + stacklevel=2, + ) else: raise - @hookimpl(hookwrapper=True) - def pytest_collection(self) -> Generator[None, None, None]: - """Validate invalid ini keys after collection is done so we take in account - options added by late-loading conftest files.""" - yield - self._validate_config_options() - - def _checkversion(self) -> None: + @hookimpl(hookwrapper=True) + def pytest_collection(self) -> Generator[None, None, None]: + """Validate invalid ini keys after collection is done so we take in account + options added by late-loading conftest files.""" + yield + self._validate_config_options() + + def _checkversion(self) -> None: import pytest minver = self.inicfg.get("minversion", None) if minver: - # Imported lazily to improve start-up time. - from packaging.version import Version - - if not isinstance(minver, str): - raise pytest.UsageError( - "%s: 'minversion' must be a single value" % self.inipath - ) - - if Version(minver) > Version(pytest.__version__): + # Imported lazily to improve start-up time. + from packaging.version import Version + + if not isinstance(minver, str): + raise pytest.UsageError( + "%s: 'minversion' must be a single value" % self.inipath + ) + + if Version(minver) > Version(pytest.__version__): raise pytest.UsageError( - "%s: 'minversion' requires pytest-%s, actual pytest-%s'" - % (self.inipath, minver, pytest.__version__,) + "%s: 'minversion' requires pytest-%s, actual pytest-%s'" + % (self.inipath, minver, pytest.__version__,) ) - def _validate_config_options(self) -> None: - for key in sorted(self._get_unknown_ini_keys()): - self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") - - def _validate_plugins(self) -> None: - required_plugins = sorted(self.getini("required_plugins")) - if not required_plugins: - return - - # Imported lazily to improve start-up time. - from packaging.version import Version - from packaging.requirements import InvalidRequirement, Requirement - - plugin_info = self.pluginmanager.list_plugin_distinfo() - plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info} - - missing_plugins = [] - for required_plugin in required_plugins: - try: - spec = Requirement(required_plugin) - except InvalidRequirement: - missing_plugins.append(required_plugin) - continue - - if spec.name not in plugin_dist_info: - missing_plugins.append(required_plugin) - elif Version(plugin_dist_info[spec.name]) not in spec.specifier: - missing_plugins.append(required_plugin) - - if missing_plugins: - raise UsageError( - "Missing required plugins: {}".format(", ".join(missing_plugins)), - ) - - def _warn_or_fail_if_strict(self, message: str) -> None: - if self.known_args_namespace.strict_config: - raise UsageError(message) - - self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3) - - def _get_unknown_ini_keys(self) -> List[str]: - parser_inicfg = self._parser._inidict - return [name for name in self.inicfg if name not in parser_inicfg] - - def parse(self, args: List[str], addopts: bool = True) -> None: - # Parse given cmdline arguments into this config object. + def _validate_config_options(self) -> None: + for key in sorted(self._get_unknown_ini_keys()): + self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") + + def _validate_plugins(self) -> None: + required_plugins = sorted(self.getini("required_plugins")) + if not required_plugins: + return + + # Imported lazily to improve start-up time. + from packaging.version import Version + from packaging.requirements import InvalidRequirement, Requirement + + plugin_info = self.pluginmanager.list_plugin_distinfo() + plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info} + + missing_plugins = [] + for required_plugin in required_plugins: + try: + spec = Requirement(required_plugin) + except InvalidRequirement: + missing_plugins.append(required_plugin) + continue + + if spec.name not in plugin_dist_info: + missing_plugins.append(required_plugin) + elif Version(plugin_dist_info[spec.name]) not in spec.specifier: + missing_plugins.append(required_plugin) + + if missing_plugins: + raise UsageError( + "Missing required plugins: {}".format(", ".join(missing_plugins)), + ) + + def _warn_or_fail_if_strict(self, message: str) -> None: + if self.known_args_namespace.strict_config: + raise UsageError(message) + + self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3) + + def _get_unknown_ini_keys(self) -> List[str]: + parser_inicfg = self._parser._inidict + return [name for name in self.inicfg if name not in parser_inicfg] + + def parse(self, args: List[str], addopts: bool = True) -> None: + # Parse given cmdline arguments into this config object. assert not hasattr( self, "args" ), "can only parse cmdline args at most once per Config object" @@ -1283,91 +1283,91 @@ class Config: self._preparse(args, addopts=addopts) # XXX deprecated hook: self.hook.pytest_cmdline_preparse(config=self, args=args) - self._parser.after_preparse = True # type: ignore + self._parser.after_preparse = True # type: ignore try: args = self._parser.parse_setoption( args, self.option, namespace=self.option ) if not args: - if self.invocation_params.dir == self.rootpath: + if self.invocation_params.dir == self.rootpath: args = self.getini("testpaths") if not args: - args = [str(self.invocation_params.dir)] + args = [str(self.invocation_params.dir)] self.args = args except PrintHelp: pass - def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None: - """Issue and handle a warning during the "configure" stage. - - During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item`` - function because it is not possible to have hookwrappers around ``pytest_configure``. - - This function is mainly intended for plugins that need to issue warnings during - ``pytest_configure`` (or similar stages). - - :param warning: The warning instance. - :param stacklevel: stacklevel forwarded to warnings.warn. - """ - if self.pluginmanager.is_blocked("warnings"): - return - - cmdline_filters = self.known_args_namespace.pythonwarnings or [] - config_filters = self.getini("filterwarnings") - - with warnings.catch_warnings(record=True) as records: - warnings.simplefilter("always", type(warning)) - apply_warning_filters(config_filters, cmdline_filters) - warnings.warn(warning, stacklevel=stacklevel) - - if records: - frame = sys._getframe(stacklevel - 1) - location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name - self.hook.pytest_warning_captured.call_historic( - kwargs=dict( - warning_message=records[0], - when="config", - item=None, - location=location, - ) - ) - self.hook.pytest_warning_recorded.call_historic( - kwargs=dict( - warning_message=records[0], - when="config", - nodeid="", - location=location, - ) - ) - - def addinivalue_line(self, name: str, line: str) -> None: - """Add a line to an ini-file option. The option must have been - declared but might not yet be set in which case the line becomes - the first line in its value.""" + def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None: + """Issue and handle a warning during the "configure" stage. + + During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item`` + function because it is not possible to have hookwrappers around ``pytest_configure``. + + This function is mainly intended for plugins that need to issue warnings during + ``pytest_configure`` (or similar stages). + + :param warning: The warning instance. + :param stacklevel: stacklevel forwarded to warnings.warn. + """ + if self.pluginmanager.is_blocked("warnings"): + return + + cmdline_filters = self.known_args_namespace.pythonwarnings or [] + config_filters = self.getini("filterwarnings") + + with warnings.catch_warnings(record=True) as records: + warnings.simplefilter("always", type(warning)) + apply_warning_filters(config_filters, cmdline_filters) + warnings.warn(warning, stacklevel=stacklevel) + + if records: + frame = sys._getframe(stacklevel - 1) + location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name + self.hook.pytest_warning_captured.call_historic( + kwargs=dict( + warning_message=records[0], + when="config", + item=None, + location=location, + ) + ) + self.hook.pytest_warning_recorded.call_historic( + kwargs=dict( + warning_message=records[0], + when="config", + nodeid="", + location=location, + ) + ) + + def addinivalue_line(self, name: str, line: str) -> None: + """Add a line to an ini-file option. The option must have been + declared but might not yet be set in which case the line becomes + the first line in its value.""" x = self.getini(name) assert isinstance(x, list) x.append(line) # modifies the cached list inline - def getini(self, name: str): - """Return configuration value from an :ref:`ini file <configfiles>`. - - If the specified name hasn't been registered through a prior - :py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>` - call (usually from a plugin), a ValueError is raised. - """ + def getini(self, name: str): + """Return configuration value from an :ref:`ini file <configfiles>`. + + If the specified name hasn't been registered through a prior + :py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>` + call (usually from a plugin), a ValueError is raised. + """ try: return self._inicache[name] except KeyError: self._inicache[name] = val = self._getini(name) return val - def _getini(self, name: str): + def _getini(self, name: str): try: description, type, default = self._parser._inidict[name] - except KeyError as e: - raise ValueError(f"unknown configuration value: {name!r}") from e - override_value = self._get_override_ini_value(name) - if override_value is None: + except KeyError as e: + raise ValueError(f"unknown configuration value: {name!r}") from e + override_value = self._get_override_ini_value(name) + if override_value is None: try: value = self.inicfg[name] except KeyError: @@ -1376,86 +1376,86 @@ class Config: if type is None: return "" return [] - else: - value = override_value - # Coerce the values based on types. - # - # Note: some coercions are only required if we are reading from .ini files, because - # the file format doesn't contain type information, but when reading from toml we will - # get either str or list of str values (see _parse_ini_config_from_pyproject_toml). - # For example: - # - # ini: - # a_line_list = "tests acceptance" - # in this case, we need to split the string to obtain a list of strings. - # - # toml: - # a_line_list = ["tests", "acceptance"] - # in this case, we already have a list ready to use. - # + else: + value = override_value + # Coerce the values based on types. + # + # Note: some coercions are only required if we are reading from .ini files, because + # the file format doesn't contain type information, but when reading from toml we will + # get either str or list of str values (see _parse_ini_config_from_pyproject_toml). + # For example: + # + # ini: + # a_line_list = "tests acceptance" + # in this case, we need to split the string to obtain a list of strings. + # + # toml: + # a_line_list = ["tests", "acceptance"] + # in this case, we already have a list ready to use. + # if type == "pathlist": - # TODO: This assert is probably not valid in all cases. - assert self.inipath is not None - dp = self.inipath.parent - input_values = shlex.split(value) if isinstance(value, str) else value - return [py.path.local(str(dp / x)) for x in input_values] + # TODO: This assert is probably not valid in all cases. + assert self.inipath is not None + dp = self.inipath.parent + input_values = shlex.split(value) if isinstance(value, str) else value + return [py.path.local(str(dp / x)) for x in input_values] elif type == "args": - return shlex.split(value) if isinstance(value, str) else value + return shlex.split(value) if isinstance(value, str) else value elif type == "linelist": - if isinstance(value, str): - return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] - else: - return value + if isinstance(value, str): + return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] + else: + return value elif type == "bool": - return _strtobool(str(value).strip()) + return _strtobool(str(value).strip()) else: - assert type in [None, "string"] + assert type in [None, "string"] return value - def _getconftest_pathlist( - self, name: str, path: py.path.local - ) -> Optional[List[py.path.local]]: + def _getconftest_pathlist( + self, name: str, path: py.path.local + ) -> Optional[List[py.path.local]]: try: - mod, relroots = self.pluginmanager._rget_with_confmod( - name, path, self.getoption("importmode") - ) + mod, relroots = self.pluginmanager._rget_with_confmod( + name, path, self.getoption("importmode") + ) except KeyError: return None modpath = py.path.local(mod.__file__).dirpath() - values: List[py.path.local] = [] + values: List[py.path.local] = [] for relroot in relroots: if not isinstance(relroot, py.path.local): - relroot = relroot.replace("/", os.sep) + relroot = relroot.replace("/", os.sep) relroot = modpath.join(relroot, abs=True) values.append(relroot) return values - def _get_override_ini_value(self, name: str) -> Optional[str]: + def _get_override_ini_value(self, name: str) -> Optional[str]: value = None - # override_ini is a list of "ini=value" options. - # Always use the last item if multiple values are set for same ini-name, - # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2. + # override_ini is a list of "ini=value" options. + # Always use the last item if multiple values are set for same ini-name, + # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2. for ini_config in self._override_ini: try: key, user_ini_value = ini_config.split("=", 1) - except ValueError as e: - raise UsageError( - "-o/--override-ini expects option=value style (got: {!r}).".format( - ini_config - ) - ) from e + except ValueError as e: + raise UsageError( + "-o/--override-ini expects option=value style (got: {!r}).".format( + ini_config + ) + ) from e else: if key == name: value = user_ini_value return value - def getoption(self, name: str, default=notset, skip: bool = False): - """Return command line option value. + def getoption(self, name: str, default=notset, skip: bool = False): + """Return command line option value. - :param name: Name of the option. You may also specify + :param name: Name of the option. You may also specify the literal ``--OPT`` option instead of the "dest" option name. - :param default: Default value if no option of that name exists. - :param skip: If True, raise pytest.skip if option does not exists + :param default: Default value if no option of that name exists. + :param skip: If True, raise pytest.skip if option does not exists or has a None value. """ name = self._opt2dest.get(name, name) @@ -1464,143 +1464,143 @@ class Config: if val is None and skip: raise AttributeError(name) return val - except AttributeError as e: + except AttributeError as e: if default is not notset: return default if skip: import pytest - pytest.skip(f"no {name!r} option found") - raise ValueError(f"no option named {name!r}") from e + pytest.skip(f"no {name!r} option found") + raise ValueError(f"no option named {name!r}") from e - def getvalue(self, name: str, path=None): - """Deprecated, use getoption() instead.""" + def getvalue(self, name: str, path=None): + """Deprecated, use getoption() instead.""" return self.getoption(name) - def getvalueorskip(self, name: str, path=None): - """Deprecated, use getoption(skip=True) instead.""" + def getvalueorskip(self, name: str, path=None): + """Deprecated, use getoption(skip=True) instead.""" return self.getoption(name, skip=True) - def _warn_about_missing_assertion(self, mode: str) -> None: - if not _assertion_supported(): - if mode == "plain": - warning_text = ( - "ASSERTIONS ARE NOT EXECUTED" - " and FAILING TESTS WILL PASS. Are you" - " using python -O?" - ) - else: - warning_text = ( - "assertions not in test modules or" - " plugins will be ignored" - " because assert statements are not executed " - "by the underlying Python interpreter " - "(are you using python -O?)\n" - ) - self.issue_config_time_warning( - PytestConfigWarning(warning_text), stacklevel=3, - ) - - def _warn_about_skipped_plugins(self) -> None: - for module_name, msg in self.pluginmanager.skipped_plugins: - self.issue_config_time_warning( - PytestConfigWarning(f"skipped plugin {module_name!r}: {msg}"), - stacklevel=2, - ) - - -def _assertion_supported() -> bool: + def _warn_about_missing_assertion(self, mode: str) -> None: + if not _assertion_supported(): + if mode == "plain": + warning_text = ( + "ASSERTIONS ARE NOT EXECUTED" + " and FAILING TESTS WILL PASS. Are you" + " using python -O?" + ) + else: + warning_text = ( + "assertions not in test modules or" + " plugins will be ignored" + " because assert statements are not executed " + "by the underlying Python interpreter " + "(are you using python -O?)\n" + ) + self.issue_config_time_warning( + PytestConfigWarning(warning_text), stacklevel=3, + ) + + def _warn_about_skipped_plugins(self) -> None: + for module_name, msg in self.pluginmanager.skipped_plugins: + self.issue_config_time_warning( + PytestConfigWarning(f"skipped plugin {module_name!r}: {msg}"), + stacklevel=2, + ) + + +def _assertion_supported() -> bool: try: assert False except AssertionError: return True else: - return False # type: ignore[unreachable] + return False # type: ignore[unreachable] -def create_terminal_writer( - config: Config, file: Optional[TextIO] = None -) -> TerminalWriter: - """Create a TerminalWriter instance configured according to the options - in the config object. +def create_terminal_writer( + config: Config, file: Optional[TextIO] = None +) -> TerminalWriter: + """Create a TerminalWriter instance configured according to the options + in the config object. - Every code which requires a TerminalWriter object and has access to a - config object should use this function. - """ - tw = TerminalWriter(file=file) + Every code which requires a TerminalWriter object and has access to a + config object should use this function. + """ + tw = TerminalWriter(file=file) if config.option.color == "yes": tw.hasmarkup = True - elif config.option.color == "no": + elif config.option.color == "no": tw.hasmarkup = False - - if config.option.code_highlight == "yes": - tw.code_highlight = True - elif config.option.code_highlight == "no": - tw.code_highlight = False - + + if config.option.code_highlight == "yes": + tw.code_highlight = True + elif config.option.code_highlight == "no": + tw.code_highlight = False + return tw -def _strtobool(val: str) -> bool: - """Convert a string representation of truth to True or False. +def _strtobool(val: str) -> bool: + """Convert a string representation of truth to True or False. True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if 'val' is anything else. - .. note:: Copied from distutils.util. + .. note:: Copied from distutils.util. """ val = val.lower() if val in ("y", "yes", "t", "true", "on", "1"): - return True + return True elif val in ("n", "no", "f", "false", "off", "0"): - return False - else: - raise ValueError(f"invalid truth value {val!r}") - - -@lru_cache(maxsize=50) -def parse_warning_filter( - arg: str, *, escape: bool -) -> Tuple[str, str, Type[Warning], str, int]: - """Parse a warnings filter string. - - This is copied from warnings._setoption, but does not apply the filter, - only parses it, and makes the escaping optional. - """ - parts = arg.split(":") - if len(parts) > 5: - raise warnings._OptionError(f"too many fields (max 5): {arg!r}") - while len(parts) < 5: - parts.append("") - action_, message, category_, module, lineno_ = [s.strip() for s in parts] - action: str = warnings._getaction(action_) # type: ignore[attr-defined] - category: Type[Warning] = warnings._getcategory(category_) # type: ignore[attr-defined] - if message and escape: - message = re.escape(message) - if module and escape: - module = re.escape(module) + r"\Z" - if lineno_: - try: - lineno = int(lineno_) - if lineno < 0: - raise ValueError - except (ValueError, OverflowError) as e: - raise warnings._OptionError(f"invalid lineno {lineno_!r}") from e + return False else: - lineno = 0 - return action, message, category, module, lineno - - -def apply_warning_filters( - config_filters: Iterable[str], cmdline_filters: Iterable[str] -) -> None: - """Applies pytest-configured filters to the warnings module""" - # Filters should have this precedence: cmdline options, config. - # Filters should be applied in the inverse order of precedence. - for arg in config_filters: - warnings.filterwarnings(*parse_warning_filter(arg, escape=False)) - - for arg in cmdline_filters: - warnings.filterwarnings(*parse_warning_filter(arg, escape=True)) + raise ValueError(f"invalid truth value {val!r}") + + +@lru_cache(maxsize=50) +def parse_warning_filter( + arg: str, *, escape: bool +) -> Tuple[str, str, Type[Warning], str, int]: + """Parse a warnings filter string. + + This is copied from warnings._setoption, but does not apply the filter, + only parses it, and makes the escaping optional. + """ + parts = arg.split(":") + if len(parts) > 5: + raise warnings._OptionError(f"too many fields (max 5): {arg!r}") + while len(parts) < 5: + parts.append("") + action_, message, category_, module, lineno_ = [s.strip() for s in parts] + action: str = warnings._getaction(action_) # type: ignore[attr-defined] + category: Type[Warning] = warnings._getcategory(category_) # type: ignore[attr-defined] + if message and escape: + message = re.escape(message) + if module and escape: + module = re.escape(module) + r"\Z" + if lineno_: + try: + lineno = int(lineno_) + if lineno < 0: + raise ValueError + except (ValueError, OverflowError) as e: + raise warnings._OptionError(f"invalid lineno {lineno_!r}") from e + else: + lineno = 0 + return action, message, category, module, lineno + + +def apply_warning_filters( + config_filters: Iterable[str], cmdline_filters: Iterable[str] +) -> None: + """Applies pytest-configured filters to the warnings module""" + # Filters should have this precedence: cmdline options, config. + # Filters should be applied in the inverse order of precedence. + for arg in config_filters: + warnings.filterwarnings(*parse_warning_filter(arg, escape=False)) + + for arg in cmdline_filters: + warnings.filterwarnings(*parse_warning_filter(arg, escape=True)) diff --git a/contrib/python/pytest/py3/_pytest/config/argparsing.py b/contrib/python/pytest/py3/_pytest/config/argparsing.py index 9a48196552..2418831249 100644 --- a/contrib/python/pytest/py3/_pytest/config/argparsing.py +++ b/contrib/python/pytest/py3/_pytest/config/argparsing.py @@ -1,72 +1,72 @@ import argparse -import sys +import sys import warnings -from gettext import gettext -from typing import Any -from typing import Callable -from typing import cast -from typing import Dict -from typing import List -from typing import Mapping -from typing import Optional -from typing import Sequence -from typing import Tuple -from typing import TYPE_CHECKING -from typing import Union +from gettext import gettext +from typing import Any +from typing import Callable +from typing import cast +from typing import Dict +from typing import List +from typing import Mapping +from typing import Optional +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING +from typing import Union import py -import _pytest._io -from _pytest.compat import final -from _pytest.config.exceptions import UsageError - -if TYPE_CHECKING: - from typing import NoReturn - from typing_extensions import Literal +import _pytest._io +from _pytest.compat import final +from _pytest.config.exceptions import UsageError +if TYPE_CHECKING: + from typing import NoReturn + from typing_extensions import Literal + FILE_OR_DIR = "file_or_dir" -@final -class Parser: - """Parser for command line arguments and ini-file values. +@final +class Parser: + """Parser for command line arguments and ini-file values. - :ivar extra_info: Dict of generic param -> value to display in case + :ivar extra_info: Dict of generic param -> value to display in case there's an error processing the command line arguments. """ - prog: Optional[str] = None - - def __init__( - self, - usage: Optional[str] = None, - processopt: Optional[Callable[["Argument"], None]] = None, - ) -> None: + prog: Optional[str] = None + + def __init__( + self, + usage: Optional[str] = None, + processopt: Optional[Callable[["Argument"], None]] = None, + ) -> None: self._anonymous = OptionGroup("custom options", parser=self) - self._groups: List[OptionGroup] = [] + self._groups: List[OptionGroup] = [] self._processopt = processopt self._usage = usage - self._inidict: Dict[str, Tuple[str, Optional[str], Any]] = {} - self._ininames: List[str] = [] - self.extra_info: Dict[str, Any] = {} + self._inidict: Dict[str, Tuple[str, Optional[str], Any]] = {} + self._ininames: List[str] = [] + self.extra_info: Dict[str, Any] = {} - def processoption(self, option: "Argument") -> None: + def processoption(self, option: "Argument") -> None: if self._processopt: if option.dest: self._processopt(option) - def getgroup( - self, name: str, description: str = "", after: Optional[str] = None - ) -> "OptionGroup": - """Get (or create) a named option Group. + def getgroup( + self, name: str, description: str = "", after: Optional[str] = None + ) -> "OptionGroup": + """Get (or create) a named option Group. - :name: Name of the option group. - :description: Long description for --help output. - :after: Name of another group, used for ordering --help output. + :name: Name of the option group. + :description: Long description for --help output. + :after: Name of another group, used for ordering --help output. The returned group object has an ``addoption`` method with the same signature as :py:func:`parser.addoption - <_pytest.config.argparsing.Parser.addoption>` but will be shown in the + <_pytest.config.argparsing.Parser.addoption>` but will be shown in the respective group in the output of ``pytest. --help``. """ for group in self._groups: @@ -80,37 +80,37 @@ class Parser: self._groups.insert(i + 1, group) return group - def addoption(self, *opts: str, **attrs: Any) -> None: - """Register a command line option. + def addoption(self, *opts: str, **attrs: Any) -> None: + """Register a command line option. - :opts: Option names, can be short or long options. - :attrs: Same attributes which the ``add_argument()`` function of the - `argparse library <https://docs.python.org/library/argparse.html>`_ + :opts: Option names, can be short or long options. + :attrs: Same attributes which the ``add_argument()`` function of the + `argparse library <https://docs.python.org/library/argparse.html>`_ accepts. - After command line parsing, options are available on the pytest config + After command line parsing, options are available on the pytest config object via ``config.option.NAME`` where ``NAME`` is usually set by passing a ``dest`` attribute, for example ``addoption("--long", dest="NAME", ...)``. """ self._anonymous.addoption(*opts, **attrs) - def parse( - self, - args: Sequence[Union[str, py.path.local]], - namespace: Optional[argparse.Namespace] = None, - ) -> argparse.Namespace: + def parse( + self, + args: Sequence[Union[str, py.path.local]], + namespace: Optional[argparse.Namespace] = None, + ) -> argparse.Namespace: from _pytest._argcomplete import try_argcomplete self.optparser = self._getparser() try_argcomplete(self.optparser) - strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] - return self.optparser.parse_args(strargs, namespace=namespace) + strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] + return self.optparser.parse_args(strargs, namespace=namespace) - def _getparser(self) -> "MyOptionParser": + def _getparser(self) -> "MyOptionParser": from _pytest._argcomplete import filescompleter - optparser = MyOptionParser(self, self.extra_info, prog=self.prog) + optparser = MyOptionParser(self, self.extra_info, prog=self.prog) groups = self._groups + [self._anonymous] for group in groups: if group.options: @@ -120,98 +120,98 @@ class Parser: n = option.names() a = option.attrs() arggroup.add_argument(*n, **a) - file_or_dir_arg = optparser.add_argument(FILE_OR_DIR, nargs="*") + file_or_dir_arg = optparser.add_argument(FILE_OR_DIR, nargs="*") # bash like autocompletion for dirs (appending '/') - # Type ignored because typeshed doesn't know about argcomplete. - file_or_dir_arg.completer = filescompleter # type: ignore + # Type ignored because typeshed doesn't know about argcomplete. + file_or_dir_arg.completer = filescompleter # type: ignore return optparser - def parse_setoption( - self, - args: Sequence[Union[str, py.path.local]], - option: argparse.Namespace, - namespace: Optional[argparse.Namespace] = None, - ) -> List[str]: + def parse_setoption( + self, + args: Sequence[Union[str, py.path.local]], + option: argparse.Namespace, + namespace: Optional[argparse.Namespace] = None, + ) -> List[str]: parsedoption = self.parse(args, namespace=namespace) for name, value in parsedoption.__dict__.items(): setattr(option, name, value) - return cast(List[str], getattr(parsedoption, FILE_OR_DIR)) - - def parse_known_args( - self, - args: Sequence[Union[str, py.path.local]], - namespace: Optional[argparse.Namespace] = None, - ) -> argparse.Namespace: - """Parse and return a namespace object with known arguments at this point.""" + return cast(List[str], getattr(parsedoption, FILE_OR_DIR)) + + def parse_known_args( + self, + args: Sequence[Union[str, py.path.local]], + namespace: Optional[argparse.Namespace] = None, + ) -> argparse.Namespace: + """Parse and return a namespace object with known arguments at this point.""" return self.parse_known_and_unknown_args(args, namespace=namespace)[0] - def parse_known_and_unknown_args( - self, - args: Sequence[Union[str, py.path.local]], - namespace: Optional[argparse.Namespace] = None, - ) -> Tuple[argparse.Namespace, List[str]]: - """Parse and return a namespace object with known arguments, and - the remaining arguments unknown at this point.""" + def parse_known_and_unknown_args( + self, + args: Sequence[Union[str, py.path.local]], + namespace: Optional[argparse.Namespace] = None, + ) -> Tuple[argparse.Namespace, List[str]]: + """Parse and return a namespace object with known arguments, and + the remaining arguments unknown at this point.""" optparser = self._getparser() - strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] - return optparser.parse_known_args(strargs, namespace=namespace) - - def addini( - self, - name: str, - help: str, - type: Optional[ - "Literal['string', 'pathlist', 'args', 'linelist', 'bool']" - ] = None, - default=None, - ) -> None: - """Register an ini-file option. - - :name: Name of the ini-variable. - :type: Type of the variable, can be ``string``, ``pathlist``, ``args``, - ``linelist`` or ``bool``. Defaults to ``string`` if ``None`` or - not passed. - :default: Default value if no ini-file option exists but is queried. + strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] + return optparser.parse_known_args(strargs, namespace=namespace) + + def addini( + self, + name: str, + help: str, + type: Optional[ + "Literal['string', 'pathlist', 'args', 'linelist', 'bool']" + ] = None, + default=None, + ) -> None: + """Register an ini-file option. + + :name: Name of the ini-variable. + :type: Type of the variable, can be ``string``, ``pathlist``, ``args``, + ``linelist`` or ``bool``. Defaults to ``string`` if ``None`` or + not passed. + :default: Default value if no ini-file option exists but is queried. The value of ini-variables can be retrieved via a call to :py:func:`config.getini(name) <_pytest.config.Config.getini>`. """ - assert type in (None, "string", "pathlist", "args", "linelist", "bool") + assert type in (None, "string", "pathlist", "args", "linelist", "bool") self._inidict[name] = (help, type, default) self._ininames.append(name) class ArgumentError(Exception): - """Raised if an Argument instance is created with invalid or - inconsistent arguments.""" + """Raised if an Argument instance is created with invalid or + inconsistent arguments.""" - def __init__(self, msg: str, option: Union["Argument", str]) -> None: + def __init__(self, msg: str, option: Union["Argument", str]) -> None: self.msg = msg self.option_id = str(option) - def __str__(self) -> str: + def __str__(self) -> str: if self.option_id: - return f"option {self.option_id}: {self.msg}" + return f"option {self.option_id}: {self.msg}" else: return self.msg -class Argument: - """Class that mimics the necessary behaviour of optparse.Option. - - It's currently a least effort implementation and ignoring choices - and integer prefixes. +class Argument: + """Class that mimics the necessary behaviour of optparse.Option. + It's currently a least effort implementation and ignoring choices + and integer prefixes. + https://docs.python.org/3/library/optparse.html#optparse-standard-option-types """ _typ_map = {"int": int, "string": str, "float": float, "complex": complex} - def __init__(self, *names: str, **attrs: Any) -> None: - """Store parms in private vars for use in add_argument.""" + def __init__(self, *names: str, **attrs: Any) -> None: + """Store parms in private vars for use in add_argument.""" self._attrs = attrs - self._short_opts: List[str] = [] - self._long_opts: List[str] = [] + self._short_opts: List[str] = [] + self._long_opts: List[str] = [] if "%default" in (attrs.get("help") or ""): warnings.warn( 'pytest now uses argparse. "%default" should be' @@ -224,8 +224,8 @@ class Argument: except KeyError: pass else: - # This might raise a keyerror as well, don't want to catch that. - if isinstance(typ, str): + # This might raise a keyerror as well, don't want to catch that. + if isinstance(typ, str): if typ == "choice": warnings.warn( "`type` argument to addoption() is the string %r." @@ -247,35 +247,35 @@ class Argument: stacklevel=4, ) attrs["type"] = Argument._typ_map[typ] - # Used in test_parseopt -> test_parse_defaultgetter. + # Used in test_parseopt -> test_parse_defaultgetter. self.type = attrs["type"] else: self.type = typ try: - # Attribute existence is tested in Config._processopt. + # Attribute existence is tested in Config._processopt. self.default = attrs["default"] except KeyError: pass self._set_opt_strings(names) - dest: Optional[str] = attrs.get("dest") - if dest: - self.dest = dest - elif self._long_opts: - self.dest = self._long_opts[0][2:].replace("-", "_") - else: - try: - self.dest = self._short_opts[0][1:] - except IndexError as e: - self.dest = "???" # Needed for the error repr. - raise ArgumentError("need a long or short option", self) from e - - def names(self) -> List[str]: + dest: Optional[str] = attrs.get("dest") + if dest: + self.dest = dest + elif self._long_opts: + self.dest = self._long_opts[0][2:].replace("-", "_") + else: + try: + self.dest = self._short_opts[0][1:] + except IndexError as e: + self.dest = "???" # Needed for the error repr. + raise ArgumentError("need a long or short option", self) from e + + def names(self) -> List[str]: return self._short_opts + self._long_opts - def attrs(self) -> Mapping[str, Any]: - # Update any attributes set by processopt. + def attrs(self) -> Mapping[str, Any]: + # Update any attributes set by processopt. attrs = "default dest help".split() - attrs.append(self.dest) + attrs.append(self.dest) for attr in attrs: try: self._attrs[attr] = getattr(self, attr) @@ -288,11 +288,11 @@ class Argument: self._attrs["help"] = a return self._attrs - def _set_opt_strings(self, opts: Sequence[str]) -> None: - """Directly from optparse. + def _set_opt_strings(self, opts: Sequence[str]) -> None: + """Directly from optparse. - Might not be necessary as this is passed to argparse later on. - """ + Might not be necessary as this is passed to argparse later on. + """ for opt in opts: if len(opt) < 2: raise ArgumentError( @@ -317,8 +317,8 @@ class Argument: ) self._long_opts.append(opt) - def __repr__(self) -> str: - args: List[str] = [] + def __repr__(self) -> str: + args: List[str] = [] if self._short_opts: args += ["_short_opts: " + repr(self._short_opts)] if self._long_opts: @@ -331,22 +331,22 @@ class Argument: return "Argument({})".format(", ".join(args)) -class OptionGroup: - def __init__( - self, name: str, description: str = "", parser: Optional[Parser] = None - ) -> None: +class OptionGroup: + def __init__( + self, name: str, description: str = "", parser: Optional[Parser] = None + ) -> None: self.name = name self.description = description - self.options: List[Argument] = [] + self.options: List[Argument] = [] self.parser = parser - def addoption(self, *optnames: str, **attrs: Any) -> None: - """Add an option to this group. + def addoption(self, *optnames: str, **attrs: Any) -> None: + """Add an option to this group. - If a shortened version of a long option is specified, it will + If a shortened version of a long option is specified, it will be suppressed in the help. addoption('--twowords', '--two-words') results in help showing '--two-words' only, but --twowords gets - accepted **and** the automatic destination is in args.twowords. + accepted **and** the automatic destination is in args.twowords. """ conflict = set(optnames).intersection( name for opt in self.options for name in opt.names() @@ -356,11 +356,11 @@ class OptionGroup: option = Argument(*optnames, **attrs) self._addoption_instance(option, shortupper=False) - def _addoption(self, *optnames: str, **attrs: Any) -> None: + def _addoption(self, *optnames: str, **attrs: Any) -> None: option = Argument(*optnames, **attrs) self._addoption_instance(option, shortupper=True) - def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None: + def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None: if not shortupper: for opt in option._short_opts: if opt[0] == "-" and opt[1].islower(): @@ -371,133 +371,133 @@ class OptionGroup: class MyOptionParser(argparse.ArgumentParser): - def __init__( - self, - parser: Parser, - extra_info: Optional[Dict[str, Any]] = None, - prog: Optional[str] = None, - ) -> None: + def __init__( + self, + parser: Parser, + extra_info: Optional[Dict[str, Any]] = None, + prog: Optional[str] = None, + ) -> None: self._parser = parser argparse.ArgumentParser.__init__( self, - prog=prog, + prog=prog, usage=parser._usage, add_help=False, formatter_class=DropShorterLongHelpFormatter, - allow_abbrev=False, + allow_abbrev=False, ) # extra_info is a dict of (param -> value) to display if there's - # an usage error to provide more contextual information to the user. - self.extra_info = extra_info if extra_info else {} - - def error(self, message: str) -> "NoReturn": - """Transform argparse error message into UsageError.""" - msg = f"{self.prog}: error: {message}" - - if hasattr(self._parser, "_config_source_hint"): - # Type ignored because the attribute is set dynamically. - msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore - - raise UsageError(self.format_usage() + msg) - - # Type ignored because typeshed has a very complex type in the superclass. - def parse_args( # type: ignore - self, - args: Optional[Sequence[str]] = None, - namespace: Optional[argparse.Namespace] = None, - ) -> argparse.Namespace: - """Allow splitting of positional arguments.""" - parsed, unrecognized = self.parse_known_args(args, namespace) - if unrecognized: - for arg in unrecognized: + # an usage error to provide more contextual information to the user. + self.extra_info = extra_info if extra_info else {} + + def error(self, message: str) -> "NoReturn": + """Transform argparse error message into UsageError.""" + msg = f"{self.prog}: error: {message}" + + if hasattr(self._parser, "_config_source_hint"): + # Type ignored because the attribute is set dynamically. + msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore + + raise UsageError(self.format_usage() + msg) + + # Type ignored because typeshed has a very complex type in the superclass. + def parse_args( # type: ignore + self, + args: Optional[Sequence[str]] = None, + namespace: Optional[argparse.Namespace] = None, + ) -> argparse.Namespace: + """Allow splitting of positional arguments.""" + parsed, unrecognized = self.parse_known_args(args, namespace) + if unrecognized: + for arg in unrecognized: if arg and arg[0] == "-": - lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))] + lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))] for k, v in sorted(self.extra_info.items()): - lines.append(f" {k}: {v}") + lines.append(f" {k}: {v}") self.error("\n".join(lines)) - getattr(parsed, FILE_OR_DIR).extend(unrecognized) - return parsed - - if sys.version_info[:2] < (3, 9): # pragma: no cover - # Backport of https://github.com/python/cpython/pull/14316 so we can - # disable long --argument abbreviations without breaking short flags. - def _parse_optional( - self, arg_string: str - ) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]: - if not arg_string: - return None - if not arg_string[0] in self.prefix_chars: - return None - if arg_string in self._option_string_actions: - action = self._option_string_actions[arg_string] - return action, arg_string, None - if len(arg_string) == 1: - return None - if "=" in arg_string: - option_string, explicit_arg = arg_string.split("=", 1) - if option_string in self._option_string_actions: - action = self._option_string_actions[option_string] - return action, option_string, explicit_arg - if self.allow_abbrev or not arg_string.startswith("--"): - option_tuples = self._get_option_tuples(arg_string) - if len(option_tuples) > 1: - msg = gettext( - "ambiguous option: %(option)s could match %(matches)s" - ) - options = ", ".join(option for _, option, _ in option_tuples) - self.error(msg % {"option": arg_string, "matches": options}) - elif len(option_tuples) == 1: - (option_tuple,) = option_tuples - return option_tuple - if self._negative_number_matcher.match(arg_string): - if not self._has_negative_number_optionals: - return None - if " " in arg_string: - return None - return None, arg_string, None - - + getattr(parsed, FILE_OR_DIR).extend(unrecognized) + return parsed + + if sys.version_info[:2] < (3, 9): # pragma: no cover + # Backport of https://github.com/python/cpython/pull/14316 so we can + # disable long --argument abbreviations without breaking short flags. + def _parse_optional( + self, arg_string: str + ) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]: + if not arg_string: + return None + if not arg_string[0] in self.prefix_chars: + return None + if arg_string in self._option_string_actions: + action = self._option_string_actions[arg_string] + return action, arg_string, None + if len(arg_string) == 1: + return None + if "=" in arg_string: + option_string, explicit_arg = arg_string.split("=", 1) + if option_string in self._option_string_actions: + action = self._option_string_actions[option_string] + return action, option_string, explicit_arg + if self.allow_abbrev or not arg_string.startswith("--"): + option_tuples = self._get_option_tuples(arg_string) + if len(option_tuples) > 1: + msg = gettext( + "ambiguous option: %(option)s could match %(matches)s" + ) + options = ", ".join(option for _, option, _ in option_tuples) + self.error(msg % {"option": arg_string, "matches": options}) + elif len(option_tuples) == 1: + (option_tuple,) = option_tuples + return option_tuple + if self._negative_number_matcher.match(arg_string): + if not self._has_negative_number_optionals: + return None + if " " in arg_string: + return None + return None, arg_string, None + + class DropShorterLongHelpFormatter(argparse.HelpFormatter): - """Shorten help for long options that differ only in extra hyphens. + """Shorten help for long options that differ only in extra hyphens. - - Collapse **long** options that are the same except for extra hyphens. - - Shortcut if there are only two options and one of them is a short one. - - Cache result on the action object as this is called at least 2 times. + - Collapse **long** options that are the same except for extra hyphens. + - Shortcut if there are only two options and one of them is a short one. + - Cache result on the action object as this is called at least 2 times. """ - def __init__(self, *args: Any, **kwargs: Any) -> None: - # Use more accurate terminal width. - if "width" not in kwargs: - kwargs["width"] = _pytest._io.get_terminal_width() - super().__init__(*args, **kwargs) - - def _format_action_invocation(self, action: argparse.Action) -> str: + def __init__(self, *args: Any, **kwargs: Any) -> None: + # Use more accurate terminal width. + if "width" not in kwargs: + kwargs["width"] = _pytest._io.get_terminal_width() + super().__init__(*args, **kwargs) + + def _format_action_invocation(self, action: argparse.Action) -> str: orgstr = argparse.HelpFormatter._format_action_invocation(self, action) if orgstr and orgstr[0] != "-": # only optional arguments return orgstr - res: Optional[str] = getattr(action, "_formatted_action_invocation", None) + res: Optional[str] = getattr(action, "_formatted_action_invocation", None) if res: return res options = orgstr.split(", ") if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2): # a shortcut for '-h, --help' or '--abc', '-a' - action._formatted_action_invocation = orgstr # type: ignore + action._formatted_action_invocation = orgstr # type: ignore return orgstr return_list = [] - short_long: Dict[str, str] = {} + short_long: Dict[str, str] = {} for option in options: if len(option) == 2 or option[2] == " ": continue if not option.startswith("--"): raise ArgumentError( - 'long optional argument without "--": [%s]' % (option), option + 'long optional argument without "--": [%s]' % (option), option ) xxoption = option[2:] - shortened = xxoption.replace("-", "") - if shortened not in short_long or len(short_long[shortened]) < len( - xxoption - ): - short_long[shortened] = xxoption + shortened = xxoption.replace("-", "") + if shortened not in short_long or len(short_long[shortened]) < len( + xxoption + ): + short_long[shortened] = xxoption # now short_long has been filled out to the longest with dashes # **and** we keep the right option ordering from add_argument for option in options: @@ -505,18 +505,18 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): return_list.append(option) if option[2:] == short_long.get(option.replace("-", "")): return_list.append(option.replace(" ", "=", 1)) - formatted_action_invocation = ", ".join(return_list) - action._formatted_action_invocation = formatted_action_invocation # type: ignore - return formatted_action_invocation - - def _split_lines(self, text, width): - """Wrap lines after splitting on original newlines. - - This allows to have explicit line breaks in the help text. - """ - import textwrap - - lines = [] - for line in text.splitlines(): - lines.extend(textwrap.wrap(line.strip(), width)) - return lines + formatted_action_invocation = ", ".join(return_list) + action._formatted_action_invocation = formatted_action_invocation # type: ignore + return formatted_action_invocation + + def _split_lines(self, text, width): + """Wrap lines after splitting on original newlines. + + This allows to have explicit line breaks in the help text. + """ + import textwrap + + lines = [] + for line in text.splitlines(): + lines.extend(textwrap.wrap(line.strip(), width)) + return lines diff --git a/contrib/python/pytest/py3/_pytest/config/exceptions.py b/contrib/python/pytest/py3/_pytest/config/exceptions.py index 4f1320e758..ab63bfd361 100644 --- a/contrib/python/pytest/py3/_pytest/config/exceptions.py +++ b/contrib/python/pytest/py3/_pytest/config/exceptions.py @@ -1,11 +1,11 @@ -from _pytest.compat import final - - -@final +from _pytest.compat import final + + +@final class UsageError(Exception): - """Error in pytest usage or invocation.""" + """Error in pytest usage or invocation.""" class PrintHelp(Exception): - """Raised when pytest should print its help to skip the rest of the + """Raised when pytest should print its help to skip the rest of the argument parsing and validation.""" diff --git a/contrib/python/pytest/py3/_pytest/config/findpaths.py b/contrib/python/pytest/py3/_pytest/config/findpaths.py index 2edf54536b..c599736a75 100644 --- a/contrib/python/pytest/py3/_pytest/config/findpaths.py +++ b/contrib/python/pytest/py3/_pytest/config/findpaths.py @@ -1,211 +1,211 @@ import os -from pathlib import Path -from typing import Dict -from typing import Iterable -from typing import List -from typing import Optional -from typing import Sequence -from typing import Tuple -from typing import TYPE_CHECKING -from typing import Union - -import iniconfig +from pathlib import Path +from typing import Dict +from typing import Iterable +from typing import List +from typing import Optional +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING +from typing import Union + +import iniconfig from .exceptions import UsageError -from _pytest.outcomes import fail -from _pytest.pathlib import absolutepath -from _pytest.pathlib import commonpath - -if TYPE_CHECKING: - from . import Config - - -def _parse_ini_config(path: Path) -> iniconfig.IniConfig: - """Parse the given generic '.ini' file using legacy IniConfig parser, returning - the parsed object. - - Raise UsageError if the file cannot be parsed. - """ +from _pytest.outcomes import fail +from _pytest.pathlib import absolutepath +from _pytest.pathlib import commonpath + +if TYPE_CHECKING: + from . import Config + + +def _parse_ini_config(path: Path) -> iniconfig.IniConfig: + """Parse the given generic '.ini' file using legacy IniConfig parser, returning + the parsed object. + + Raise UsageError if the file cannot be parsed. + """ try: - return iniconfig.IniConfig(str(path)) - except iniconfig.ParseError as exc: - raise UsageError(str(exc)) from exc - + return iniconfig.IniConfig(str(path)) + except iniconfig.ParseError as exc: + raise UsageError(str(exc)) from exc -def load_config_dict_from_file( - filepath: Path, -) -> Optional[Dict[str, Union[str, List[str]]]]: - """Load pytest configuration from the given file path, if supported. - Return None if the file does not contain valid pytest configuration. +def load_config_dict_from_file( + filepath: Path, +) -> Optional[Dict[str, Union[str, List[str]]]]: + """Load pytest configuration from the given file path, if supported. + + Return None if the file does not contain valid pytest configuration. """ - # Configuration from ini files are obtained from the [pytest] section, if present. - if filepath.suffix == ".ini": - iniconfig = _parse_ini_config(filepath) - - if "pytest" in iniconfig: - return dict(iniconfig["pytest"].items()) - else: - # "pytest.ini" files are always the source of configuration, even if empty. - if filepath.name == "pytest.ini": - return {} - - # '.cfg' files are considered if they contain a "[tool:pytest]" section. - elif filepath.suffix == ".cfg": - iniconfig = _parse_ini_config(filepath) - - if "tool:pytest" in iniconfig.sections: - return dict(iniconfig["tool:pytest"].items()) - elif "pytest" in iniconfig.sections: - # If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that - # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086). - fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) - - # '.toml' files are considered if they contain a [tool.pytest.ini_options] table. - elif filepath.suffix == ".toml": - import toml - - config = toml.load(str(filepath)) - - result = config.get("tool", {}).get("pytest", {}).get("ini_options", None) - if result is not None: - # TOML supports richer data types than ini files (strings, arrays, floats, ints, etc), - # however we need to convert all scalar values to str for compatibility with the rest - # of the configuration system, which expects strings only. - def make_scalar(v: object) -> Union[str, List[str]]: - return v if isinstance(v, list) else str(v) - - return {k: make_scalar(v) for k, v in result.items()} - - return None - - -def locate_config( - args: Iterable[Path], -) -> Tuple[ - Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]], -]: - """Search in the list of arguments for a valid ini-file for pytest, - and return a tuple of (rootdir, inifile, cfg-dict).""" - config_names = [ - "pytest.ini", - "pyproject.toml", - "tox.ini", - "setup.cfg", - ] + # Configuration from ini files are obtained from the [pytest] section, if present. + if filepath.suffix == ".ini": + iniconfig = _parse_ini_config(filepath) + + if "pytest" in iniconfig: + return dict(iniconfig["pytest"].items()) + else: + # "pytest.ini" files are always the source of configuration, even if empty. + if filepath.name == "pytest.ini": + return {} + + # '.cfg' files are considered if they contain a "[tool:pytest]" section. + elif filepath.suffix == ".cfg": + iniconfig = _parse_ini_config(filepath) + + if "tool:pytest" in iniconfig.sections: + return dict(iniconfig["tool:pytest"].items()) + elif "pytest" in iniconfig.sections: + # If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that + # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086). + fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) + + # '.toml' files are considered if they contain a [tool.pytest.ini_options] table. + elif filepath.suffix == ".toml": + import toml + + config = toml.load(str(filepath)) + + result = config.get("tool", {}).get("pytest", {}).get("ini_options", None) + if result is not None: + # TOML supports richer data types than ini files (strings, arrays, floats, ints, etc), + # however we need to convert all scalar values to str for compatibility with the rest + # of the configuration system, which expects strings only. + def make_scalar(v: object) -> Union[str, List[str]]: + return v if isinstance(v, list) else str(v) + + return {k: make_scalar(v) for k, v in result.items()} + + return None + + +def locate_config( + args: Iterable[Path], +) -> Tuple[ + Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]], +]: + """Search in the list of arguments for a valid ini-file for pytest, + and return a tuple of (rootdir, inifile, cfg-dict).""" + config_names = [ + "pytest.ini", + "pyproject.toml", + "tox.ini", + "setup.cfg", + ] args = [x for x in args if not str(x).startswith("-")] if not args: - args = [Path.cwd()] + args = [Path.cwd()] for arg in args: - argpath = absolutepath(arg) - for base in (argpath, *argpath.parents): - for config_name in config_names: - p = base / config_name - if p.is_file(): - ini_config = load_config_dict_from_file(p) - if ini_config is not None: - return base, p, ini_config - return None, None, {} - - -def get_common_ancestor(paths: Iterable[Path]) -> Path: - common_ancestor: Optional[Path] = None + argpath = absolutepath(arg) + for base in (argpath, *argpath.parents): + for config_name in config_names: + p = base / config_name + if p.is_file(): + ini_config = load_config_dict_from_file(p) + if ini_config is not None: + return base, p, ini_config + return None, None, {} + + +def get_common_ancestor(paths: Iterable[Path]) -> Path: + common_ancestor: Optional[Path] = None for path in paths: if not path.exists(): continue if common_ancestor is None: common_ancestor = path else: - if common_ancestor in path.parents or path == common_ancestor: + if common_ancestor in path.parents or path == common_ancestor: continue - elif path in common_ancestor.parents: + elif path in common_ancestor.parents: common_ancestor = path else: - shared = commonpath(path, common_ancestor) + shared = commonpath(path, common_ancestor) if shared is not None: common_ancestor = shared if common_ancestor is None: - common_ancestor = Path.cwd() - elif common_ancestor.is_file(): - common_ancestor = common_ancestor.parent + common_ancestor = Path.cwd() + elif common_ancestor.is_file(): + common_ancestor = common_ancestor.parent return common_ancestor -def get_dirs_from_args(args: Iterable[str]) -> List[Path]: - def is_option(x: str) -> bool: - return x.startswith("-") +def get_dirs_from_args(args: Iterable[str]) -> List[Path]: + def is_option(x: str) -> bool: + return x.startswith("-") - def get_file_part_from_node_id(x: str) -> str: - return x.split("::")[0] + def get_file_part_from_node_id(x: str) -> str: + return x.split("::")[0] - def get_dir_from_path(path: Path) -> Path: - if path.is_dir(): + def get_dir_from_path(path: Path) -> Path: + if path.is_dir(): return path - return path.parent - - def safe_exists(path: Path) -> bool: - # This can throw on paths that contain characters unrepresentable at the OS level, - # or with invalid syntax on Windows (https://bugs.python.org/issue35306) - try: - return path.exists() - except OSError: - return False - + return path.parent + + def safe_exists(path: Path) -> bool: + # This can throw on paths that contain characters unrepresentable at the OS level, + # or with invalid syntax on Windows (https://bugs.python.org/issue35306) + try: + return path.exists() + except OSError: + return False + # These look like paths but may not exist possible_paths = ( - absolutepath(get_file_part_from_node_id(arg)) + absolutepath(get_file_part_from_node_id(arg)) for arg in args if not is_option(arg) ) - return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)] - - -CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." + return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)] -def determine_setup( - inifile: Optional[str], - args: Sequence[str], - rootdir_cmd_arg: Optional[str] = None, - config: Optional["Config"] = None, -) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]: - rootdir = None +CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." + + +def determine_setup( + inifile: Optional[str], + args: Sequence[str], + rootdir_cmd_arg: Optional[str] = None, + config: Optional["Config"] = None, +) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]: + rootdir = None dirs = get_dirs_from_args(args) if inifile: - inipath_ = absolutepath(inifile) - inipath: Optional[Path] = inipath_ - inicfg = load_config_dict_from_file(inipath_) or {} - if rootdir_cmd_arg is None: - rootdir = get_common_ancestor(dirs) + inipath_ = absolutepath(inifile) + inipath: Optional[Path] = inipath_ + inicfg = load_config_dict_from_file(inipath_) or {} + if rootdir_cmd_arg is None: + rootdir = get_common_ancestor(dirs) else: ancestor = get_common_ancestor(dirs) - rootdir, inipath, inicfg = locate_config([ancestor]) - if rootdir is None and rootdir_cmd_arg is None: - for possible_rootdir in (ancestor, *ancestor.parents): - if (possible_rootdir / "setup.py").is_file(): - rootdir = possible_rootdir + rootdir, inipath, inicfg = locate_config([ancestor]) + if rootdir is None and rootdir_cmd_arg is None: + for possible_rootdir in (ancestor, *ancestor.parents): + if (possible_rootdir / "setup.py").is_file(): + rootdir = possible_rootdir break else: - if dirs != [ancestor]: - rootdir, inipath, inicfg = locate_config(dirs) + if dirs != [ancestor]: + rootdir, inipath, inicfg = locate_config(dirs) if rootdir is None: - if config is not None: - cwd = config.invocation_params.dir - else: - cwd = Path.cwd() - rootdir = get_common_ancestor([cwd, ancestor]) + if config is not None: + cwd = config.invocation_params.dir + else: + cwd = Path.cwd() + rootdir = get_common_ancestor([cwd, ancestor]) is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/" if is_fs_root: rootdir = ancestor if rootdir_cmd_arg: - rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg)) - if not rootdir.is_dir(): + rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg)) + if not rootdir.is_dir(): raise UsageError( "Directory '{}' not found. Check your '--rootdir' option.".format( - rootdir + rootdir ) ) - assert rootdir is not None - return rootdir, inipath, inicfg or {} + assert rootdir is not None + return rootdir, inipath, inicfg or {} |