Skip to content

Commit 0f03844

Browse files
committed
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 pytest-dev#3342.
1 parent 60a358f commit 0f03844

20 files changed

+91
-42
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ env/
3535
.tox
3636
.cache
3737
.pytest_cache
38+
.mypy_cache
3839
.coverage
3940
.coverage.*
4041
coverage.xml

bench/bench.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pstats
77

88
script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"]
9-
stats = cProfile.run("pytest.cmdline.main(%r)" % script, "prof")
9+
cProfile.run("pytest.cmdline.main(%r)" % script, "prof")
1010
p = pstats.Stats("prof")
1111
p.strip_dirs()
1212
p.sort_stats("cumulative")

setup.cfg

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,26 @@ ignore =
6161

6262
[devpi:upload]
6363
formats = sdist.tgz,bdist_wheel
64+
65+
[mypy]
66+
python_version = 3.5
67+
# check_untyped_defs = True
68+
# disallow_any_decorated = True
69+
# disallow_any_explicit = True
70+
# disallow_any_expr = True
71+
# disallow_any_generics = True
72+
# disallow_any_unimported = True
73+
# disallow_incomplete_defs = True
74+
# disallow_subclassing_any = True
75+
# disallow_untyped_calls = True
76+
# disallow_untyped_decorators = True
77+
# disallow_untyped_defs = True
78+
ignore_missing_imports = True
79+
; implicit_reexport = False
80+
no_implicit_optional = True
81+
strict_equality = True
82+
strict_optional = True
83+
warn_redundant_casts = True
84+
# warn_return_any = True
85+
warn_unused_configs = True
86+
warn_unused_ignores = True

src/_pytest/_argcomplete.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import os
5757
import sys
5858
from glob import glob
59+
from typing import Optional
5960

6061

6162
class FastFilesCompleter:
@@ -91,7 +92,7 @@ def __call__(self, prefix, **kwargs):
9192
import argcomplete.completers
9293
except ImportError:
9394
sys.exit(-1)
94-
filescompleter = FastFilesCompleter()
95+
filescompleter = FastFilesCompleter() # type: Optional[FastFilesCompleter]
9596

9697
def try_argcomplete(parser):
9798
argcomplete.autocomplete(parser, always_complete_options=False)

src/_pytest/_code/code.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ def __init__(self, rawcode):
3333
def __eq__(self, other):
3434
return self.raw == other.raw
3535

36-
__hash__ = None
36+
# Ignore type because of https://github.com/python/mypy/issues/4266.
37+
__hash__ = None # type: ignore
3738

3839
def __ne__(self, other):
3940
return not self == other
@@ -255,10 +256,10 @@ def __str__(self):
255256
line = "???"
256257
return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line)
257258

258-
def name(self):
259+
def _name_get(self):
259260
return self.frame.code.raw.co_name
260261

261-
name = property(name, None, None, "co_name of underlaying code")
262+
name = property(_name_get, None, None, "co_name of underlaying code")
262263

263264

264265
class Traceback(list):

src/_pytest/_code/source.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ def __eq__(self, other):
4444
return str(self) == other
4545
return False
4646

47-
__hash__ = None
47+
# Ignore type because of https://github.com/python/mypy/issues/4266.
48+
__hash__ = None # type: ignore
4849

4950
def __getitem__(self, key):
5051
if isinstance(key, int):

src/_pytest/assertion/rewrite.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
import sys
1313
import tokenize
1414
import types
15+
from typing import Dict
16+
from typing import List
17+
from typing import Optional
18+
from typing import Set
1519

1620
import atomicwrites
1721

@@ -459,39 +463,40 @@ def _fix(node, lineno, col_offset):
459463
return node
460464

461465

462-
def _get_assertion_exprs(src: bytes): # -> Dict[int, str]
466+
def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
463467
"""Returns a mapping from {lineno: "assertion test expression"}"""
464-
ret = {}
468+
ret = {} # type: Dict[int, str]
465469

466470
depth = 0
467-
lines = []
468-
assert_lineno = None
469-
seen_lines = set()
471+
lines = [] # type: List[str]
472+
assert_lineno = None # type: Optional[int]
473+
seen_lines = set() # type: Set[int]
470474

471475
def _write_and_reset() -> None:
472476
nonlocal depth, lines, assert_lineno, seen_lines
477+
assert assert_lineno is not None
473478
ret[assert_lineno] = "".join(lines).rstrip().rstrip("\\")
474479
depth = 0
475480
lines = []
476481
assert_lineno = None
477482
seen_lines = set()
478483

479484
tokens = tokenize.tokenize(io.BytesIO(src).readline)
480-
for tp, src, (lineno, offset), _, line in tokens:
481-
if tp == tokenize.NAME and src == "assert":
485+
for tp, string, (lineno, offset), _, line in tokens:
486+
if tp == tokenize.NAME and string == "assert":
482487
assert_lineno = lineno
483488
elif assert_lineno is not None:
484489
# keep track of depth for the assert-message `,` lookup
485-
if tp == tokenize.OP and src in "([{":
490+
if tp == tokenize.OP and string in "([{":
486491
depth += 1
487-
elif tp == tokenize.OP and src in ")]}":
492+
elif tp == tokenize.OP and string in ")]}":
488493
depth -= 1
489494

490495
if not lines:
491496
lines.append(line[offset:])
492497
seen_lines.add(lineno)
493498
# a non-nested comma separates the expression from the message
494-
elif depth == 0 and tp == tokenize.OP and src == ",":
499+
elif depth == 0 and tp == tokenize.OP and string == ",":
495500
# one line assert with message
496501
if lineno in seen_lines and len(lines) == 1:
497502
offset_in_trimmed = offset + len(lines[-1]) - len(line)

src/_pytest/capture.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,7 @@ def __repr__(self):
568568
self.targetfd, getattr(self, "targetfd_save", None), self._state
569569
)
570570

571-
def start(self):
571+
def start(self): # type: ignore
572572
""" Start capturing on targetfd using memorized tmpfile. """
573573
try:
574574
os.fstat(self.targetfd_save)
@@ -585,7 +585,7 @@ def snap(self):
585585
self.tmpfile.truncate()
586586
return res
587587

588-
def done(self):
588+
def done(self): # type: ignore
589589
""" stop capturing, restore streams, return original capture file,
590590
seeked to position zero. """
591591
targetfd_save = self.__dict__.pop("targetfd_save")
@@ -618,7 +618,7 @@ class FDCapture(FDCaptureBinary):
618618
snap() produces text
619619
"""
620620

621-
EMPTY_BUFFER = str()
621+
EMPTY_BUFFER = str() # type: ignore
622622

623623
def snap(self):
624624
res = super().snap()
@@ -679,7 +679,7 @@ def writeorg(self, data):
679679

680680

681681
class SysCaptureBinary(SysCapture):
682-
EMPTY_BUFFER = b""
682+
EMPTY_BUFFER = b"" # type: ignore
683683

684684
def snap(self):
685685
res = self.tmpfile.buffer.getvalue()

src/_pytest/debugging.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ class pytestPDB:
7474

7575
_pluginmanager = None
7676
_config = None
77-
_saved = []
77+
_saved = [] # type: list
7878
_recursive_debug = 0
7979
_wrapped_pdb_cls = None
8080

src/_pytest/fixtures.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from collections import defaultdict
77
from collections import deque
88
from collections import OrderedDict
9+
from typing import Dict
10+
from typing import Tuple
911

1012
import attr
1113
import py
@@ -54,10 +56,10 @@ def pytest_sessionstart(session):
5456
session._fixturemanager = FixtureManager(session)
5557

5658

57-
scopename2class = {}
59+
scopename2class = {} # type: Dict[str, type]
5860

5961

60-
scope2props = dict(session=())
62+
scope2props = dict(session=()) # type: Dict[str, Tuple[str, ...]]
6163
scope2props["package"] = ("fspath",)
6264
scope2props["module"] = ("fspath", "module")
6365
scope2props["class"] = scope2props["module"] + ("cls",)
@@ -960,7 +962,7 @@ class FixtureFunctionMarker:
960962
scope = attr.ib()
961963
params = attr.ib(converter=attr.converters.optional(tuple))
962964
autouse = attr.ib(default=False)
963-
ids = attr.ib(default=None, converter=_ensure_immutable_ids)
965+
ids = attr.ib(default=None, converter=_ensure_immutable_ids) # type: ignore
964966
name = attr.ib(default=None)
965967

966968
def __call__(self, function):

src/_pytest/mark/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def pytest_cmdline_main(config):
9191
return 0
9292

9393

94-
pytest_cmdline_main.tryfirst = True
94+
pytest_cmdline_main.tryfirst = True # type: ignore
9595

9696

9797
def deselect_by_keyword(items, config):

src/_pytest/mark/structures.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from collections import namedtuple
44
from collections.abc import MutableMapping
55
from operator import attrgetter
6+
from typing import Set
67

78
import attr
89

@@ -298,7 +299,7 @@ def test_function():
298299
on the ``test_function`` object. """
299300

300301
_config = None
301-
_markers = set()
302+
_markers = set() # type: Set[str]
302303

303304
def __getattr__(self, name):
304305
if name[0] == "_":

src/_pytest/outcomes.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def exit(msg, returncode=None):
7070
raise Exit(msg, returncode)
7171

7272

73-
exit.Exception = Exit
73+
exit.Exception = Exit # type: ignore
7474

7575

7676
def skip(msg="", *, allow_module_level=False):
@@ -96,7 +96,7 @@ def skip(msg="", *, allow_module_level=False):
9696
raise Skipped(msg=msg, allow_module_level=allow_module_level)
9797

9898

99-
skip.Exception = Skipped
99+
skip.Exception = Skipped # type: ignore
100100

101101

102102
def fail(msg="", pytrace=True):
@@ -111,7 +111,7 @@ def fail(msg="", pytrace=True):
111111
raise Failed(msg=msg, pytrace=pytrace)
112112

113113

114-
fail.Exception = Failed
114+
fail.Exception = Failed # type: ignore
115115

116116

117117
class XFailed(Failed):
@@ -132,7 +132,7 @@ def xfail(reason=""):
132132
raise XFailed(reason)
133133

134134

135-
xfail.Exception = XFailed
135+
xfail.Exception = XFailed # type: ignore
136136

137137

138138
def importorskip(modname, minversion=None, reason=None):

src/_pytest/python.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1356,7 +1356,9 @@ def write_docstring(tw, doc, indent=" "):
13561356
tw.write(indent + line + "\n")
13571357

13581358

1359-
class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
1359+
class Function( # type: ignore
1360+
FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr
1361+
):
13601362
""" a Function Item is responsible for setting up and executing a
13611363
Python test function.
13621364
"""

src/_pytest/python_api.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from decimal import Decimal
1010
from itertools import filterfalse
1111
from numbers import Number
12+
from typing import Union
1213

1314
from more_itertools.more import always_iterable
1415

@@ -58,7 +59,8 @@ def __eq__(self, actual):
5859
a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual)
5960
)
6061

61-
__hash__ = None
62+
# Ignore type because of https://github.com/python/mypy/issues/4266.
63+
__hash__ = None # type: ignore
6264

6365
def __ne__(self, actual):
6466
return not (actual == self)
@@ -202,8 +204,10 @@ class ApproxScalar(ApproxBase):
202204
Perform approximate comparisons where the expected value is a single number.
203205
"""
204206

205-
DEFAULT_ABSOLUTE_TOLERANCE = 1e-12
206-
DEFAULT_RELATIVE_TOLERANCE = 1e-6
207+
# Using Real should be better than this Union, but not possible yet:
208+
# https://github.com/python/typeshed/pull/3108
209+
DEFAULT_ABSOLUTE_TOLERANCE = 1e-12 # type: Union[float, Decimal]
210+
DEFAULT_RELATIVE_TOLERANCE = 1e-6 # type: Union[float, Decimal]
207211

208212
def __repr__(self):
209213
"""
@@ -261,7 +265,8 @@ def __eq__(self, actual):
261265
# Return true if the two numbers are within the tolerance.
262266
return abs(self.expected - actual) <= self.tolerance
263267

264-
__hash__ = None
268+
# Ignore type because of https://github.com/python/mypy/issues/4266.
269+
__hash__ = None # type: ignore
265270

266271
@property
267272
def tolerance(self):
@@ -691,7 +696,7 @@ def raises(expected_exception, *args, **kwargs):
691696
fail(message)
692697

693698

694-
raises.Exception = fail.Exception
699+
raises.Exception = fail.Exception # type: ignore
695700

696701

697702
class RaisesContext:

src/_pytest/reports.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from pprint import pprint
2+
from typing import Optional
23

34
import py
45

@@ -28,7 +29,7 @@ def getslaveinfoline(node):
2829

2930

3031
class BaseReport:
31-
when = None
32+
when = None # type: Optional[str]
3233
location = None
3334

3435
def __init__(self, **kw):

src/_pytest/tmpdir.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ class TempPathFactory:
2626
# using os.path.abspath() to get absolute path instead of resolve() as it
2727
# does not work the same in all platforms (see #4427)
2828
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012)
29-
converter=attr.converters.optional(lambda p: Path(os.path.abspath(str(p))))
29+
converter=attr.converters.optional(
30+
lambda p: Path(os.path.abspath(str(p))) # type: ignore
31+
)
3032
)
3133
_trace = attr.ib()
3234
_basetemp = attr.ib(default=None)

testing/test_compat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def test_safe_isclass():
141141
assert safe_isclass(type) is True
142142

143143
class CrappyClass(Exception):
144-
@property
144+
@property # type: ignore
145145
def __class__(self):
146146
assert False, "Should be ignored"
147147

testing/test_pdb.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from _pytest.debugging import _validate_usepdb_cls
77

88
try:
9-
breakpoint
9+
breakpoint # type: ignore
1010
except NameError:
1111
SUPPORTS_BREAKPOINT_BUILTIN = False
1212
else:

0 commit comments

Comments
 (0)