diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index a40b23470..121ef3a9b 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -928,8 +928,13 @@ class TerminalRepr: raise NotImplementedError() +# This class is abstract -- only subclasses are instantiated. @attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore class ExceptionRepr(TerminalRepr): + # Provided by in subclasses. + reprcrash = None # type: Optional[ReprFileLocation] + reprtraceback = None # type: ReprTraceback + def __attrs_post_init__(self): self.sections = [] # type: List[Tuple[str, str, str]] diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index cec0c5501..e77b1b0b8 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -23,6 +23,8 @@ from typing import Set from typing import Tuple from typing import Union +import py + from _pytest._io.saferepr import saferepr from _pytest._version import version from _pytest.assertion import util @@ -177,10 +179,10 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) """ if self.session is not None and not self._session_paths_checked: self._session_paths_checked = True - for path in self.session._initialpaths: + for initial_path in self.session._initialpaths: # Make something as c:/projects/my_project/path.py -> # ['c:', 'projects', 'my_project', 'path.py'] - parts = str(path).split(os.path.sep) + parts = str(initial_path).split(os.path.sep) # add 'path' to basenames to be checked. self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0]) @@ -213,7 +215,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) return True if self.session is not None: - if self.session.isinitpath(fn): + if self.session.isinitpath(py.path.local(fn)): state.trace( "matched test file (was specified on cmdline): {!r}".format(fn) ) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 9baee1d4e..967272ca6 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -464,8 +464,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: @pytest.hookimpl(tryfirst=True) def pytest_configure(config: Config) -> None: - # Type ignored: pending mechanism to store typed objects scoped to config. - config.cache = Cache.for_config(config) # type: ignore # noqa: F821 + config.cache = Cache.for_config(config) config.pluginmanager.register(LFPlugin(config), "lfplugin") config.pluginmanager.register(NFPlugin(config), "nfplugin") @@ -496,7 +495,7 @@ def pytest_report_header(config: Config) -> Optional[str]: # starting with .., ../.. if sensible try: - displaypath = cachedir.relative_to(config.rootdir) + displaypath = cachedir.relative_to(str(config.rootdir)) except ValueError: displaypath = cachedir return "cachedir: {}".format(displaypath) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 041041284..daded6395 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -519,11 +519,10 @@ class MultiCapture: def pop_outerr_to_orig(self): """ pop current snapshot out/err capture and flush to orig streams. """ out, err = self.readouterr() - # TODO: Fix type ignores. if out: - self.out.writeorg(out) # type: ignore[union-attr] # noqa: F821 + self.out.writeorg(out) if err: - self.err.writeorg(err) # type: ignore[union-attr] # noqa: F821 + self.err.writeorg(err) return out, err def suspend_capturing(self, in_: bool = False) -> None: @@ -543,8 +542,7 @@ class MultiCapture: if self.err: self.err.resume() if self._in_suspended: - # TODO: Fix type ignore. - self.in_.resume() # type: ignore[union-attr] # noqa: F821 + self.in_.resume() self._in_suspended = False def stop_capturing(self) -> None: @@ -751,11 +749,11 @@ class CaptureManager: yield @pytest.hookimpl(tryfirst=True) - def pytest_keyboard_interrupt(self, excinfo): + def pytest_keyboard_interrupt(self) -> None: self.stop_global_capturing() @pytest.hookimpl(tryfirst=True) - def pytest_internalerror(self, excinfo): + def pytest_internalerror(self) -> None: self.stop_global_capturing() diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 6e26bf15c..b77968110 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -48,6 +48,7 @@ from _pytest.warning_types import PytestConfigWarning if TYPE_CHECKING: from typing import Type + from _pytest._code.code import _TracebackStyle from .argparsing import Argument @@ -307,10 +308,9 @@ class PytestPluginManager(PluginManager): self._dirpath2confmods = {} # type: Dict[Any, List[object]] # Maps a py.path.local to a module object. self._conftestpath2mod = {} # type: Dict[Any, object] - self._confcutdir = None + self._confcutdir = None # type: Optional[py.path.local] self._noconftest = False - # Set of py.path.local's. - self._duplicatepaths = set() # type: Set[Any] + self._duplicatepaths = set() # type: Set[py.path.local] self.add_hookspecs(_pytest.hookspec) self.register(self) @@ -893,9 +893,13 @@ class Config: return self - def notify_exception(self, excinfo, option=None): + def notify_exception( + self, + excinfo: ExceptionInfo[BaseException], + option: Optional[argparse.Namespace] = None, + ) -> None: if option and getattr(option, "fulltrace", False): - style = "long" + style = "long" # type: _TracebackStyle else: style = "native" excrepr = excinfo.getrepr( @@ -940,13 +944,12 @@ class Config: ns, unknown_args = self._parser.parse_known_and_unknown_args( args, namespace=copy.copy(self.option) ) - r = determine_setup( + self.rootdir, self.inifile, self.inicfg = determine_setup( ns.inifilename, ns.file_or_dir + unknown_args, rootdir_cmd_arg=ns.rootdir or None, config=self, ) - self.rootdir, self.inifile, self.inicfg = r self._parser.extra_info["rootdir"] = self.rootdir self._parser.extra_info["inifile"] = self.inifile self._parser.addini("addopts", "extra command line options", "args") @@ -994,9 +997,7 @@ class Config: package_files = ( str(file) for dist in importlib_metadata.distributions() - # Type ignored due to missing stub: - # https://github.com/python/typeshed/pull/3795 - if any(ep.group == "pytest11" for ep in dist.entry_points) # type: ignore + if any(ep.group == "pytest11" for ep in dist.entry_points) for file in dist.files or [] ) @@ -1073,6 +1074,11 @@ class Config: # 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.inifile + ) + if Version(minver) > Version(pytest.__version__): raise pytest.UsageError( "%s: 'minversion' requires pytest-%s, actual pytest-%s'" @@ -1187,6 +1193,8 @@ class Config: # 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.inifile is not None dp = py.path.local(self.inifile).dirpath() input_values = shlex.split(value) if isinstance(value, str) else value return [dp.join(x, abs=True) for x in input_values] diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 796fa9b0a..ae8c5f47f 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -63,7 +63,7 @@ def load_config_dict_from_file( elif filepath.ext == ".toml": import toml - config = toml.load(filepath) + config = toml.load(str(filepath)) result = config.get("tool", {}).get("pytest", {}).get("ini_options", None) if result is not None: @@ -161,16 +161,18 @@ def determine_setup( args: List[str], rootdir_cmd_arg: Optional[str] = None, config: Optional["Config"] = None, -) -> Tuple[py.path.local, Optional[str], Dict[str, Union[str, List[str]]]]: +) -> Tuple[py.path.local, Optional[py.path.local], Dict[str, Union[str, List[str]]]]: rootdir = None dirs = get_dirs_from_args(args) if inifile: - inicfg = load_config_dict_from_file(py.path.local(inifile)) or {} + inipath_ = py.path.local(inifile) + inipath = inipath_ # type: Optional[py.path.local] + 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, inifile, inicfg = locate_config([ancestor]) + rootdir, inipath, inicfg = locate_config([ancestor]) if rootdir is None and rootdir_cmd_arg is None: for possible_rootdir in ancestor.parts(reverse=True): if possible_rootdir.join("setup.py").exists(): @@ -178,7 +180,7 @@ def determine_setup( break else: if dirs != [ancestor]: - rootdir, inifile, inicfg = locate_config(dirs) + rootdir, inipath, inicfg = locate_config(dirs) if rootdir is None: if config is not None: cwd = config.invocation_dir @@ -196,4 +198,5 @@ def determine_setup( rootdir ) ) - return rootdir, inifile, inicfg or {} + assert rootdir is not None + return rootdir, inipath, inicfg or {} diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 3001db4ec..0567927c0 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -2,11 +2,13 @@ import argparse import functools import sys +import types from typing import Generator from typing import Tuple from typing import Union from _pytest import outcomes +from _pytest._code import ExceptionInfo from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import ConftestImportFailure @@ -280,9 +282,10 @@ class PdbInvoke: out, err = capman.read_global_capture() sys.stdout.write(out) sys.stdout.write(err) + assert call.excinfo is not None _enter_pdb(node, call.excinfo, report) - def pytest_internalerror(self, excrepr, excinfo) -> None: + def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None: tb = _postmortem_traceback(excinfo) post_mortem(tb) @@ -320,7 +323,9 @@ def maybe_wrap_pytest_function_for_tracing(pyfuncitem): wrap_pytest_function_for_tracing(pyfuncitem) -def _enter_pdb(node: Node, excinfo, rep: BaseReport) -> BaseReport: +def _enter_pdb( + node: Node, excinfo: ExceptionInfo[BaseException], rep: BaseReport +) -> BaseReport: # XXX we re-use the TerminalReporter's terminalwriter # because this seems to avoid some encoding related troubles # for not completely clear reasons. @@ -349,7 +354,7 @@ def _enter_pdb(node: Node, excinfo, rep: BaseReport) -> BaseReport: return rep -def _postmortem_traceback(excinfo): +def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.TracebackType: from doctest import UnexpectedException if isinstance(excinfo.value, UnexpectedException): @@ -361,10 +366,11 @@ def _postmortem_traceback(excinfo): # Use the underlying exception instead: return excinfo.value.excinfo[2] else: + assert excinfo._excinfo is not None return excinfo._excinfo[2] -def post_mortem(t) -> None: +def post_mortem(t: types.TracebackType) -> None: p = pytestPDB._init_pdb("post_mortem") p.reset() p.interaction(None, t) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 18a9fb39a..1c1726d53 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -19,6 +19,8 @@ if TYPE_CHECKING: import warnings from typing_extensions import Literal + from _pytest._code.code import ExceptionRepr + from _pytest.code import ExceptionInfo from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import PytestPluginManager @@ -30,6 +32,7 @@ if TYPE_CHECKING: from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.nodes import Node + from _pytest.outcomes import Exit from _pytest.python import Function from _pytest.python import Metafunc from _pytest.python import Module @@ -757,11 +760,19 @@ def pytest_doctest_prepare_content(content): # ------------------------------------------------------------------------- -def pytest_internalerror(excrepr, excinfo): - """ called for internal errors. """ +def pytest_internalerror( + excrepr: "ExceptionRepr", excinfo: "ExceptionInfo[BaseException]", +) -> Optional[bool]: + """Called for internal errors. + + Return True to suppress the fallback handling of printing an + INTERNALERROR message directly to sys.stderr. + """ -def pytest_keyboard_interrupt(excinfo): +def pytest_keyboard_interrupt( + excinfo: "ExceptionInfo[Union[KeyboardInterrupt, Exit]]", +) -> None: """ called for keyboard interrupt. """ diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index e62bc5235..86e8fcf38 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -26,6 +26,7 @@ import pytest from _pytest import deprecated from _pytest import nodes from _pytest import timing +from _pytest._code.code import ExceptionRepr from _pytest.compat import TYPE_CHECKING from _pytest.config import Config from _pytest.config import filename_arg @@ -642,7 +643,7 @@ class LogXML: else: reporter.append_collect_skipped(report) - def pytest_internalerror(self, excrepr) -> None: + def pytest_internalerror(self, excrepr: ExceptionRepr) -> None: reporter = self.node_reporter("internal") reporter.attrs.update(classname="pytest", name="internal") reporter._add_simple(Junit.error, "internal error", excrepr) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index ef90c94e8..04bf74b6c 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -586,7 +586,7 @@ class LoggingPlugin: fpath = Path(fname) if not fpath.is_absolute(): - fpath = Path(self._config.rootdir, fpath) + fpath = Path(str(self._config.rootdir), fpath) if not fpath.parent.exists(): fpath.parent.mkdir(exist_ok=True, parents=True) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 6d008bcaa..2ec9046b0 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -439,7 +439,7 @@ class Session(nodes.FSCollector): ) # type: Dict[Tuple[Type[nodes.Collector], str], CollectReport] # Dirnames of pkgs with dunder-init files. - self._collection_pkg_roots = {} # type: Dict[py.path.local, Package] + self._collection_pkg_roots = {} # type: Dict[str, Package] self._bestrelpathcache = _bestrelpath_cache( config.rootdir @@ -601,7 +601,7 @@ class Session(nodes.FSCollector): col = self._collectfile(pkginit, handle_dupes=False) if col: if isinstance(col[0], Package): - self._collection_pkg_roots[parent] = col[0] + self._collection_pkg_roots[str(parent)] = col[0] # always store a list in the cache, matchnodes expects it self._collection_node_cache1[col[0].fspath] = [col[0]] @@ -623,8 +623,8 @@ class Session(nodes.FSCollector): for x in self._collectfile(pkginit): yield x if isinstance(x, Package): - self._collection_pkg_roots[dirpath] = x - if dirpath in self._collection_pkg_roots: + self._collection_pkg_roots[str(dirpath)] = x + if str(dirpath) in self._collection_pkg_roots: # Do not collect packages here. continue diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 7abff9b7b..3d512816c 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -66,9 +66,7 @@ def get_empty_parameterset_mark( fs, lineno, ) - # Type ignored because MarkDecorator.__call__() is a bit tough to - # annotate ATM. - return mark(reason=reason) # type: ignore[no-any-return] # noqa: F723 + return mark(reason=reason) class ParameterSet( diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 9d802a625..09f1ac36e 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -4,17 +4,29 @@ import re import sys import warnings from contextlib import contextmanager +from typing import Any from typing import Generator +from typing import List +from typing import MutableMapping +from typing import Optional +from typing import Tuple +from typing import TypeVar +from typing import Union import pytest +from _pytest.compat import overload from _pytest.fixtures import fixture from _pytest.pathlib import Path RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$") +K = TypeVar("K") +V = TypeVar("V") + + @fixture -def monkeypatch(): +def monkeypatch() -> Generator["MonkeyPatch", None, None]: """The returned ``monkeypatch`` fixture provides these helper methods to modify objects, dictionaries or os.environ:: @@ -37,7 +49,7 @@ def monkeypatch(): mpatch.undo() -def resolve(name): +def resolve(name: str) -> object: # simplified from zope.dottedname parts = name.split(".") @@ -66,7 +78,7 @@ def resolve(name): return found -def annotated_getattr(obj, name, ann): +def annotated_getattr(obj: object, name: str, ann: str) -> object: try: obj = getattr(obj, name) except AttributeError: @@ -78,7 +90,7 @@ def annotated_getattr(obj, name, ann): return obj -def derive_importpath(import_path, raising): +def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]: if not isinstance(import_path, str) or "." not in import_path: raise TypeError( "must be absolute import path string, not {!r}".format(import_path) @@ -91,7 +103,7 @@ def derive_importpath(import_path, raising): class Notset: - def __repr__(self): + def __repr__(self) -> str: return "" @@ -102,11 +114,13 @@ class MonkeyPatch: """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes. """ - def __init__(self): - self._setattr = [] - self._setitem = [] - self._cwd = None - self._savesyspath = None + def __init__(self) -> None: + self._setattr = [] # type: List[Tuple[object, str, object]] + self._setitem = ( + [] + ) # type: List[Tuple[MutableMapping[Any, Any], object, object]] + self._cwd = None # type: Optional[str] + self._savesyspath = None # type: Optional[List[str]] @contextmanager def context(self) -> Generator["MonkeyPatch", None, None]: @@ -133,7 +147,25 @@ class MonkeyPatch: finally: m.undo() - def setattr(self, target, name, value=notset, raising=True): + @overload + def setattr( + self, target: str, name: object, value: Notset = ..., raising: bool = ..., + ) -> None: + raise NotImplementedError() + + @overload # noqa: F811 + def setattr( # noqa: F811 + self, target: object, name: str, value: object, raising: bool = ..., + ) -> None: + raise NotImplementedError() + + def setattr( # noqa: F811 + self, + target: Union[str, object], + name: Union[object, str], + value: object = notset, + raising: bool = True, + ) -> None: """ Set attribute value on target, memorizing the old value. By default raise AttributeError if the attribute did not exist. @@ -150,7 +182,7 @@ class MonkeyPatch: __tracebackhide__ = True import inspect - if value is notset: + if isinstance(value, Notset): if not isinstance(target, str): raise TypeError( "use setattr(target, name, value) or " @@ -159,6 +191,13 @@ class MonkeyPatch: ) value = name name, target = derive_importpath(target, raising) + else: + if not isinstance(name, str): + raise TypeError( + "use setattr(target, name, value) with name being a string or " + "setattr(target, value) with target being a dotted " + "import string" + ) oldval = getattr(target, name, notset) if raising and oldval is notset: @@ -170,7 +209,12 @@ class MonkeyPatch: self._setattr.append((target, name, oldval)) setattr(target, name, value) - def delattr(self, target, name=notset, raising=True): + def delattr( + self, + target: Union[object, str], + name: Union[str, Notset] = notset, + raising: bool = True, + ) -> None: """ Delete attribute ``name`` from ``target``, by default raise AttributeError it the attribute did not previously exist. @@ -184,7 +228,7 @@ class MonkeyPatch: __tracebackhide__ = True import inspect - if name is notset: + if isinstance(name, Notset): if not isinstance(target, str): raise TypeError( "use delattr(target, name) or " @@ -204,12 +248,12 @@ class MonkeyPatch: self._setattr.append((target, name, oldval)) delattr(target, name) - def setitem(self, dic, name, value): + def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None: """ Set dictionary entry ``name`` to value. """ self._setitem.append((dic, name, dic.get(name, notset))) dic[name] = value - def delitem(self, dic, name, raising=True): + def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None: """ Delete ``name`` from dict. Raise KeyError if it doesn't exist. If ``raising`` is set to False, no exception will be raised if the @@ -222,7 +266,7 @@ class MonkeyPatch: self._setitem.append((dic, name, dic.get(name, notset))) del dic[name] - def setenv(self, name, value, prepend=None): + def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None: """ Set environment variable ``name`` to ``value``. If ``prepend`` is a character, read the current environment variable value and prepend the ``value`` adjoined with the ``prepend`` character.""" @@ -241,16 +285,17 @@ class MonkeyPatch: value = value + prepend + os.environ[name] self.setitem(os.environ, name, value) - def delenv(self, name, raising=True): + def delenv(self, name: str, raising: bool = True) -> None: """ Delete ``name`` from the environment. Raise KeyError if it does not exist. If ``raising`` is set to False, no exception will be raised if the environment variable is missing. """ - self.delitem(os.environ, name, raising=raising) + environ = os.environ # type: MutableMapping[str, str] + self.delitem(environ, name, raising=raising) - def syspath_prepend(self, path): + def syspath_prepend(self, path) -> None: """ Prepend ``path`` to ``sys.path`` list of import locations. """ from pkg_resources import fixup_namespace_packages @@ -272,7 +317,7 @@ class MonkeyPatch: invalidate_caches() - def chdir(self, path): + def chdir(self, path) -> None: """ Change the current working directory to the specified path. Path can be a string or a py.path.local object. """ @@ -286,7 +331,7 @@ class MonkeyPatch: else: os.chdir(path) - def undo(self): + def undo(self) -> None: """ Undo previous changes. This call consumes the undo stack. Calling it a second time has no effect unless you do more monkeypatching after the undo call. @@ -306,14 +351,14 @@ class MonkeyPatch: else: delattr(obj, name) self._setattr[:] = [] - for dictionary, name, value in reversed(self._setitem): + for dictionary, key, value in reversed(self._setitem): if value is notset: try: - del dictionary[name] + del dictionary[key] except KeyError: pass # was already deleted, so we have the desired state else: - dictionary[name] = value + dictionary[key] = value self._setitem[:] = [] if self._savesyspath is not None: sys.path[:] = self._savesyspath diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 3757e0b27..c6c77f529 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -393,7 +393,7 @@ class Node(metaclass=NodeMeta): # It will be better to just always display paths relative to invocation_dir, but # this requires a lot of plumbing (#6428). try: - abspath = Path(os.getcwd()) != Path(self.config.invocation_dir) + abspath = Path(os.getcwd()) != Path(str(self.config.invocation_dir)) except OSError: abspath = True diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 4b716c616..c52771057 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -656,7 +656,7 @@ class Package(Module): parts_ = parts(path.strpath) if any( - pkg_prefix in parts_ and pkg_prefix.join("__init__.py") != path + str(pkg_prefix) in parts_ and pkg_prefix.join("__init__.py") != path for pkg_prefix in pkg_prefixes ): continue @@ -1332,7 +1332,7 @@ def _show_fixtures_per_test(config, session): def get_best_relpath(func): loc = getlocation(func, curdir) - return curdir.bestrelpath(loc) + return curdir.bestrelpath(py.path.local(loc)) def write_fixture(fixture_def): argname = fixture_def.argname @@ -1406,7 +1406,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: ( len(fixturedef.baseid), fixturedef.func.__module__, - curdir.bestrelpath(loc), + curdir.bestrelpath(py.path.local(loc)), fixturedef.argname, fixturedef, ) diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index c870ef08e..cd6824abf 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -5,6 +5,7 @@ import os import py +from _pytest._code.code import ExceptionRepr from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.reports import CollectReport @@ -99,9 +100,9 @@ class ResultLog: longrepr = "%s:%d: %s" % report.longrepr # type: ignore self.log_outcome(report, code, longrepr) - def pytest_internalerror(self, excrepr): - reprcrash = getattr(excrepr, "reprcrash", None) - path = getattr(reprcrash, "path", None) - if path is None: + def pytest_internalerror(self, excrepr: ExceptionRepr) -> None: + if excrepr.reprcrash is not None: + path = excrepr.reprcrash.path + else: path = "cwd:%s" % py.path.local() self.write_log_entry(path, "!", str(excrepr)) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 9c2665fb8..8c2a30739 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -30,6 +30,8 @@ from more_itertools import collapse import pytest from _pytest import nodes from _pytest import timing +from _pytest._code import ExceptionInfo +from _pytest._code.code import ExceptionRepr from _pytest._io import TerminalWriter from _pytest._io.wcwidth import wcswidth from _pytest.compat import order_preserving_dict @@ -315,6 +317,7 @@ class TerminalReporter: self._show_progress_info = self._determine_show_progress_info() self._collect_report_last_write = None # type: Optional[float] self._already_displayed_warnings = None # type: Optional[int] + self._keyboardinterrupt_memo = None # type: Optional[ExceptionRepr] @property def writer(self) -> TerminalWriter: @@ -377,9 +380,9 @@ class TerminalReporter: if self.currentfspath is not None and self._show_progress_info: self._write_progress_information_filling_space() self.currentfspath = fspath - fspath = self.startdir.bestrelpath(fspath) + relfspath = self.startdir.bestrelpath(fspath) self._tw.line() - self._tw.write(fspath + " ") + self._tw.write(relfspath + " ") self._tw.write(res, flush=True, **markup) def write_ensure_prefix(self, prefix, extra: str = "", **kwargs) -> None: @@ -448,10 +451,10 @@ class TerminalReporter: if set_main_color: self._set_main_color() - def pytest_internalerror(self, excrepr): + def pytest_internalerror(self, excrepr: ExceptionRepr) -> bool: for line in str(excrepr).split("\n"): self.write_line("INTERNALERROR> " + line) - return 1 + return True def pytest_warning_recorded( self, warning_message: warnings.WarningMessage, nodeid: str, @@ -783,7 +786,7 @@ class TerminalReporter: self.write_sep("!", str(session.shouldfail), red=True) if exitstatus == ExitCode.INTERRUPTED: self._report_keyboardinterrupt() - del self._keyboardinterrupt_memo + self._keyboardinterrupt_memo = None elif session.shouldstop: self.write_sep("!", str(session.shouldstop), red=True) self.summary_stats() @@ -799,15 +802,17 @@ class TerminalReporter: # Display any extra warnings from teardown here (if any). self.summary_warnings() - def pytest_keyboard_interrupt(self, excinfo) -> None: + def pytest_keyboard_interrupt(self, excinfo: ExceptionInfo[BaseException]) -> None: self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) def pytest_unconfigure(self) -> None: - if hasattr(self, "_keyboardinterrupt_memo"): + if self._keyboardinterrupt_memo is not None: self._report_keyboardinterrupt() def _report_keyboardinterrupt(self) -> None: excrepr = self._keyboardinterrupt_memo + assert excrepr is not None + assert excrepr.reprcrash is not None msg = excrepr.reprcrash.message self.write_sep("!", msg) if "KeyboardInterrupt" in msg: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 7dfd588a0..686fe1b98 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -580,8 +580,9 @@ class TestInvocationVariants: assert res.ret == 0 res.stdout.fnmatch_lines(["*1 passed*"]) - def test_equivalence_pytest_pytest(self): - assert pytest.main == py.test.cmdline.main + def test_equivalence_pytest_pydottest(self) -> None: + # Type ignored because `py.test` is not and will not be typed. + assert pytest.main == py.test.cmdline.main # type: ignore[attr-defined] def test_invoke_with_invalid_type(self): with pytest.raises( diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 3813993be..38893deb9 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1258,7 +1258,7 @@ class TestEarlyRewriteBailout: def spy_find_spec(name, path): self.find_spec_calls.append(name) - return importlib.machinery.PathFinder.find_spec(name, path) # type: ignore + return importlib.machinery.PathFinder.find_spec(name, path) hook = AssertionRewritingHook(pytestconfig) # use default patterns, otherwise we inherit pytest's testing config diff --git a/testing/test_config.py b/testing/test_config.py index a10ac41dd..c77ab1a48 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -683,13 +683,14 @@ class TestConfigFromdictargs: ) with cwd.ensure(dir=True).as_cwd(): config = Config.fromdictargs(option_dict, ()) + inipath = py.path.local(inifile) assert config.args == [str(cwd)] assert config.option.inifilename == inifile assert config.option.capture == "no" # this indicates this is the file used for getting configuration values - assert config.inifile == inifile + assert config.inifile == inipath assert config.inicfg.get("name") == "value" assert config.inicfg.get("should_not_be_set") is None diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 1a3afbea9..509e72599 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -5,9 +5,12 @@ import textwrap from typing import Dict from typing import Generator +import py + import pytest from _pytest.compat import TYPE_CHECKING from _pytest.monkeypatch import MonkeyPatch +from _pytest.pytester import Testdir if TYPE_CHECKING: from typing import Type @@ -45,9 +48,12 @@ def test_setattr() -> None: monkeypatch.undo() # double-undo makes no modification assert A.x == 5 + with pytest.raises(TypeError): + monkeypatch.setattr(A, "y") # type: ignore[call-overload] + class TestSetattrWithImportPath: - def test_string_expression(self, monkeypatch): + def test_string_expression(self, monkeypatch: MonkeyPatch) -> None: monkeypatch.setattr("os.path.abspath", lambda x: "hello2") assert os.path.abspath("123") == "hello2" @@ -64,30 +70,31 @@ class TestSetattrWithImportPath: assert _pytest.config.Config == 42 # type: ignore monkeypatch.delattr("_pytest.config.Config") - def test_wrong_target(self, monkeypatch): - pytest.raises(TypeError, lambda: monkeypatch.setattr(None, None)) + def test_wrong_target(self, monkeypatch: MonkeyPatch) -> None: + with pytest.raises(TypeError): + monkeypatch.setattr(None, None) # type: ignore[call-overload] - def test_unknown_import(self, monkeypatch): - pytest.raises(ImportError, lambda: monkeypatch.setattr("unkn123.classx", None)) + def test_unknown_import(self, monkeypatch: MonkeyPatch) -> None: + with pytest.raises(ImportError): + monkeypatch.setattr("unkn123.classx", None) - def test_unknown_attr(self, monkeypatch): - pytest.raises( - AttributeError, lambda: monkeypatch.setattr("os.path.qweqwe", None) - ) + def test_unknown_attr(self, monkeypatch: MonkeyPatch) -> None: + with pytest.raises(AttributeError): + monkeypatch.setattr("os.path.qweqwe", None) def test_unknown_attr_non_raising(self, monkeypatch: MonkeyPatch) -> None: # https://github.com/pytest-dev/pytest/issues/746 monkeypatch.setattr("os.path.qweqwe", 42, raising=False) assert os.path.qweqwe == 42 # type: ignore - def test_delattr(self, monkeypatch): + def test_delattr(self, monkeypatch: MonkeyPatch) -> None: monkeypatch.delattr("os.path.abspath") assert not hasattr(os.path, "abspath") monkeypatch.undo() assert os.path.abspath -def test_delattr(): +def test_delattr() -> None: class A: x = 1 @@ -107,7 +114,7 @@ def test_delattr(): assert A.x == 1 -def test_setitem(): +def test_setitem() -> None: d = {"x": 1} monkeypatch = MonkeyPatch() monkeypatch.setitem(d, "x", 2) @@ -135,7 +142,7 @@ def test_setitem_deleted_meanwhile() -> None: @pytest.mark.parametrize("before", [True, False]) -def test_setenv_deleted_meanwhile(before): +def test_setenv_deleted_meanwhile(before: bool) -> None: key = "qwpeoip123" if before: os.environ[key] = "world" @@ -167,10 +174,10 @@ def test_delitem() -> None: assert d == {"hello": "world", "x": 1} -def test_setenv(): +def test_setenv() -> None: monkeypatch = MonkeyPatch() with pytest.warns(pytest.PytestWarning): - monkeypatch.setenv("XYZ123", 2) + monkeypatch.setenv("XYZ123", 2) # type: ignore[arg-type] import os assert os.environ["XYZ123"] == "2" @@ -178,7 +185,7 @@ def test_setenv(): assert "XYZ123" not in os.environ -def test_delenv(): +def test_delenv() -> None: name = "xyz1234" assert name not in os.environ monkeypatch = MonkeyPatch() @@ -208,31 +215,28 @@ class TestEnvironWarnings: VAR_NAME = "PYTEST_INTERNAL_MY_VAR" - def test_setenv_non_str_warning(self, monkeypatch): + def test_setenv_non_str_warning(self, monkeypatch: MonkeyPatch) -> None: value = 2 msg = ( "Value of environment variable PYTEST_INTERNAL_MY_VAR type should be str, " "but got 2 (type: int); converted to str implicitly" ) with pytest.warns(pytest.PytestWarning, match=re.escape(msg)): - monkeypatch.setenv(str(self.VAR_NAME), value) + monkeypatch.setenv(str(self.VAR_NAME), value) # type: ignore[arg-type] -def test_setenv_prepend(): +def test_setenv_prepend() -> None: import os monkeypatch = MonkeyPatch() - with pytest.warns(pytest.PytestWarning): - monkeypatch.setenv("XYZ123", 2, prepend="-") - assert os.environ["XYZ123"] == "2" - with pytest.warns(pytest.PytestWarning): - monkeypatch.setenv("XYZ123", 3, prepend="-") + monkeypatch.setenv("XYZ123", "2", prepend="-") + monkeypatch.setenv("XYZ123", "3", prepend="-") assert os.environ["XYZ123"] == "3-2" monkeypatch.undo() assert "XYZ123" not in os.environ -def test_monkeypatch_plugin(testdir): +def test_monkeypatch_plugin(testdir: Testdir) -> None: reprec = testdir.inline_runsource( """ def test_method(monkeypatch): @@ -243,7 +247,7 @@ def test_monkeypatch_plugin(testdir): assert tuple(res) == (1, 0, 0), res -def test_syspath_prepend(mp: MonkeyPatch): +def test_syspath_prepend(mp: MonkeyPatch) -> None: old = list(sys.path) mp.syspath_prepend("world") mp.syspath_prepend("hello") @@ -255,7 +259,7 @@ def test_syspath_prepend(mp: MonkeyPatch): assert sys.path == old -def test_syspath_prepend_double_undo(mp: MonkeyPatch): +def test_syspath_prepend_double_undo(mp: MonkeyPatch) -> None: old_syspath = sys.path[:] try: mp.syspath_prepend("hello world") @@ -267,24 +271,24 @@ def test_syspath_prepend_double_undo(mp: MonkeyPatch): sys.path[:] = old_syspath -def test_chdir_with_path_local(mp: MonkeyPatch, tmpdir): +def test_chdir_with_path_local(mp: MonkeyPatch, tmpdir: py.path.local) -> None: mp.chdir(tmpdir) assert os.getcwd() == tmpdir.strpath -def test_chdir_with_str(mp: MonkeyPatch, tmpdir): +def test_chdir_with_str(mp: MonkeyPatch, tmpdir: py.path.local) -> None: mp.chdir(tmpdir.strpath) assert os.getcwd() == tmpdir.strpath -def test_chdir_undo(mp: MonkeyPatch, tmpdir): +def test_chdir_undo(mp: MonkeyPatch, tmpdir: py.path.local) -> None: cwd = os.getcwd() mp.chdir(tmpdir) mp.undo() assert os.getcwd() == cwd -def test_chdir_double_undo(mp: MonkeyPatch, tmpdir): +def test_chdir_double_undo(mp: MonkeyPatch, tmpdir: py.path.local) -> None: mp.chdir(tmpdir.strpath) mp.undo() tmpdir.chdir() @@ -292,7 +296,7 @@ def test_chdir_double_undo(mp: MonkeyPatch, tmpdir): assert os.getcwd() == tmpdir.strpath -def test_issue185_time_breaks(testdir): +def test_issue185_time_breaks(testdir: Testdir) -> None: testdir.makepyfile( """ import time @@ -310,7 +314,7 @@ def test_issue185_time_breaks(testdir): ) -def test_importerror(testdir): +def test_importerror(testdir: Testdir) -> None: p = testdir.mkpydir("package") p.join("a.py").write( textwrap.dedent( @@ -360,7 +364,7 @@ def test_issue156_undo_staticmethod(Sample: "Type[Sample]") -> None: assert Sample.hello() -def test_undo_class_descriptors_delattr(): +def test_undo_class_descriptors_delattr() -> None: class SampleParent: @classmethod def hello(_cls): @@ -387,7 +391,7 @@ def test_undo_class_descriptors_delattr(): assert original_world == SampleChild.world -def test_issue1338_name_resolving(): +def test_issue1338_name_resolving() -> None: pytest.importorskip("requests") monkeypatch = MonkeyPatch() try: @@ -396,7 +400,7 @@ def test_issue1338_name_resolving(): monkeypatch.undo() -def test_context(): +def test_context() -> None: monkeypatch = MonkeyPatch() import functools @@ -408,7 +412,9 @@ def test_context(): assert inspect.isclass(functools.partial) -def test_syspath_prepend_with_namespace_packages(testdir, monkeypatch): +def test_syspath_prepend_with_namespace_packages( + testdir: Testdir, monkeypatch: MonkeyPatch +) -> None: for dirname in "hello", "world": d = testdir.mkdir(dirname) ns = d.mkdir("ns_pkg")