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:
parent
60a358fa2d
commit
c1167ac552
|
@ -35,6 +35,7 @@ env/
|
|||
.tox
|
||||
.cache
|
||||
.pytest_cache
|
||||
.mypy_cache
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage.xml
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -74,7 +74,7 @@ class pytestPDB:
|
|||
|
||||
_pluginmanager = None
|
||||
_config = None
|
||||
_saved = []
|
||||
_saved = [] # type: list
|
||||
_recursive_debug = 0
|
||||
_wrapped_pdb_cls = None
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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] == "_":
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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!")
|
||||
|
||||
|
|
|
@ -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__"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue