Skip to content

Commit ae92cf2

Browse files
committed
Fix typing issues in testtools
1 parent 3e5748c commit ae92cf2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+748
-537
lines changed

testtools/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,10 @@
4141
"skip",
4242
"skipIf",
4343
"skipUnless",
44-
"try_import",
4544
"unique_text_generator",
4645
"version",
4746
]
4847

49-
from testtools.helpers import try_import
5048
from testtools.matchers._impl import Matcher # noqa: F401
5149
from testtools.runtest import (
5250
MultipleExceptions,

testtools/_version.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Type stub for auto-generated _version module
2+
__version__: tuple[int | str, ...]
3+
version: str

testtools/content.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,9 +235,9 @@ def filter_stack(stack):
235235

236236
def json_content(json_data):
237237
"""Create a JSON Content object from JSON-encodeable data."""
238-
data = json.dumps(json_data)
238+
json_str = json.dumps(json_data)
239239
# The json module perversely returns native str not bytes
240-
data = data.encode("utf8")
240+
data = json_str.encode("utf8")
241241
return Content(JSON, lambda: [data])
242242

243243

testtools/helpers.py

Lines changed: 9 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,14 @@
11
# Copyright (c) 2010-2012 testtools developers. See LICENSE for details.
22

3-
import sys
3+
from typing import Any, Callable, TypeVar
44

5+
T = TypeVar("T")
6+
K = TypeVar("K")
7+
V = TypeVar("V")
8+
R = TypeVar("R")
59

6-
def try_import(name, alternative=None, error_callback=None):
7-
"""Attempt to import a module, with a fallback.
810

9-
Attempt to import ``name``. If it fails, return ``alternative``. When
10-
supporting multiple versions of Python or optional dependencies, it is
11-
useful to be able to try to import a module.
12-
13-
:param name: The name of the object to import, e.g. ``os.path`` or
14-
``os.path.join``.
15-
:param alternative: The value to return if no module can be imported.
16-
Defaults to None.
17-
:param error_callback: If non-None, a callable that is passed the
18-
ImportError when the module cannot be loaded.
19-
"""
20-
module_segments = name.split(".")
21-
last_error = None
22-
remainder = []
23-
24-
# module_name will be what successfully imports. We cannot walk from the
25-
# __import__ result because in import loops (A imports A.B, which imports
26-
# C, which calls try_import("A.B")) A.B will not yet be set.
27-
while module_segments:
28-
module_name = ".".join(module_segments)
29-
try:
30-
__import__(module_name)
31-
except ImportError:
32-
last_error = sys.exc_info()[1]
33-
remainder.append(module_segments.pop())
34-
continue
35-
else:
36-
break
37-
else:
38-
if last_error is not None and error_callback is not None:
39-
error_callback(last_error)
40-
return alternative
41-
42-
module = sys.modules[module_name]
43-
nonexistent = object()
44-
for segment in reversed(remainder):
45-
module = getattr(module, segment, nonexistent)
46-
if module is nonexistent:
47-
if last_error is not None and error_callback is not None:
48-
error_callback(last_error)
49-
return alternative
50-
51-
return module
52-
53-
54-
def map_values(function, dictionary):
11+
def map_values(function: Callable[[V], R], dictionary: dict[K, V]) -> dict[K, R]:
5512
"""Map ``function`` across the values of ``dictionary``.
5613
5714
:return: A dict with the same keys as ``dictionary``, where the value
@@ -60,17 +17,17 @@ def map_values(function, dictionary):
6017
return {k: function(dictionary[k]) for k in dictionary}
6118

6219

63-
def filter_values(function, dictionary):
20+
def filter_values(function: Callable[[V], bool], dictionary: dict[K, V]) -> dict[K, V]:
6421
"""Filter ``dictionary`` by its values using ``function``."""
6522
return {k: v for k, v in dictionary.items() if function(v)}
6623

6724

68-
def dict_subtract(a, b):
25+
def dict_subtract(a: dict[K, V], b: dict[K, Any]) -> dict[K, V]:
6926
"""Return the part of ``a`` that's not in ``b``."""
7027
return {k: a[k] for k in set(a) - set(b)}
7128

7229

73-
def list_subtract(a, b):
30+
def list_subtract(a: list[T], b: list[T]) -> list[T]:
7431
"""Return a list ``a`` without the elements of ``b``.
7532
7633
If a particular value is in ``a`` twice and ``b`` once then the returned

testtools/matchers/_basic.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import operator
1919
import re
2020
from pprint import pformat
21+
from typing import Any, Callable
2122

2223
from ..compat import (
2324
text_repr,
@@ -45,6 +46,10 @@ def _format(thing):
4546
class _BinaryComparison:
4647
"""Matcher that compares an object to another object."""
4748

49+
mismatch_string: str
50+
# comparator is defined by subclasses - using Any to allow different signatures
51+
comparator: Callable[..., Any]
52+
4853
def __init__(self, expected):
4954
self.expected = expected
5055

@@ -56,9 +61,6 @@ def match(self, other):
5661
return None
5762
return _BinaryMismatch(other, self.mismatch_string, self.expected)
5863

59-
def comparator(self, expected, other):
60-
raise NotImplementedError(self.comparator)
61-
6264

6365
class _BinaryMismatch(Mismatch):
6466
"""Two things did not match."""
@@ -134,14 +136,14 @@ class Is(_BinaryComparison):
134136
class LessThan(_BinaryComparison):
135137
"""Matches if the item is less than the matchers reference object."""
136138

137-
comparator = operator.__lt__
139+
comparator = operator.lt
138140
mismatch_string = ">="
139141

140142

141143
class GreaterThan(_BinaryComparison):
142144
"""Matches if the item is greater than the matchers reference object."""
143145

144-
comparator = operator.__gt__
146+
comparator = operator.gt
145147
mismatch_string = "<="
146148

147149

testtools/matchers/_datastructures.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ def match(self, observed):
178178
else:
179179
not_matched.append(value)
180180
if not_matched or remaining_matchers:
181-
remaining_matchers = list(remaining_matchers)
181+
remaining_matchers_list = list(remaining_matchers)
182182
# There are various cases that all should be reported somewhat
183183
# differently.
184184

@@ -192,39 +192,40 @@ def match(self, observed):
192192
# 5) There are more values left over than matchers.
193193

194194
if len(not_matched) == 0:
195-
if len(remaining_matchers) > 1:
196-
msg = f"There were {len(remaining_matchers)} matchers left over: "
195+
if len(remaining_matchers_list) > 1:
196+
count = len(remaining_matchers_list)
197+
msg = f"There were {count} matchers left over: "
197198
else:
198199
msg = "There was 1 matcher left over: "
199-
msg += ", ".join(map(str, remaining_matchers))
200+
msg += ", ".join(map(str, remaining_matchers_list))
200201
return Mismatch(msg)
201-
elif len(remaining_matchers) == 0:
202+
elif len(remaining_matchers_list) == 0:
202203
if len(not_matched) > 1:
203204
return Mismatch(
204205
f"There were {len(not_matched)} values left over: {not_matched}"
205206
)
206207
else:
207208
return Mismatch(f"There was 1 value left over: {not_matched}")
208209
else:
209-
common_length = min(len(remaining_matchers), len(not_matched))
210+
common_length = min(len(remaining_matchers_list), len(not_matched))
210211
if common_length == 0:
211212
raise AssertionError("common_length can't be 0 here")
212213
if common_length > 1:
213214
msg = f"There were {common_length} mismatches"
214215
else:
215216
msg = "There was 1 mismatch"
216-
if len(remaining_matchers) > len(not_matched):
217-
extra_matchers = remaining_matchers[common_length:]
217+
if len(remaining_matchers_list) > len(not_matched):
218+
extra_matchers = remaining_matchers_list[common_length:]
218219
msg += f" and {len(extra_matchers)} extra matcher"
219220
if len(extra_matchers) > 1:
220221
msg += "s"
221222
msg += ": " + ", ".join(map(str, extra_matchers))
222-
elif len(not_matched) > len(remaining_matchers):
223+
elif len(not_matched) > len(remaining_matchers_list):
223224
extra_values = not_matched[common_length:]
224225
msg += f" and {len(extra_values)} extra value"
225226
if len(extra_values) > 1:
226227
msg += "s"
227228
msg += ": " + str(extra_values)
228229
return Annotate(
229-
msg, MatchesListwise(remaining_matchers[:common_length])
230+
msg, MatchesListwise(remaining_matchers_list[:common_length])
230231
).match(not_matched[:common_length])

testtools/matchers/_doctest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ def _toAscii(self, s):
3939
if getattr(doctest, "_encoding", None) is not None:
4040
from types import FunctionType as __F
4141

42-
__f = doctest.OutputChecker.output_difference.im_func
43-
__g = dict(__f.func_globals)
42+
__f = doctest.OutputChecker.output_difference.__func__ # type: ignore[attr-defined]
43+
__g = dict(__f.__globals__)
4444

4545
def _indent(s, indent=4, _pattern=re.compile("^(?!$)", re.MULTILINE)):
4646
"""Prepend non-empty lines in ``s`` with ``indent`` number of spaces"""

testtools/matchers/_filesystem.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def match(self, path):
130130
f.close()
131131

132132
def __str__(self):
133-
return f"File at path exists and contains {self.contents}"
133+
return f"File at path exists and contains {self.matcher}"
134134

135135

136136
class HasPermissions(Matcher):

testtools/matchers/_higherorder.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,9 +345,9 @@ def __init__(self, predicate, message, name, *args, **kwargs):
345345
self.kwargs = kwargs
346346

347347
def __str__(self):
348-
args = [str(arg) for arg in self.args]
348+
args_list = [str(arg) for arg in self.args]
349349
kwargs = ["{}={}".format(*item) for item in self.kwargs.items()]
350-
args = ", ".join(args + kwargs)
350+
args = ", ".join(args_list + kwargs)
351351
if self.name is None:
352352
name = f"MatchesPredicateWithParams({self.predicate!r}, {self.message!r})"
353353
else:

testtools/run.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@
1212
import sys
1313
import unittest
1414
from functools import partial
15+
from typing import Any
1516

1617
from testtools import TextTestResult
1718
from testtools.compat import unicode_output_stream
1819
from testtools.testsuite import filter_by_ids, iterate_tests, sorted_tests
1920

21+
# unittest.TestProgram has these methods but mypy's stubs don't include them
22+
# We'll just use unittest.TestProgram directly and ignore the type errors
23+
2024
defaultTestLoader = unittest.defaultTestLoader
2125
defaultTestLoaderCls = unittest.TestLoader
2226
have_discover = True
@@ -131,6 +135,7 @@ class TestProgram(unittest.TestProgram):
131135
verbosity = 1
132136
failfast = catchbreak = buffer = progName = None
133137
_discovery_parser = None
138+
test: Any # Set by parent class
134139

135140
def __init__(
136141
self,
@@ -210,7 +215,7 @@ def __init__(
210215
del self.testLoader.errors[:]
211216

212217
def _getParentArgParser(self):
213-
parser = super()._getParentArgParser()
218+
parser = super()._getParentArgParser() # type: ignore[misc]
214219
# XXX: Local edit (see http://bugs.python.org/issue22860)
215220
parser.add_argument(
216221
"-l",
@@ -230,7 +235,7 @@ def _getParentArgParser(self):
230235
return parser
231236

232237
def _do_discovery(self, argv, Loader=None):
233-
super()._do_discovery(argv, Loader=Loader)
238+
super()._do_discovery(argv, Loader=Loader) # type: ignore[misc]
234239
# XXX: Local edit (see http://bugs.python.org/issue22860)
235240
self.test = sorted_tests(self.test)
236241

0 commit comments

Comments
 (0)