Merge pull request #7401 from bluetech/typing-config

config: improve typing
This commit is contained in:
Ran Benita 2020-06-23 18:58:21 +03:00 committed by GitHub
commit 3624acb665
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 171 additions and 117 deletions

View File

@ -6,6 +6,7 @@ from .code import Frame
from .code import getfslineno from .code import getfslineno
from .code import getrawcode from .code import getrawcode
from .code import Traceback from .code import Traceback
from .code import TracebackEntry
from .source import compile_ as compile from .source import compile_ as compile
from .source import Source from .source import Source
@ -17,6 +18,7 @@ __all__ = [
"getfslineno", "getfslineno",
"getrawcode", "getrawcode",
"Traceback", "Traceback",
"TracebackEntry",
"compile", "compile",
"Source", "Source",
] ]

View File

@ -213,7 +213,7 @@ class TracebackEntry:
return source.getstatement(self.lineno) return source.getstatement(self.lineno)
@property @property
def path(self): def path(self) -> Union[py.path.local, str]:
""" path to the source code """ """ path to the source code """
return self.frame.code.path return self.frame.code.path

View File

@ -1,5 +1,6 @@
""" command line options, ini-file and conftest.py processing. """ """ command line options, ini-file and conftest.py processing. """
import argparse import argparse
import collections.abc
import contextlib import contextlib
import copy import copy
import enum import enum
@ -15,10 +16,13 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import Dict from typing import Dict
from typing import IO from typing import IO
from typing import Iterable
from typing import Iterator
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Sequence from typing import Sequence
from typing import Set from typing import Set
from typing import TextIO
from typing import Tuple from typing import Tuple
from typing import Union from typing import Union
@ -42,6 +46,7 @@ from _pytest.compat import TYPE_CHECKING
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import Skipped from _pytest.outcomes import Skipped
from _pytest.pathlib import import_path from _pytest.pathlib import import_path
from _pytest.pathlib import ImportMode
from _pytest.pathlib import Path from _pytest.pathlib import Path
from _pytest.store import Store from _pytest.store import Store
from _pytest.warning_types import PytestConfigWarning from _pytest.warning_types import PytestConfigWarning
@ -50,6 +55,7 @@ if TYPE_CHECKING:
from typing import Type from typing import Type
from _pytest._code.code import _TracebackStyle from _pytest._code.code import _TracebackStyle
from _pytest.terminal import TerminalReporter
from .argparsing import Argument from .argparsing import Argument
@ -88,18 +94,24 @@ class ExitCode(enum.IntEnum):
class ConftestImportFailure(Exception): class ConftestImportFailure(Exception):
def __init__(self, path, excinfo): def __init__(
self,
path: py.path.local,
excinfo: Tuple["Type[Exception]", Exception, TracebackType],
) -> None:
super().__init__(path, excinfo) super().__init__(path, excinfo)
self.path = path self.path = path
self.excinfo = excinfo # type: Tuple[Type[Exception], Exception, TracebackType] self.excinfo = excinfo
def __str__(self): def __str__(self) -> str:
return "{}: {} (from {})".format( return "{}: {} (from {})".format(
self.excinfo[0].__name__, self.excinfo[1], self.path self.excinfo[0].__name__, self.excinfo[1], self.path
) )
def filter_traceback_for_conftest_import_failure(entry) -> bool: def filter_traceback_for_conftest_import_failure(
entry: _pytest._code.TracebackEntry,
) -> bool:
"""filters tracebacks entries which point to pytest internals or importlib. """filters 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 Make a special case for importlib because we use it to import test modules and conftest files
@ -108,7 +120,10 @@ def filter_traceback_for_conftest_import_failure(entry) -> bool:
return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep) return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep)
def main(args=None, plugins=None) -> Union[int, ExitCode]: def main(
args: Optional[List[str]] = None,
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
) -> Union[int, ExitCode]:
""" return exit code, after performing an in-process test run. """ return exit code, after performing an in-process test run.
:arg args: list of command line arguments. :arg args: list of command line arguments.
@ -177,7 +192,7 @@ class cmdline: # compatibility namespace
main = staticmethod(main) main = staticmethod(main)
def filename_arg(path, optname): def filename_arg(path: str, optname: str) -> str:
""" Argparse type validator for filename arguments. """ Argparse type validator for filename arguments.
:path: path of filename :path: path of filename
@ -188,7 +203,7 @@ def filename_arg(path, optname):
return path return path
def directory_arg(path, optname): def directory_arg(path: str, optname: str) -> str:
"""Argparse type validator for directory arguments. """Argparse type validator for directory arguments.
:path: path of directory :path: path of directory
@ -239,13 +254,16 @@ builtin_plugins = set(default_plugins)
builtin_plugins.add("pytester") builtin_plugins.add("pytester")
def get_config(args=None, plugins=None): 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 # subsequent calls to main will create a fresh instance
pluginmanager = PytestPluginManager() pluginmanager = PytestPluginManager()
config = Config( config = Config(
pluginmanager, pluginmanager,
invocation_params=Config.InvocationParams( invocation_params=Config.InvocationParams(
args=args or (), plugins=plugins, dir=Path.cwd() args=args or (), plugins=plugins, dir=Path.cwd(),
), ),
) )
@ -255,10 +273,11 @@ def get_config(args=None, plugins=None):
for spec in default_plugins: for spec in default_plugins:
pluginmanager.import_plugin(spec) pluginmanager.import_plugin(spec)
return config return config
def get_plugin_manager(): def get_plugin_manager() -> "PytestPluginManager":
""" """
Obtain a new instance of the Obtain a new instance of the
:py:class:`_pytest.config.PytestPluginManager`, with default plugins :py:class:`_pytest.config.PytestPluginManager`, with default plugins
@ -271,8 +290,9 @@ def get_plugin_manager():
def _prepareconfig( def _prepareconfig(
args: Optional[Union[py.path.local, List[str]]] = None, plugins=None args: Optional[Union[py.path.local, List[str]]] = None,
): plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
) -> "Config":
if args is None: if args is None:
args = sys.argv[1:] args = sys.argv[1:]
elif isinstance(args, py.path.local): elif isinstance(args, py.path.local):
@ -290,9 +310,10 @@ def _prepareconfig(
pluginmanager.consider_pluginarg(plugin) pluginmanager.consider_pluginarg(plugin)
else: else:
pluginmanager.register(plugin) pluginmanager.register(plugin)
return pluginmanager.hook.pytest_cmdline_parse( config = pluginmanager.hook.pytest_cmdline_parse(
pluginmanager=pluginmanager, args=args pluginmanager=pluginmanager, args=args
) )
return config
except BaseException: except BaseException:
config._ensure_unconfigure() config._ensure_unconfigure()
raise raise
@ -313,13 +334,11 @@ class PytestPluginManager(PluginManager):
super().__init__("pytest") super().__init__("pytest")
# The objects are module objects, only used generically. # The objects are module objects, only used generically.
self._conftest_plugins = set() # type: Set[object] self._conftest_plugins = set() # type: Set[types.ModuleType]
# state related to local conftest plugins # State related to local conftest plugins.
# Maps a py.path.local to a list of module objects. self._dirpath2confmods = {} # type: Dict[py.path.local, List[types.ModuleType]]
self._dirpath2confmods = {} # type: Dict[Any, List[object]] self._conftestpath2mod = {} # type: Dict[Path, types.ModuleType]
# Maps a py.path.local to a module object.
self._conftestpath2mod = {} # type: Dict[Any, object]
self._confcutdir = None # type: Optional[py.path.local] self._confcutdir = None # type: Optional[py.path.local]
self._noconftest = False self._noconftest = False
self._duplicatepaths = set() # type: Set[py.path.local] self._duplicatepaths = set() # type: Set[py.path.local]
@ -328,7 +347,7 @@ class PytestPluginManager(PluginManager):
self.register(self) self.register(self)
if os.environ.get("PYTEST_DEBUG"): if os.environ.get("PYTEST_DEBUG"):
err = sys.stderr # type: IO[str] err = sys.stderr # type: IO[str]
encoding = getattr(err, "encoding", "utf8") encoding = getattr(err, "encoding", "utf8") # type: str
try: try:
err = open( err = open(
os.dup(err.fileno()), mode=err.mode, buffering=1, encoding=encoding, os.dup(err.fileno()), mode=err.mode, buffering=1, encoding=encoding,
@ -343,7 +362,7 @@ class PytestPluginManager(PluginManager):
# 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 self._configured = False
def parse_hookimpl_opts(self, plugin, name): def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):
# pytest hooks are always prefixed with pytest_ # pytest hooks are always prefixed with pytest_
# so we avoid accessing possibly non-readable attributes # so we avoid accessing possibly non-readable attributes
# (see issue #1073) # (see issue #1073)
@ -372,7 +391,7 @@ class PytestPluginManager(PluginManager):
opts.setdefault(name, hasattr(method, name) or name in known_marks) opts.setdefault(name, hasattr(method, name) or name in known_marks)
return opts return opts
def parse_hookspec_opts(self, module_or_class, name): def parse_hookspec_opts(self, module_or_class, name: str):
opts = super().parse_hookspec_opts(module_or_class, name) opts = super().parse_hookspec_opts(module_or_class, name)
if opts is None: if opts is None:
method = getattr(module_or_class, name) method = getattr(module_or_class, name)
@ -389,7 +408,9 @@ class PytestPluginManager(PluginManager):
} }
return opts return opts
def register(self, plugin: _PluggyPlugin, name: Optional[str] = None): def register(
self, plugin: _PluggyPlugin, name: Optional[str] = None
) -> Optional[str]:
if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS: if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS:
warnings.warn( warnings.warn(
PytestConfigWarning( PytestConfigWarning(
@ -399,8 +420,8 @@ class PytestPluginManager(PluginManager):
) )
) )
) )
return return None
ret = super().register(plugin, name) ret = super().register(plugin, name) # type: Optional[str]
if ret: if ret:
self.hook.pytest_plugin_registered.call_historic( self.hook.pytest_plugin_registered.call_historic(
kwargs=dict(plugin=plugin, manager=self) kwargs=dict(plugin=plugin, manager=self)
@ -410,11 +431,12 @@ class PytestPluginManager(PluginManager):
self.consider_module(plugin) self.consider_module(plugin)
return ret return ret
def getplugin(self, name): def getplugin(self, name: str):
# support deprecated naming because plugins (xdist e.g.) use it # support deprecated naming because plugins (xdist e.g.) use it
return self.get_plugin(name) plugin = self.get_plugin(name) # type: Optional[_PluggyPlugin]
return plugin
def hasplugin(self, name): def hasplugin(self, name: str) -> bool:
"""Return True if the plugin with the given name is registered.""" """Return True if the plugin with the given name is registered."""
return bool(self.get_plugin(name)) return bool(self.get_plugin(name))
@ -436,7 +458,7 @@ class PytestPluginManager(PluginManager):
# #
# internal API for local conftest plugin handling # internal API for local conftest plugin handling
# #
def _set_initial_conftests(self, namespace): def _set_initial_conftests(self, namespace: argparse.Namespace) -> None:
""" load initial conftest files given a preparsed "namespace". """ load initial conftest files given a preparsed "namespace".
As conftest files may add their own command line options As conftest files may add their own command line options
which have arguments ('--my-opt somepath') we might get some which have arguments ('--my-opt somepath') we might get some
@ -454,8 +476,8 @@ class PytestPluginManager(PluginManager):
self._using_pyargs = namespace.pyargs self._using_pyargs = namespace.pyargs
testpaths = namespace.file_or_dir testpaths = namespace.file_or_dir
foundanchor = False foundanchor = False
for path in testpaths: for testpath in testpaths:
path = str(path) path = str(testpath)
# remove node-id syntax # remove node-id syntax
i = path.find("::") i = path.find("::")
if i != -1: if i != -1:
@ -467,7 +489,9 @@ class PytestPluginManager(PluginManager):
if not foundanchor: if not foundanchor:
self._try_load_conftest(current, namespace.importmode) self._try_load_conftest(current, namespace.importmode)
def _try_load_conftest(self, anchor, importmode): def _try_load_conftest(
self, anchor: py.path.local, importmode: Union[str, ImportMode]
) -> None:
self._getconftestmodules(anchor, importmode) self._getconftestmodules(anchor, importmode)
# let's also consider test* subdirs # let's also consider test* subdirs
if anchor.check(dir=1): if anchor.check(dir=1):
@ -476,7 +500,9 @@ class PytestPluginManager(PluginManager):
self._getconftestmodules(x, importmode) self._getconftestmodules(x, importmode)
@lru_cache(maxsize=128) @lru_cache(maxsize=128)
def _getconftestmodules(self, path, importmode): def _getconftestmodules(
self, path: py.path.local, importmode: Union[str, ImportMode],
) -> List[types.ModuleType]:
if self._noconftest: if self._noconftest:
return [] return []
@ -499,7 +525,9 @@ class PytestPluginManager(PluginManager):
self._dirpath2confmods[directory] = clist self._dirpath2confmods[directory] = clist
return clist return clist
def _rget_with_confmod(self, name, 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) modules = self._getconftestmodules(path, importmode)
for mod in reversed(modules): for mod in reversed(modules):
try: try:
@ -508,7 +536,9 @@ class PytestPluginManager(PluginManager):
continue continue
raise KeyError(name) raise KeyError(name)
def _importconftest(self, conftestpath, importmode): 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 # Use a resolved Path object as key to avoid loading the same conftest twice
# with build systems that create build directories containing # with build systems that create build directories containing
# symlinks to actual files. # symlinks to actual files.
@ -526,7 +556,9 @@ class PytestPluginManager(PluginManager):
try: try:
mod = import_path(conftestpath, mode=importmode) mod = import_path(conftestpath, mode=importmode)
except Exception as e: except Exception as e:
raise ConftestImportFailure(conftestpath, sys.exc_info()) from 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._check_non_top_pytest_plugins(mod, conftestpath)
@ -542,7 +574,9 @@ class PytestPluginManager(PluginManager):
self.consider_conftest(mod) self.consider_conftest(mod)
return mod return mod
def _check_non_top_pytest_plugins(self, mod, conftestpath): def _check_non_top_pytest_plugins(
self, mod: types.ModuleType, conftestpath: py.path.local,
) -> None:
if ( if (
hasattr(mod, "pytest_plugins") hasattr(mod, "pytest_plugins")
and self._configured and self._configured
@ -564,7 +598,9 @@ class PytestPluginManager(PluginManager):
# #
# #
def consider_preparse(self, args, *, exclude_only: bool = False) -> None: def consider_preparse(
self, args: Sequence[str], *, exclude_only: bool = False
) -> None:
i = 0 i = 0
n = len(args) n = len(args)
while i < n: while i < n:
@ -585,7 +621,7 @@ class PytestPluginManager(PluginManager):
continue continue
self.consider_pluginarg(parg) self.consider_pluginarg(parg)
def consider_pluginarg(self, arg) -> None: def consider_pluginarg(self, arg: str) -> None:
if arg.startswith("no:"): if arg.startswith("no:"):
name = arg[3:] name = arg[3:]
if name in essential_plugins: if name in essential_plugins:
@ -610,7 +646,7 @@ class PytestPluginManager(PluginManager):
del self._name2plugin["pytest_" + name] del self._name2plugin["pytest_" + name]
self.import_plugin(arg, consider_entry_points=True) self.import_plugin(arg, consider_entry_points=True)
def consider_conftest(self, conftestmodule) -> None: def consider_conftest(self, conftestmodule: types.ModuleType) -> None:
self.register(conftestmodule, name=conftestmodule.__file__) self.register(conftestmodule, name=conftestmodule.__file__)
def consider_env(self) -> None: def consider_env(self) -> None:
@ -619,7 +655,9 @@ class PytestPluginManager(PluginManager):
def consider_module(self, mod: types.ModuleType) -> None: def consider_module(self, mod: types.ModuleType) -> None:
self._import_plugin_specs(getattr(mod, "pytest_plugins", [])) self._import_plugin_specs(getattr(mod, "pytest_plugins", []))
def _import_plugin_specs(self, spec): def _import_plugin_specs(
self, spec: Union[None, types.ModuleType, str, Sequence[str]]
) -> None:
plugins = _get_plugin_specs_as_list(spec) plugins = _get_plugin_specs_as_list(spec)
for import_spec in plugins: for import_spec in plugins:
self.import_plugin(import_spec) self.import_plugin(import_spec)
@ -636,7 +674,6 @@ class PytestPluginManager(PluginManager):
assert isinstance(modname, str), ( assert isinstance(modname, str), (
"module name as text required, got %r" % modname "module name as text required, got %r" % modname
) )
modname = str(modname)
if self.is_blocked(modname) or self.get_plugin(modname) is not None: if self.is_blocked(modname) or self.get_plugin(modname) is not None:
return return
@ -668,27 +705,29 @@ class PytestPluginManager(PluginManager):
self.register(mod, modname) self.register(mod, modname)
def _get_plugin_specs_as_list(specs): def _get_plugin_specs_as_list(
""" specs: Union[None, types.ModuleType, str, Sequence[str]]
Parses a list of "plugin specs" and returns a list of plugin names. ) -> List[str]:
"""Parse a plugins specification into a list of plugin names."""
Plugin specs can be given as a list of strings separated by "," or already as a list/tuple in # None means empty.
which case it is returned as a list. Specs can also be `None` in which case an if specs is None:
empty list is returned.
"""
if specs is not None and not isinstance(specs, types.ModuleType):
if isinstance(specs, str):
specs = specs.split(",") if specs else []
if not isinstance(specs, (list, tuple)):
raise UsageError(
"Plugin specs must be a ','-separated string or a "
"list/tuple of strings for plugin names. Given: %r" % specs
)
return list(specs)
return [] 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
)
def _ensure_removed_sysmodule(modname): def _ensure_removed_sysmodule(modname: str) -> None:
try: try:
del sys.modules[modname] del sys.modules[modname]
except KeyError: except KeyError:
@ -703,7 +742,7 @@ class Notset:
notset = Notset() notset = Notset()
def _iter_rewritable_modules(package_files): 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 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 marked for assertion rewrite (for example the package "pytest_mock/__init__.py" should
@ -766,6 +805,10 @@ def _iter_rewritable_modules(package_files):
yield from _iter_rewritable_modules(new_package_files) yield from _iter_rewritable_modules(new_package_files)
def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:
return tuple(args)
class Config: class Config:
""" """
Access to configuration values, pluginmanager and plugin hooks. Access to configuration values, pluginmanager and plugin hooks.
@ -793,9 +836,9 @@ class Config:
Plugins accessing ``InvocationParams`` must be aware of that. Plugins accessing ``InvocationParams`` must be aware of that.
""" """
args = attr.ib(converter=tuple) args = attr.ib(type=Tuple[str, ...], converter=_args_converter)
"""tuple of command-line arguments as passed to ``pytest.main()``.""" """tuple of command-line arguments as passed to ``pytest.main()``."""
plugins = attr.ib() plugins = attr.ib(type=Optional[Sequence[Union[str, _PluggyPlugin]]])
"""list of extra plugins, might be `None`.""" """list of extra plugins, might be `None`."""
dir = attr.ib(type=Path) dir = attr.ib(type=Path)
"""directory where ``pytest.main()`` was invoked from.""" """directory where ``pytest.main()`` was invoked from."""
@ -855,7 +898,7 @@ class Config:
"""Backward compatibility""" """Backward compatibility"""
return py.path.local(str(self.invocation_params.dir)) return py.path.local(str(self.invocation_params.dir))
def add_cleanup(self, func) -> None: def add_cleanup(self, func: Callable[[], None]) -> None:
""" Add a function to be called when the config object gets out of """ Add a function to be called when the config object gets out of
use (usually coninciding with pytest_unconfigure).""" use (usually coninciding with pytest_unconfigure)."""
self._cleanup.append(func) self._cleanup.append(func)
@ -876,12 +919,15 @@ class Config:
fin = self._cleanup.pop() fin = self._cleanup.pop()
fin() fin()
def get_terminal_writer(self): def get_terminal_writer(self) -> TerminalWriter:
return self.pluginmanager.get_plugin("terminalreporter")._tw terminalreporter = self.pluginmanager.get_plugin(
"terminalreporter"
) # type: TerminalReporter
return terminalreporter._tw
def pytest_cmdline_parse( def pytest_cmdline_parse(
self, pluginmanager: PytestPluginManager, args: List[str] self, pluginmanager: PytestPluginManager, args: List[str]
) -> object: ) -> "Config":
try: try:
self.parse(args) self.parse(args)
except UsageError: except UsageError:
@ -923,7 +969,7 @@ class Config:
sys.stderr.write("INTERNALERROR> %s\n" % line) sys.stderr.write("INTERNALERROR> %s\n" % line)
sys.stderr.flush() sys.stderr.flush()
def cwd_relative_nodeid(self, nodeid): def cwd_relative_nodeid(self, nodeid: str) -> str:
# nodeid's are relative to the rootpath, compute relative to cwd # nodeid's are relative to the rootpath, compute relative to cwd
if self.invocation_dir != self.rootdir: if self.invocation_dir != self.rootdir:
fullpath = self.rootdir.join(nodeid) fullpath = self.rootdir.join(nodeid)
@ -931,7 +977,7 @@ class Config:
return nodeid return nodeid
@classmethod @classmethod
def fromdictargs(cls, option_dict, args): def fromdictargs(cls, option_dict, args) -> "Config":
""" constructor usable for subprocesses. """ """ constructor usable for subprocesses. """
config = get_config(args) config = get_config(args)
config.option.__dict__.update(option_dict) config.option.__dict__.update(option_dict)
@ -949,7 +995,7 @@ class Config:
setattr(self.option, opt.dest, opt.default) setattr(self.option, opt.dest, opt.default)
@hookimpl(trylast=True) @hookimpl(trylast=True)
def pytest_load_initial_conftests(self, early_config): def pytest_load_initial_conftests(self, early_config: "Config") -> None:
self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
def _initini(self, args: Sequence[str]) -> None: def _initini(self, args: Sequence[str]) -> None:
@ -1078,7 +1124,7 @@ class Config:
raise raise
self._validate_keys() self._validate_keys()
def _checkversion(self): def _checkversion(self) -> None:
import pytest import pytest
minver = self.inicfg.get("minversion", None) minver = self.inicfg.get("minversion", None)
@ -1167,7 +1213,7 @@ class Config:
except PrintHelp: except PrintHelp:
pass pass
def addinivalue_line(self, name, line): def addinivalue_line(self, name: str, line: str) -> None:
""" add a line to an ini-file option. The option must have been """ 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 declared but might not yet be set in which case the line becomes the
the first line in its value. """ the first line in its value. """
@ -1186,7 +1232,7 @@ class Config:
self._inicache[name] = val = self._getini(name) self._inicache[name] = val = self._getini(name)
return val return val
def _getini(self, name: str) -> Any: def _getini(self, name: str):
try: try:
description, type, default = self._parser._inidict[name] description, type, default = self._parser._inidict[name]
except KeyError as e: except KeyError as e:
@ -1231,12 +1277,14 @@ class Config:
else: else:
return value return value
elif type == "bool": elif type == "bool":
return bool(_strtobool(str(value).strip())) return _strtobool(str(value).strip())
else: else:
assert type is None assert type is None
return value return value
def _getconftest_pathlist(self, name, path): def _getconftest_pathlist(
self, name: str, path: py.path.local
) -> Optional[List[py.path.local]]:
try: try:
mod, relroots = self.pluginmanager._rget_with_confmod( mod, relroots = self.pluginmanager._rget_with_confmod(
name, path, self.getoption("importmode") name, path, self.getoption("importmode")
@ -1244,7 +1292,7 @@ class Config:
except KeyError: except KeyError:
return None return None
modpath = py.path.local(mod.__file__).dirpath() modpath = py.path.local(mod.__file__).dirpath()
values = [] values = [] # type: List[py.path.local]
for relroot in relroots: for relroot in relroots:
if not isinstance(relroot, py.path.local): if not isinstance(relroot, py.path.local):
relroot = relroot.replace("/", py.path.local.sep) relroot = relroot.replace("/", py.path.local.sep)
@ -1295,16 +1343,16 @@ class Config:
pytest.skip("no {!r} option found".format(name)) pytest.skip("no {!r} option found".format(name))
raise ValueError("no option named {!r}".format(name)) from e raise ValueError("no option named {!r}".format(name)) from e
def getvalue(self, name, path=None): def getvalue(self, name: str, path=None):
""" (deprecated, use getoption()) """ """ (deprecated, use getoption()) """
return self.getoption(name) return self.getoption(name)
def getvalueorskip(self, name, path=None): def getvalueorskip(self, name: str, path=None):
""" (deprecated, use getoption(skip=True)) """ """ (deprecated, use getoption(skip=True)) """
return self.getoption(name, skip=True) return self.getoption(name, skip=True)
def _assertion_supported(): def _assertion_supported() -> bool:
try: try:
assert False assert False
except AssertionError: except AssertionError:
@ -1313,7 +1361,7 @@ def _assertion_supported():
return False return False
def _warn_about_missing_assertion(mode): def _warn_about_missing_assertion(mode) -> None:
if not _assertion_supported(): if not _assertion_supported():
if mode == "plain": if mode == "plain":
sys.stderr.write( sys.stderr.write(
@ -1331,12 +1379,14 @@ def _warn_about_missing_assertion(mode):
) )
def create_terminal_writer(config: Config, *args, **kwargs) -> TerminalWriter: def create_terminal_writer(
config: Config, file: Optional[TextIO] = None
) -> TerminalWriter:
"""Create a TerminalWriter instance configured according to the options """Create a TerminalWriter instance configured according to the options
in the config object. Every code which requires a TerminalWriter object in the config object. Every code which requires a TerminalWriter object
and has access to a config object should use this function. and has access to a config object should use this function.
""" """
tw = TerminalWriter(*args, **kwargs) tw = TerminalWriter(file=file)
if config.option.color == "yes": if config.option.color == "yes":
tw.hasmarkup = True tw.hasmarkup = True
if config.option.color == "no": if config.option.color == "no":
@ -1344,8 +1394,8 @@ def create_terminal_writer(config: Config, *args, **kwargs) -> TerminalWriter:
return tw return tw
def _strtobool(val): def _strtobool(val: str) -> bool:
"""Convert a string representation of truth to true (1) or false (0). """Convert a string representation of truth to True or False.
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
@ -1355,8 +1405,8 @@ def _strtobool(val):
""" """
val = val.lower() val = val.lower()
if val in ("y", "yes", "t", "true", "on", "1"): if val in ("y", "yes", "t", "true", "on", "1"):
return 1 return True
elif val in ("n", "no", "f", "false", "off", "0"): elif val in ("n", "no", "f", "false", "off", "0"):
return 0 return False
else: else:
raise ValueError("invalid truth value {!r}".format(val)) raise ValueError("invalid truth value {!r}".format(val))

View File

@ -96,7 +96,7 @@ def pytest_addoption(parser: Parser) -> None:
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_cmdline_parse(): def pytest_cmdline_parse():
outcome = yield outcome = yield
config = outcome.get_result() config = outcome.get_result() # type: Config
if config.option.debug: if config.option.debug:
path = os.path.abspath("pytestdebug.log") path = os.path.abspath("pytestdebug.log")
debugfile = open(path, "w") debugfile = open(path, "w")
@ -124,7 +124,7 @@ def pytest_cmdline_parse():
config.add_cleanup(unset_tracing) config.add_cleanup(unset_tracing)
def showversion(config): def showversion(config: Config) -> None:
if config.option.version > 1: if config.option.version > 1:
sys.stderr.write( sys.stderr.write(
"This is pytest version {}, imported from {}\n".format( "This is pytest version {}, imported from {}\n".format(
@ -224,7 +224,7 @@ def showhelp(config: Config) -> None:
conftest_options = [("pytest_plugins", "list of plugin names to load")] conftest_options = [("pytest_plugins", "list of plugin names to load")]
def getpluginversioninfo(config): def getpluginversioninfo(config: Config) -> List[str]:
lines = [] lines = []
plugininfo = config.pluginmanager.list_plugin_distinfo() plugininfo = config.pluginmanager.list_plugin_distinfo()
if plugininfo: if plugininfo:

View File

@ -143,7 +143,7 @@ def pytest_configure(config: "Config") -> None:
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_cmdline_parse( def pytest_cmdline_parse(
pluginmanager: "PytestPluginManager", args: List[str] pluginmanager: "PytestPluginManager", args: List[str]
) -> Optional[object]: ) -> Optional["Config"]:
"""return initialized config object, parsing the specified args. """return initialized config object, parsing the specified args.
Stops at first non-None result, see :ref:`firstresult` Stops at first non-None result, see :ref:`firstresult`

View File

@ -141,9 +141,14 @@ class PercentStyleMultiline(logging.PercentStyle):
if auto_indent_option is None: if auto_indent_option is None:
return 0 return 0
elif type(auto_indent_option) is int: elif isinstance(auto_indent_option, bool):
if auto_indent_option:
return -1
else:
return 0
elif isinstance(auto_indent_option, int):
return int(auto_indent_option) return int(auto_indent_option)
elif type(auto_indent_option) is str: elif isinstance(auto_indent_option, str):
try: try:
return int(auto_indent_option) return int(auto_indent_option)
except ValueError: except ValueError:
@ -153,9 +158,6 @@ class PercentStyleMultiline(logging.PercentStyle):
return -1 return -1
except ValueError: except ValueError:
return 0 return 0
elif type(auto_indent_option) is bool:
if auto_indent_option:
return -1
return 0 return 0

View File

@ -466,7 +466,7 @@ def import_path(
""" """
mode = ImportMode(mode) mode = ImportMode(mode)
path = Path(p) path = Path(str(p))
if not path.exists(): if not path.exists():
raise ImportError(path) raise ImportError(path)

View File

@ -1054,7 +1054,7 @@ class Testdir:
args.append("--basetemp=%s" % self.tmpdir.dirpath("basetemp")) args.append("--basetemp=%s" % self.tmpdir.dirpath("basetemp"))
return args return args
def parseconfig(self, *args: Union[str, py.path.local]) -> Config: def parseconfig(self, *args) -> Config:
"""Return a new pytest Config instance from given commandline args. """Return a new pytest Config instance from given commandline args.
This invokes the pytest bootstrapping code in _pytest.config to create This invokes the pytest bootstrapping code in _pytest.config to create
@ -1070,14 +1070,14 @@ class Testdir:
import _pytest.config import _pytest.config
config = _pytest.config._prepareconfig(args, self.plugins) # type: Config config = _pytest.config._prepareconfig(args, self.plugins) # type: ignore[arg-type]
# we don't know what the test will do with this half-setup config # we don't know what the test will do with this half-setup config
# object and thus we make sure it gets unconfigured properly in any # object and thus we make sure it gets unconfigured properly in any
# case (otherwise capturing could still be active, for example) # case (otherwise capturing could still be active, for example)
self.request.addfinalizer(config._ensure_unconfigure) self.request.addfinalizer(config._ensure_unconfigure)
return config return config
def parseconfigure(self, *args): def parseconfigure(self, *args) -> Config:
"""Return a new pytest configured Config instance. """Return a new pytest configured Config instance.
This returns a new :py:class:`_pytest.config.Config` instance like This returns a new :py:class:`_pytest.config.Config` instance like
@ -1318,7 +1318,7 @@ class Testdir:
Returns a :py:class:`RunResult`. Returns a :py:class:`RunResult`.
""" """
__tracebackhide__ = True __tracebackhide__ = True
p = make_numbered_dir(root=Path(self.tmpdir), prefix="runpytest-") p = make_numbered_dir(root=Path(str(self.tmpdir)), prefix="runpytest-")
args = ("--basetemp=%s" % p,) + args args = ("--basetemp=%s" % p,) + args
plugins = [x for x in self.plugins if isinstance(x, str)] plugins = [x for x in self.plugins if isinstance(x, str)]
if plugins: if plugins:

View File

@ -13,6 +13,7 @@ from .pathlib import LOCK_TIMEOUT
from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import Path from .pathlib import Path
from _pytest.config import Config
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
@ -135,7 +136,7 @@ def get_user() -> Optional[str]:
return None return None
def pytest_configure(config) -> None: def pytest_configure(config: Config) -> None:
"""Create a TempdirFactory and attach it to the config object. """Create a TempdirFactory and attach it to the config object.
This is to comply with existing plugins which expect the handler to be This is to comply with existing plugins which expect the handler to be

View File

@ -585,11 +585,11 @@ class TestInvocationVariants:
# Type ignored because `py.test` is not and will not be typed. # Type ignored because `py.test` is not and will not be typed.
assert pytest.main == py.test.cmdline.main # type: ignore[attr-defined] assert pytest.main == py.test.cmdline.main # type: ignore[attr-defined]
def test_invoke_with_invalid_type(self): def test_invoke_with_invalid_type(self) -> None:
with pytest.raises( with pytest.raises(
TypeError, match="expected to be a list of strings, got: '-h'" TypeError, match="expected to be a list of strings, got: '-h'"
): ):
pytest.main("-h") pytest.main("-h") # type: ignore[arg-type]
def test_invoke_with_path(self, tmpdir, capsys): def test_invoke_with_path(self, tmpdir, capsys):
retcode = pytest.main(tmpdir) retcode = pytest.main(tmpdir)

View File

@ -372,7 +372,7 @@ def test_excinfo_no_python_sourcecode(tmpdir):
for item in excinfo.traceback: for item in excinfo.traceback:
print(item) # XXX: for some reason jinja.Template.render is printed in full print(item) # XXX: for some reason jinja.Template.render is printed in full
item.source # shouldn't fail item.source # shouldn't fail
if item.path.basename == "test.txt": if isinstance(item.path, py.path.local) and item.path.basename == "test.txt":
assert str(item.source) == "{{ h()}}:" assert str(item.source) == "{{ h()}}:"

View File

@ -11,6 +11,7 @@ import py.path
import _pytest._code import _pytest._code
import pytest import pytest
from _pytest.compat import importlib_metadata from _pytest.compat import importlib_metadata
from _pytest.config import _get_plugin_specs_as_list
from _pytest.config import _iter_rewritable_modules from _pytest.config import _iter_rewritable_modules
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ConftestImportFailure from _pytest.config import ConftestImportFailure
@ -1115,21 +1116,17 @@ def test_load_initial_conftest_last_ordering(_config_for_test):
assert [x.function.__module__ for x in values] == expected assert [x.function.__module__ for x in values] == expected
def test_get_plugin_specs_as_list(): def test_get_plugin_specs_as_list() -> None:
from _pytest.config import _get_plugin_specs_as_list def exp_match(val: object) -> str:
def exp_match(val):
return ( return (
"Plugin specs must be a ','-separated string" "Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %s"
" or a list/tuple of strings for plugin names. Given: {}".format( % re.escape(repr(val))
re.escape(repr(val))
)
) )
with pytest.raises(pytest.UsageError, match=exp_match({"foo"})): with pytest.raises(pytest.UsageError, match=exp_match({"foo"})):
_get_plugin_specs_as_list({"foo"}) _get_plugin_specs_as_list({"foo"}) # type: ignore[arg-type]
with pytest.raises(pytest.UsageError, match=exp_match({})): with pytest.raises(pytest.UsageError, match=exp_match({})):
_get_plugin_specs_as_list(dict()) _get_plugin_specs_as_list(dict()) # type: ignore[arg-type]
assert _get_plugin_specs_as_list(None) == [] assert _get_plugin_specs_as_list(None) == []
assert _get_plugin_specs_as_list("") == [] assert _get_plugin_specs_as_list("") == []
@ -1778,5 +1775,7 @@ def test_conftest_import_error_repr(tmpdir):
): ):
try: try:
raise RuntimeError("some error") raise RuntimeError("some error")
except Exception as e: except Exception as exc:
raise ConftestImportFailure(path, sys.exc_info()) from e assert exc.__traceback__ is not None
exc_info = (type(exc), exc, exc.__traceback__)
raise ConftestImportFailure(path, exc_info) from exc