Skip to content

Commit

Permalink
Merge pull request pytest-dev#7358 from bluetech/typing2
Browse files Browse the repository at this point in the history
More type annotations, fix some typing bugs
  • Loading branch information
bluetech committed Jun 13, 2020
2 parents aaa6f1c + a5ab7c1 commit f551cab
Show file tree
Hide file tree
Showing 21 changed files with 216 additions and 126 deletions.
5 changes: 5 additions & 0 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,8 +928,13 @@ def toterminal(self, tw: TerminalWriter) -> None:
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]]

Expand Down
8 changes: 5 additions & 3 deletions src/_pytest/assertion/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
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
Expand Down Expand Up @@ -177,10 +179,10 @@ def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool:
"""
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])

Expand Down Expand Up @@ -213,7 +215,7 @@ def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool:
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)
)
Expand Down
5 changes: 2 additions & 3 deletions src/_pytest/cacheprovider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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)
Expand Down
12 changes: 5 additions & 7 deletions src/_pytest/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,11 +519,10 @@ def start_capturing(self) -> None:
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:
Expand All @@ -543,8 +542,7 @@ def resume_capturing(self) -> None:
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:
Expand Down Expand Up @@ -751,11 +749,11 @@ def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:
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()


Expand Down
28 changes: 18 additions & 10 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
if TYPE_CHECKING:
from typing import Type

from _pytest._code.code import _TracebackStyle
from .argparsing import Argument


Expand Down Expand Up @@ -307,10 +308,9 @@ def __init__(self) -> None:
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)
Expand Down Expand Up @@ -893,9 +893,13 @@ def pytest_cmdline_parse(

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(
Expand Down Expand Up @@ -940,13 +944,12 @@ def _initini(self, args: Sequence[str]) -> None:
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")
Expand Down Expand Up @@ -994,9 +997,7 @@ def _mark_plugins_for_rewrite(self, hook) -> None:
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 []
)

Expand Down Expand Up @@ -1073,6 +1074,11 @@ def _checkversion(self):
# 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'"
Expand Down Expand Up @@ -1187,6 +1193,8 @@ def _getini(self, name: str) -> Any:
# 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]
Expand Down
15 changes: 9 additions & 6 deletions src/_pytest/config/findpaths.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -161,24 +161,26 @@ 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():
rootdir = possible_rootdir
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
Expand All @@ -196,4 +198,5 @@ def determine_setup(
rootdir
)
)
return rootdir, inifile, inicfg or {}
assert rootdir is not None
return rootdir, inipath, inicfg or {}
14 changes: 10 additions & 4 deletions src/_pytest/debugging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -280,9 +282,10 @@ def pytest_exception_interact(
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)

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand Down
17 changes: 14 additions & 3 deletions src/_pytest/hookspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
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
Expand All @@ -30,6 +32,7 @@
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
Expand Down Expand Up @@ -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. """


Expand Down
3 changes: 2 additions & 1 deletion src/_pytest/junitxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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
Expand Down Expand Up @@ -642,7 +643,7 @@ def pytest_collectreport(self, report: TestReport) -> None:
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)
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ def set_log_path(self, fname: str) -> None:
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)
Expand Down
8 changes: 4 additions & 4 deletions src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ def __init__(self, config: Config) -> None:
) # 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
Expand Down Expand Up @@ -601,7 +601,7 @@ def _collect(
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]]

Expand All @@ -623,8 +623,8 @@ def _collect(
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

Expand Down
4 changes: 1 addition & 3 deletions src/_pytest/mark/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit f551cab

Please sign in to comment.