Add rudimentary mypy type checking

Add a very lax mypy configuration, add it to tox -e linting, and
fix/ignore the few errors that come up. The idea is to get it running
before diving in too much.

This enables:

- Progressively adding type annotations and enabling more strict
  options, which will improve the codebase (IMO).

- Annotating the public API in-line, and eventually exposing it to
  library users who use type checkers (with a py.typed file).

Though, none of this is done yet.

Refs https://github.com/pytest-dev/pytest/issues/3342.
This commit is contained in:
Ran Benita 2019-07-08 10:04:19 +03:00
parent 60a358fa2d
commit c1167ac552
30 changed files with 104 additions and 45 deletions

1
.gitignore vendored
View File

@ -35,6 +35,7 @@ env/
.tox
.cache
.pytest_cache
.mypy_cache
.coverage
.coverage.*
coverage.xml

View File

@ -28,6 +28,7 @@ repos:
hooks:
- id: flake8
language_version: python3
additional_dependencies: [flake8-typing-imports]
- repo: https://github.com/asottile/reorder_python_imports
rev: v1.4.0
hooks:
@ -42,6 +43,17 @@ repos:
rev: v1.4.0
hooks:
- id: rst-backticks
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.711
hooks:
- id: mypy
name: mypy (src)
files: ^src/
args: []
- id: mypy
name: mypy (testing)
files: ^testing/
args: []
- repo: local
hooks:
- id: rst

View File

@ -6,7 +6,7 @@ if __name__ == "__main__":
import pstats
script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"]
stats = cProfile.run("pytest.cmdline.main(%r)" % script, "prof")
cProfile.run("pytest.cmdline.main(%r)" % script, "prof")
p = pstats.Stats("prof")
p.strip_dirs()
p.sort_stats("cumulative")

View File

@ -61,3 +61,11 @@ ignore =
[devpi:upload]
formats = sdist.tgz,bdist_wheel
[mypy]
ignore_missing_imports = True
no_implicit_optional = True
strict_equality = True
warn_redundant_casts = True
warn_return_any = True
warn_unused_configs = True

View File

@ -56,6 +56,7 @@ If things do not work right away:
import os
import sys
from glob import glob
from typing import Optional
class FastFilesCompleter:
@ -91,7 +92,7 @@ if os.environ.get("_ARGCOMPLETE"):
import argcomplete.completers
except ImportError:
sys.exit(-1)
filescompleter = FastFilesCompleter()
filescompleter = FastFilesCompleter() # type: Optional[FastFilesCompleter]
def try_argcomplete(parser):
argcomplete.autocomplete(parser, always_complete_options=False)

View File

@ -33,7 +33,8 @@ class Code:
def __eq__(self, other):
return self.raw == other.raw
__hash__ = None
# Ignore type because of https://github.com/python/mypy/issues/4266.
__hash__ = None # type: ignore
def __ne__(self, other):
return not self == other
@ -188,11 +189,11 @@ class TracebackEntry:
""" path to the source code """
return self.frame.code.path
def getlocals(self):
@property
def locals(self):
""" locals of underlaying frame """
return self.frame.f_locals
locals = property(getlocals, None, None, "locals of underlaying frame")
def getfirstlinesource(self):
return self.frame.code.firstlineno
@ -255,11 +256,11 @@ class TracebackEntry:
line = "???"
return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line)
@property
def name(self):
""" co_name of underlaying code """
return self.frame.code.raw.co_name
name = property(name, None, None, "co_name of underlaying code")
class Traceback(list):
""" Traceback objects encapsulate and offer higher level

View File

@ -44,7 +44,8 @@ class Source:
return str(self) == other
return False
__hash__ = None
# Ignore type because of https://github.com/python/mypy/issues/4266.
__hash__ = None # type: ignore
def __getitem__(self, key):
if isinstance(key, int):

View File

@ -12,6 +12,10 @@ import struct
import sys
import tokenize
import types
from typing import Dict
from typing import List
from typing import Optional
from typing import Set
import atomicwrites
@ -459,17 +463,18 @@ def set_location(node, lineno, col_offset):
return node
def _get_assertion_exprs(src: bytes): # -> Dict[int, str]
def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
"""Returns a mapping from {lineno: "assertion test expression"}"""
ret = {}
ret = {} # type: Dict[int, str]
depth = 0
lines = []
assert_lineno = None
seen_lines = set()
lines = [] # type: List[str]
assert_lineno = None # type: Optional[int]
seen_lines = set() # type: Set[int]
def _write_and_reset() -> None:
nonlocal depth, lines, assert_lineno, seen_lines
assert assert_lineno is not None
ret[assert_lineno] = "".join(lines).rstrip().rstrip("\\")
depth = 0
lines = []
@ -477,21 +482,21 @@ def _get_assertion_exprs(src: bytes): # -> Dict[int, str]
seen_lines = set()
tokens = tokenize.tokenize(io.BytesIO(src).readline)
for tp, src, (lineno, offset), _, line in tokens:
if tp == tokenize.NAME and src == "assert":
for tp, source, (lineno, offset), _, line in tokens:
if tp == tokenize.NAME and source == "assert":
assert_lineno = lineno
elif assert_lineno is not None:
# keep track of depth for the assert-message `,` lookup
if tp == tokenize.OP and src in "([{":
if tp == tokenize.OP and source in "([{":
depth += 1
elif tp == tokenize.OP and src in ")]}":
elif tp == tokenize.OP and source in ")]}":
depth -= 1
if not lines:
lines.append(line[offset:])
seen_lines.add(lineno)
# a non-nested comma separates the expression from the message
elif depth == 0 and tp == tokenize.OP and src == ",":
elif depth == 0 and tp == tokenize.OP and source == ",":
# one line assert with message
if lineno in seen_lines and len(lines) == 1:
offset_in_trimmed = offset + len(lines[-1]) - len(line)

View File

@ -547,6 +547,8 @@ class FDCaptureBinary:
self.start = lambda: None
self.done = lambda: None
else:
self.start = self._start
self.done = self._done
if targetfd == 0:
assert not tmpfile, "cannot set tmpfile with stdin"
tmpfile = open(os.devnull, "r")
@ -568,7 +570,7 @@ class FDCaptureBinary:
self.targetfd, getattr(self, "targetfd_save", None), self._state
)
def start(self):
def _start(self):
""" Start capturing on targetfd using memorized tmpfile. """
try:
os.fstat(self.targetfd_save)
@ -585,7 +587,7 @@ class FDCaptureBinary:
self.tmpfile.truncate()
return res
def done(self):
def _done(self):
""" stop capturing, restore streams, return original capture file,
seeked to position zero. """
targetfd_save = self.__dict__.pop("targetfd_save")
@ -618,7 +620,8 @@ class FDCapture(FDCaptureBinary):
snap() produces text
"""
EMPTY_BUFFER = str()
# Ignore type because it doesn't match the type in the superclass (bytes).
EMPTY_BUFFER = str() # type: ignore
def snap(self):
res = super().snap()
@ -679,7 +682,8 @@ class SysCapture:
class SysCaptureBinary(SysCapture):
EMPTY_BUFFER = b""
# Ignore type because it doesn't match the type in the superclass (str).
EMPTY_BUFFER = b"" # type: ignore
def snap(self):
res = self.tmpfile.buffer.getvalue()

View File

@ -74,7 +74,7 @@ class pytestPDB:
_pluginmanager = None
_config = None
_saved = []
_saved = [] # type: list
_recursive_debug = 0
_wrapped_pdb_cls = None

View File

@ -6,6 +6,8 @@ import warnings
from collections import defaultdict
from collections import deque
from collections import OrderedDict
from typing import Dict
from typing import Tuple
import attr
import py
@ -31,6 +33,9 @@ from _pytest.deprecated import FIXTURE_NAMED_REQUEST
from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME
if False: # TYPE_CHECKING
from typing import Type
@attr.s(frozen=True)
class PseudoFixtureDef:
@ -54,10 +59,10 @@ def pytest_sessionstart(session):
session._fixturemanager = FixtureManager(session)
scopename2class = {}
scopename2class = {} # type: Dict[str, Type[nodes.Node]]
scope2props = dict(session=())
scope2props = dict(session=()) # type: Dict[str, Tuple[str, ...]]
scope2props["package"] = ("fspath",)
scope2props["module"] = ("fspath", "module")
scope2props["class"] = scope2props["module"] + ("cls",)
@ -960,7 +965,8 @@ class FixtureFunctionMarker:
scope = attr.ib()
params = attr.ib(converter=attr.converters.optional(tuple))
autouse = attr.ib(default=False)
ids = attr.ib(default=None, converter=_ensure_immutable_ids)
# Ignore type because of https://github.com/python/mypy/issues/6172.
ids = attr.ib(default=None, converter=_ensure_immutable_ids) # type: ignore
name = attr.ib(default=None)
def __call__(self, function):

View File

@ -91,7 +91,8 @@ def pytest_cmdline_main(config):
return 0
pytest_cmdline_main.tryfirst = True
# Ignore type because of https://github.com/python/mypy/issues/2087.
pytest_cmdline_main.tryfirst = True # type: ignore
def deselect_by_keyword(items, config):

View File

@ -3,6 +3,7 @@ import warnings
from collections import namedtuple
from collections.abc import MutableMapping
from operator import attrgetter
from typing import Set
import attr
@ -298,7 +299,7 @@ class MarkGenerator:
on the ``test_function`` object. """
_config = None
_markers = set()
_markers = set() # type: Set[str]
def __getattr__(self, name):
if name[0] == "_":

View File

@ -280,7 +280,8 @@ class Node:
truncate_locals=truncate_locals,
)
repr_failure = _repr_failure_py
def repr_failure(self, excinfo, style=None):
return self._repr_failure_py(excinfo, style)
def get_fslocation_from_item(item):

View File

@ -70,7 +70,8 @@ def exit(msg, returncode=None):
raise Exit(msg, returncode)
exit.Exception = Exit
# Ignore type because of https://github.com/python/mypy/issues/2087.
exit.Exception = Exit # type: ignore
def skip(msg="", *, allow_module_level=False):
@ -96,7 +97,8 @@ def skip(msg="", *, allow_module_level=False):
raise Skipped(msg=msg, allow_module_level=allow_module_level)
skip.Exception = Skipped
# Ignore type because of https://github.com/python/mypy/issues/2087.
skip.Exception = Skipped # type: ignore
def fail(msg="", pytrace=True):
@ -111,7 +113,8 @@ def fail(msg="", pytrace=True):
raise Failed(msg=msg, pytrace=pytrace)
fail.Exception = Failed
# Ignore type because of https://github.com/python/mypy/issues/2087.
fail.Exception = Failed # type: ignore
class XFailed(Failed):
@ -132,7 +135,8 @@ def xfail(reason=""):
raise XFailed(reason)
xfail.Exception = XFailed
# Ignore type because of https://github.com/python/mypy/issues/2087.
xfail.Exception = XFailed # type: ignore
def importorskip(modname, minversion=None, reason=None):

View File

@ -9,6 +9,7 @@ from collections.abc import Sized
from decimal import Decimal
from itertools import filterfalse
from numbers import Number
from typing import Union
from more_itertools.more import always_iterable
@ -58,7 +59,8 @@ class ApproxBase:
a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual)
)
__hash__ = None
# Ignore type because of https://github.com/python/mypy/issues/4266.
__hash__ = None # type: ignore
def __ne__(self, actual):
return not (actual == self)
@ -202,8 +204,10 @@ class ApproxScalar(ApproxBase):
Perform approximate comparisons where the expected value is a single number.
"""
DEFAULT_ABSOLUTE_TOLERANCE = 1e-12
DEFAULT_RELATIVE_TOLERANCE = 1e-6
# Using Real should be better than this Union, but not possible yet:
# https://github.com/python/typeshed/pull/3108
DEFAULT_ABSOLUTE_TOLERANCE = 1e-12 # type: Union[float, Decimal]
DEFAULT_RELATIVE_TOLERANCE = 1e-6 # type: Union[float, Decimal]
def __repr__(self):
"""
@ -261,7 +265,8 @@ class ApproxScalar(ApproxBase):
# Return true if the two numbers are within the tolerance.
return abs(self.expected - actual) <= self.tolerance
__hash__ = None
# Ignore type because of https://github.com/python/mypy/issues/4266.
__hash__ = None # type: ignore
@property
def tolerance(self):
@ -691,7 +696,7 @@ def raises(expected_exception, *args, **kwargs):
fail(message)
raises.Exception = fail.Exception
raises.Exception = fail.Exception # type: ignore
class RaisesContext:

View File

@ -1,4 +1,5 @@
from pprint import pprint
from typing import Optional
import py
@ -28,7 +29,7 @@ def getslaveinfoline(node):
class BaseReport:
when = None
when = None # type: Optional[str]
location = None
def __init__(self, **kw):

View File

@ -26,7 +26,10 @@ class TempPathFactory:
# using os.path.abspath() to get absolute path instead of resolve() as it
# does not work the same in all platforms (see #4427)
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012)
converter=attr.converters.optional(lambda p: Path(os.path.abspath(str(p))))
# Ignore type because of https://github.com/python/mypy/issues/6172.
converter=attr.converters.optional(
lambda p: Path(os.path.abspath(str(p))) # type: ignore
)
)
_trace = attr.ib()
_basetemp = attr.ib(default=None)

View File

@ -589,7 +589,8 @@ raise ValueError()
def test_repr_local_with_exception_in_class_property(self):
class ExceptionWithBrokenClass(Exception):
@property
# Type ignored because it's bypassed intentionally.
@property # type: ignore
def __class__(self):
raise TypeError("boom!")

View File

@ -258,7 +258,8 @@ class TestRaises:
"""Test current behavior with regard to exceptions via __class__ (#4284)."""
class CrappyClass(Exception):
@property
# Type ignored because it's bypassed intentionally.
@property # type: ignore
def __class__(self):
assert False, "via __class__"

View File

@ -141,7 +141,8 @@ def test_safe_isclass():
assert safe_isclass(type) is True
class CrappyClass(Exception):
@property
# Type ignored because it's bypassed intentionally.
@property # type: ignore
def __class__(self):
assert False, "Should be ignored"

View File

@ -6,7 +6,8 @@ import pytest
from _pytest.debugging import _validate_usepdb_cls
try:
breakpoint
# Type ignored for Python <= 3.6.
breakpoint # type: ignore
except NameError:
SUPPORTS_BREAKPOINT_BUILTIN = False
else: