diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d3a4aea4..0183a826 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,6 +44,10 @@ jobs: run: | python -m ruff format --check . + - name: Type Check + run: | + python -m mypy testtools + - name: Tests run: | python -W once -m testtools.run testtools.tests.test_suite diff --git a/.gitignore b/.gitignore index 9f196a82..dd6140b0 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ ChangeLog testtools/_version.py man/testtools.1 man +.mypy_cache/ diff --git a/MANIFEST.in b/MANIFEST.in index f8cfd680..f8f1ee6a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,4 +4,5 @@ include MANIFEST.in include NEWS include README.rst include .gitignore +include testtools/py.typed prune doc/_build diff --git a/pyproject.toml b/pyproject.toml index eb577675..106c9f5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ Homepage = "https://github.com/testing-cabal/testtools" [project.optional-dependencies] test = ["testscenarios", "testresources"] twisted = ["Twisted", "fixtures"] -dev = ["ruff==0.12.3"] +dev = ["ruff==0.12.3", "mypy>=1.0.0"] [tool.hatch.version] source = "vcs" diff --git a/testtools/__init__.py b/testtools/__init__.py index 80608726..3abd885e 100644 --- a/testtools/__init__.py +++ b/testtools/__init__.py @@ -41,12 +41,10 @@ "skip", "skipIf", "skipUnless", - "try_import", "unique_text_generator", "version", ] -from testtools.helpers import try_import from testtools.matchers._impl import Matcher # noqa: F401 from testtools.runtest import ( MultipleExceptions, diff --git a/testtools/_version.pyi b/testtools/_version.pyi new file mode 100644 index 00000000..d55dd47a --- /dev/null +++ b/testtools/_version.pyi @@ -0,0 +1,5 @@ +# Type stub for auto-generated _version module +from typing import Union + +__version__: tuple[Union[int, str], ...] # noqa: UP007 +version: str diff --git a/testtools/content.py b/testtools/content.py index deea8fc3..ef606068 100644 --- a/testtools/content.py +++ b/testtools/content.py @@ -235,9 +235,9 @@ def filter_stack(stack): def json_content(json_data): """Create a JSON Content object from JSON-encodeable data.""" - data = json.dumps(json_data) + json_str = json.dumps(json_data) # The json module perversely returns native str not bytes - data = data.encode("utf8") + data = json_str.encode("utf8") return Content(JSON, lambda: [data]) diff --git a/testtools/helpers.py b/testtools/helpers.py index f3e470f0..6ab8c94a 100644 --- a/testtools/helpers.py +++ b/testtools/helpers.py @@ -1,57 +1,14 @@ # Copyright (c) 2010-2012 testtools developers. See LICENSE for details. -import sys +from typing import Any, Callable, TypeVar +T = TypeVar("T") +K = TypeVar("K") +V = TypeVar("V") +R = TypeVar("R") -def try_import(name, alternative=None, error_callback=None): - """Attempt to import a module, with a fallback. - Attempt to import ``name``. If it fails, return ``alternative``. When - supporting multiple versions of Python or optional dependencies, it is - useful to be able to try to import a module. - - :param name: The name of the object to import, e.g. ``os.path`` or - ``os.path.join``. - :param alternative: The value to return if no module can be imported. - Defaults to None. - :param error_callback: If non-None, a callable that is passed the - ImportError when the module cannot be loaded. - """ - module_segments = name.split(".") - last_error = None - remainder = [] - - # module_name will be what successfully imports. We cannot walk from the - # __import__ result because in import loops (A imports A.B, which imports - # C, which calls try_import("A.B")) A.B will not yet be set. - while module_segments: - module_name = ".".join(module_segments) - try: - __import__(module_name) - except ImportError: - last_error = sys.exc_info()[1] - remainder.append(module_segments.pop()) - continue - else: - break - else: - if last_error is not None and error_callback is not None: - error_callback(last_error) - return alternative - - module = sys.modules[module_name] - nonexistent = object() - for segment in reversed(remainder): - module = getattr(module, segment, nonexistent) - if module is nonexistent: - if last_error is not None and error_callback is not None: - error_callback(last_error) - return alternative - - return module - - -def map_values(function, dictionary): +def map_values(function: Callable[[V], R], dictionary: dict[K, V]) -> dict[K, R]: """Map ``function`` across the values of ``dictionary``. :return: A dict with the same keys as ``dictionary``, where the value @@ -60,17 +17,17 @@ def map_values(function, dictionary): return {k: function(dictionary[k]) for k in dictionary} -def filter_values(function, dictionary): +def filter_values(function: Callable[[V], bool], dictionary: dict[K, V]) -> dict[K, V]: """Filter ``dictionary`` by its values using ``function``.""" return {k: v for k, v in dictionary.items() if function(v)} -def dict_subtract(a, b): +def dict_subtract(a: dict[K, V], b: dict[K, Any]) -> dict[K, V]: """Return the part of ``a`` that's not in ``b``.""" return {k: a[k] for k in set(a) - set(b)} -def list_subtract(a, b): +def list_subtract(a: list[T], b: list[T]) -> list[T]: """Return a list ``a`` without the elements of ``b``. If a particular value is in ``a`` twice and ``b`` once then the returned diff --git a/testtools/matchers/_basic.py b/testtools/matchers/_basic.py index c2ac5398..920c2d35 100644 --- a/testtools/matchers/_basic.py +++ b/testtools/matchers/_basic.py @@ -18,6 +18,7 @@ import operator import re from pprint import pformat +from typing import Any, Callable from ..compat import ( text_repr, @@ -45,6 +46,10 @@ def _format(thing): class _BinaryComparison: """Matcher that compares an object to another object.""" + mismatch_string: str + # comparator is defined by subclasses - using Any to allow different signatures + comparator: Callable[..., Any] + def __init__(self, expected): self.expected = expected @@ -56,9 +61,6 @@ def match(self, other): return None return _BinaryMismatch(other, self.mismatch_string, self.expected) - def comparator(self, expected, other): - raise NotImplementedError(self.comparator) - class _BinaryMismatch(Mismatch): """Two things did not match.""" @@ -134,14 +136,14 @@ class Is(_BinaryComparison): class LessThan(_BinaryComparison): """Matches if the item is less than the matchers reference object.""" - comparator = operator.__lt__ + comparator = operator.lt mismatch_string = ">=" class GreaterThan(_BinaryComparison): """Matches if the item is greater than the matchers reference object.""" - comparator = operator.__gt__ + comparator = operator.gt mismatch_string = "<=" diff --git a/testtools/matchers/_datastructures.py b/testtools/matchers/_datastructures.py index ab141968..fc94568e 100644 --- a/testtools/matchers/_datastructures.py +++ b/testtools/matchers/_datastructures.py @@ -178,7 +178,7 @@ def match(self, observed): else: not_matched.append(value) if not_matched or remaining_matchers: - remaining_matchers = list(remaining_matchers) + remaining_matchers_list = list(remaining_matchers) # There are various cases that all should be reported somewhat # differently. @@ -192,13 +192,14 @@ def match(self, observed): # 5) There are more values left over than matchers. if len(not_matched) == 0: - if len(remaining_matchers) > 1: - msg = f"There were {len(remaining_matchers)} matchers left over: " + if len(remaining_matchers_list) > 1: + count = len(remaining_matchers_list) + msg = f"There were {count} matchers left over: " else: msg = "There was 1 matcher left over: " - msg += ", ".join(map(str, remaining_matchers)) + msg += ", ".join(map(str, remaining_matchers_list)) return Mismatch(msg) - elif len(remaining_matchers) == 0: + elif len(remaining_matchers_list) == 0: if len(not_matched) > 1: return Mismatch( f"There were {len(not_matched)} values left over: {not_matched}" @@ -206,25 +207,25 @@ def match(self, observed): else: return Mismatch(f"There was 1 value left over: {not_matched}") else: - common_length = min(len(remaining_matchers), len(not_matched)) + common_length = min(len(remaining_matchers_list), len(not_matched)) if common_length == 0: raise AssertionError("common_length can't be 0 here") if common_length > 1: msg = f"There were {common_length} mismatches" else: msg = "There was 1 mismatch" - if len(remaining_matchers) > len(not_matched): - extra_matchers = remaining_matchers[common_length:] + if len(remaining_matchers_list) > len(not_matched): + extra_matchers = remaining_matchers_list[common_length:] msg += f" and {len(extra_matchers)} extra matcher" if len(extra_matchers) > 1: msg += "s" msg += ": " + ", ".join(map(str, extra_matchers)) - elif len(not_matched) > len(remaining_matchers): + elif len(not_matched) > len(remaining_matchers_list): extra_values = not_matched[common_length:] msg += f" and {len(extra_values)} extra value" if len(extra_values) > 1: msg += "s" msg += ": " + str(extra_values) return Annotate( - msg, MatchesListwise(remaining_matchers[:common_length]) + msg, MatchesListwise(remaining_matchers_list[:common_length]) ).match(not_matched[:common_length]) diff --git a/testtools/matchers/_doctest.py b/testtools/matchers/_doctest.py index 3bbcddf0..6998332b 100644 --- a/testtools/matchers/_doctest.py +++ b/testtools/matchers/_doctest.py @@ -39,8 +39,8 @@ def _toAscii(self, s): if getattr(doctest, "_encoding", None) is not None: from types import FunctionType as __F - __f = doctest.OutputChecker.output_difference.im_func - __g = dict(__f.func_globals) + __f = doctest.OutputChecker.output_difference.__func__ # type: ignore[attr-defined] + __g = dict(__f.__globals__) def _indent(s, indent=4, _pattern=re.compile("^(?!$)", re.MULTILINE)): """Prepend non-empty lines in ``s`` with ``indent`` number of spaces""" diff --git a/testtools/matchers/_exception.py b/testtools/matchers/_exception.py index d583abff..e1e822de 100644 --- a/testtools/matchers/_exception.py +++ b/testtools/matchers/_exception.py @@ -96,6 +96,9 @@ def __init__(self, exception_matcher=None): def match(self, matchee): try: + # Handle staticmethod objects by extracting the underlying function + if isinstance(matchee, staticmethod): + matchee = matchee.__func__ result = matchee() return Mismatch(f"{matchee!r} returned {result!r}") # Catch all exceptions: Raises() should be able to match a diff --git a/testtools/matchers/_filesystem.py b/testtools/matchers/_filesystem.py index 5452ef8c..5b131b4f 100644 --- a/testtools/matchers/_filesystem.py +++ b/testtools/matchers/_filesystem.py @@ -130,7 +130,7 @@ def match(self, path): f.close() def __str__(self): - return f"File at path exists and contains {self.contents}" + return f"File at path exists and contains {self.matcher}" class HasPermissions(Matcher): diff --git a/testtools/matchers/_higherorder.py b/testtools/matchers/_higherorder.py index 7c67d0d2..55cf191c 100644 --- a/testtools/matchers/_higherorder.py +++ b/testtools/matchers/_higherorder.py @@ -345,9 +345,9 @@ def __init__(self, predicate, message, name, *args, **kwargs): self.kwargs = kwargs def __str__(self): - args = [str(arg) for arg in self.args] + args_list = [str(arg) for arg in self.args] kwargs = ["{}={}".format(*item) for item in self.kwargs.items()] - args = ", ".join(args + kwargs) + args = ", ".join(args_list + kwargs) if self.name is None: name = f"MatchesPredicateWithParams({self.predicate!r}, {self.message!r})" else: diff --git a/testtools/matchers/_warnings.py b/testtools/matchers/_warnings.py index f243dc74..02526417 100644 --- a/testtools/matchers/_warnings.py +++ b/testtools/matchers/_warnings.py @@ -68,6 +68,9 @@ def __init__(self, warnings_matcher=None): def match(self, matchee): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") + # Handle staticmethod objects by extracting the underlying function + if isinstance(matchee, staticmethod): + matchee = matchee.__func__ matchee() if self.warnings_matcher is not None: return self.warnings_matcher.match(w) diff --git a/testtools/py.typed b/testtools/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/testtools/run.py b/testtools/run.py index 419ff15f..26b9daec 100755 --- a/testtools/run.py +++ b/testtools/run.py @@ -12,11 +12,15 @@ import sys import unittest from functools import partial +from typing import Any from testtools import TextTestResult from testtools.compat import unicode_output_stream from testtools.testsuite import filter_by_ids, iterate_tests, sorted_tests +# unittest.TestProgram has these methods but mypy's stubs don't include them +# We'll just use unittest.TestProgram directly and ignore the type errors + defaultTestLoader = unittest.defaultTestLoader defaultTestLoaderCls = unittest.TestLoader have_discover = True @@ -131,6 +135,7 @@ class TestProgram(unittest.TestProgram): verbosity = 1 failfast = catchbreak = buffer = progName = None _discovery_parser = None + test: Any # Set by parent class def __init__( self, @@ -210,7 +215,7 @@ def __init__( del self.testLoader.errors[:] def _getParentArgParser(self): - parser = super()._getParentArgParser() + parser = super()._getParentArgParser() # type: ignore[misc] # XXX: Local edit (see http://bugs.python.org/issue22860) parser.add_argument( "-l", @@ -230,7 +235,7 @@ def _getParentArgParser(self): return parser def _do_discovery(self, argv, Loader=None): - super()._do_discovery(argv, Loader=Loader) + super()._do_discovery(argv, Loader=Loader) # type: ignore[misc] # XXX: Local edit (see http://bugs.python.org/issue22860) self.test = sorted_tests(self.test) diff --git a/testtools/testcase.py b/testtools/testcase.py index b30c0701..84a3623c 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -19,19 +19,18 @@ import functools import itertools import sys -import types import unittest -from typing import Any, Protocol, TypeVar +from typing import Any, Protocol, TypeVar, Union from unittest.case import SkipTest from testtools import content from testtools.compat import reraise -from testtools.helpers import try_import from testtools.matchers import ( Annotate, Contains, Is, IsInstance, + Matcher, MatchesAll, MatchesException, MismatchError, @@ -166,7 +165,10 @@ def gather_details(source_dict, target_dict): # Circular import: fixtures imports gather_details from here, we import # fixtures, leading to gather_details not being available and fixtures being # unable to import it. -fixtures = try_import("fixtures") +try: + import fixtures +except ImportError: + fixtures = None class UseFixtureProtocol(Protocol): @@ -350,7 +352,7 @@ def _formatTypes(self, classOrIterable): className = ", ".join(klass.__name__ for klass in classOrIterable) return className - def addCleanup(self, function, *arguments, **keywordArguments): + def addCleanup(self, function, /, *arguments, **keywordArguments): """Add a cleanup function to be called after tearDown. Functions added with addCleanup will be called in reverse order of @@ -446,9 +448,9 @@ def assertIsInstance(self, obj, klass, msg=None): matcher = IsInstance(klass) self.assertThat(obj, matcher, msg) - def assertRaises(self, excClass, callableObj=None, *args, **kwargs): - """Fail unless an exception of class excClass is thrown - by callableObj when invoked with arguments args and keyword + def assertRaises(self, expected_exception, callable=None, *args, **kwargs): + """Fail unless an exception of class expected_exception is thrown + by callable when invoked with arguments args and keyword arguments kwargs. If a different type of exception is thrown, it will not be caught, and the test case will be deemed to have suffered an error, exactly as for an @@ -469,13 +471,13 @@ def assertRaises(self, excClass, callableObj=None, *args, **kwargs): the_exception = cm.exception self.assertEqual(the_exception.error_code, 3) """ - # If callableObj is None, we're being used as a context manager - if callableObj is None: - return _AssertRaisesContext(excClass, self, msg=kwargs.get("msg")) + # If callable is None, we're being used as a context manager + if callable is None: + return _AssertRaisesContext(expected_exception, self, msg=kwargs.get("msg")) class ReRaiseOtherTypes: def match(self, matchee): - if not issubclass(matchee[0], excClass): + if not issubclass(matchee[0], expected_exception): reraise(*matchee) class CaptureMatchee: @@ -484,9 +486,11 @@ def match(self, matchee): capture = CaptureMatchee() matcher = Raises( - MatchesAll(ReRaiseOtherTypes(), MatchesException(excClass), capture) + MatchesAll( + ReRaiseOtherTypes(), MatchesException(expected_exception), capture + ) ) - our_callable = Nullary(callableObj, *args, **kwargs) + our_callable = Nullary(callable, *args, **kwargs) self.assertThat(our_callable, matcher) return capture.matchee @@ -964,8 +968,10 @@ class WithAttributes: testtools.testcase.MyTest/test_bar[foo] """ - def id(self): - orig = super().id() + _get_test_method: Any # Provided by the class we're mixed with + + def id(self) -> str: + orig = super().id() # type: ignore[misc] # Depends on testtools.TestCase._get_test_method, be nice to support # plain unittest. fn = self._get_test_method() @@ -975,10 +981,7 @@ def id(self): return orig + "[" + ",".join(sorted(attributes)) + "]" -class_types = [type] -if getattr(types, "ClassType", None) is not None: - class_types.append(types.ClassType) -class_types = tuple(class_types) +class_types = (type,) def skip(reason): @@ -1113,9 +1116,12 @@ def __exit__(self, exc_type, exc_value, traceback): if exc_type != self.exc_type: return False if self.value_re: - matcher = MatchesException(self.exc_type, self.value_re) + exception_matcher = MatchesException(self.exc_type, self.value_re) + matcher: Union[Matcher, Annotate] if self.msg: - matcher = Annotate(self.msg, matcher) + matcher = Annotate(self.msg, exception_matcher) + else: + matcher = exception_matcher mismatch = matcher.match((exc_type, exc_value, traceback)) if mismatch: raise AssertionError(mismatch.describe()) diff --git a/testtools/testresult/doubles.py b/testtools/testresult/doubles.py index 06c3af46..33662cb5 100644 --- a/testtools/testresult/doubles.py +++ b/testtools/testresult/doubles.py @@ -241,7 +241,7 @@ def status( # Convenience for easier access to status fields _StatusEvent = namedtuple( - "_Event", + "_StatusEvent", [ "name", "test_id", diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 626f1240..b522925b 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -28,7 +28,7 @@ import sys import unittest from operator import methodcaller -from typing import ClassVar +from typing import ClassVar, Union from testtools.compat import _b from testtools.content import ( @@ -566,9 +566,20 @@ def stopTestRun(self): sink.stopTestRun() self._in_run = False - def status(self, **kwargs): - route_code = kwargs.get("route_code", None) - test_id = kwargs.get("test_id", None) + def status( + self, + test_id=None, + test_status=None, + test_tags=None, + runnable=True, + file_name=None, + file_bytes=None, + eof=False, + mime_type=None, + route_code=None, + timestamp=None, + ): + # route_code and test_id are already available as parameters if route_code is not None: prefix = route_code.split("/")[0] else: @@ -579,12 +590,23 @@ def status(self, **kwargs): route_code = route_code[len(prefix) + 1 :] if not route_code: route_code = None - kwargs["route_code"] = route_code + # Update route_code for forwarding elif test_id in self._test_ids: target = self._test_ids[test_id] else: target = self.fallback - target.status(**kwargs) + target.status( + test_id=test_id, + test_status=test_status, + test_tags=test_tags, + runnable=runnable, + file_name=file_name, + file_bytes=file_bytes, + eof=eof, + mime_type=mime_type, + route_code=route_code, + timestamp=timestamp, + ) def add_rule(self, sink, policy, do_start_stop_run=False, **policy_args): """Add a rule to route events to sink when they match a given policy. @@ -736,7 +758,7 @@ def got_file(self, file_name, file_bytes, mime_type=None): case = self else: content_type = _make_content_type(mime_type) - content_bytes = [] + content_bytes: list[bytes] = [] case = self.transform( ["details", file_name], Content(content_type, lambda: content_bytes) ) @@ -754,6 +776,7 @@ def to_test_case(self): if PlaceHolder is None: from testtools.testcase import PlaceHolder outcome = _status_map[self.status] + assert PlaceHolder is not None return PlaceHolder( self.id, outcome=outcome, @@ -1246,7 +1269,7 @@ def stopTestRun(self): self.stream.write("FAILED (") details = [] failure_count = sum( - map(len, (self.failures, self.errors, self.unexpectedSuccesses)) + len(x) for x in (self.failures, self.errors, self.unexpectedSuccesses) ) details.append(f"failures={failure_count}") self.stream.write(", ".join(details)) @@ -1290,8 +1313,8 @@ def __init__(self, target, semaphore): self.result = ExtendedToOriginalDecorator(target) self.semaphore = semaphore self._test_start = None - self._global_tags = set(), set() - self._test_tags = set(), set() + self._global_tags: tuple[set, set] = set(), set() + self._test_tags: tuple[set, set] = set(), set() def __repr__(self): return f"<{self.__class__.__name__} {self.result!r}>" @@ -1508,12 +1531,13 @@ def addUnexpectedSuccess(self, test, details=None): test.fail("") except test.failureException: return self.addFailure(test, sys.exc_info()) - if details is not None: - try: - return outcome(test, details=details) - except TypeError: - pass - return outcome(test) + else: + if details is not None: + try: + return outcome(test, details=details) + except TypeError: + pass + return outcome(test) finally: if self.failfast: self.stop() @@ -1644,6 +1668,7 @@ def __init__(self, decorated): # Deal with mismatched base class constructors. TestControl.__init__(self) self._started = False + self._tags: Union[TagContext, None] = None def _get_failfast(self): return len(self.targets) == 2 @@ -1758,6 +1783,8 @@ def startTestRun(self): @property def current_tags(self): """The currently set tags.""" + if self._tags is None: + return set() return self._tags.get_current_tags() def tags(self, new_tags, gone_tags): @@ -1766,7 +1793,8 @@ def tags(self, new_tags, gone_tags): :param new_tags: A set of tags to be added to the stream. :param gone_tags: A set of tags to be removed from the stream. """ - self._tags.change_tags(new_tags, gone_tags) + if self._tags is not None: + self._tags.change_tags(new_tags, gone_tags) def _now(self): """Return the current 'test time'. @@ -1863,10 +1891,33 @@ def __init__(self, decorated): # _StreamToTestRecord buffers and gives us individual tests. self.hook = _StreamToTestRecord(self._handle_tests) - def status(self, test_id=None, test_status=None, *args, **kwargs): + def status( + self, + test_id=None, + test_status=None, + test_tags=None, + runnable=True, + file_name=None, + file_bytes=None, + eof=False, + mime_type=None, + route_code=None, + timestamp=None, + ): if test_status == "exists": return - self.hook.status(test_id=test_id, test_status=test_status, *args, **kwargs) + self.hook.status( + test_id=test_id, + test_status=test_status, + test_tags=test_tags, + runnable=runnable, + file_name=file_name, + file_bytes=file_bytes, + eof=eof, + mime_type=mime_type, + route_code=route_code, + timestamp=timestamp, + ) def startTestRun(self): self.decorated.startTestRun() diff --git a/testtools/tests/helpers.py b/testtools/tests/helpers.py index 9b9086ff..c2875a2a 100644 --- a/testtools/tests/helpers.py +++ b/testtools/tests/helpers.py @@ -45,21 +45,24 @@ def stopTest(self, test): self._events.append(("stopTest", test)) super().stopTest(test) - def addFailure(self, test, error): - self._events.append(("addFailure", test, error)) - super().addFailure(test, error) - - def addError(self, test, error): - self._events.append(("addError", test, error)) - super().addError(test, error) - - def addSkip(self, test, reason): + def addFailure(self, test, err=None, details=None): + self._events.append(("addFailure", test, err)) + super().addFailure(test, err, details) + + def addError(self, test, err=None, details=None): + self._events.append(("addError", test, err)) + super().addError(test, err, details) + + def addSkip(self, test, reason=None, details=None): + # Extract reason from details if not provided directly + if reason is None and details and "reason" in details: + reason = details["reason"].as_text() self._events.append(("addSkip", test, reason)) - super().addSkip(test, reason) + super().addSkip(test, reason, details) - def addSuccess(self, test): + def addSuccess(self, test, details=None): self._events.append(("addSuccess", test)) - super().addSuccess(test) + super().addSuccess(test, details) def startTestRun(self): self._events.append("startTestRun") diff --git a/testtools/tests/matchers/helpers.py b/testtools/tests/matchers/helpers.py index 4a018aaf..57f0d992 100644 --- a/testtools/tests/matchers/helpers.py +++ b/testtools/tests/matchers/helpers.py @@ -1,12 +1,26 @@ # Copyright (c) 2008-2012 testtools developers. See LICENSE for details. -from testtools.tests.helpers import FullStackRunTest +from typing import Any, Callable, ClassVar, Protocol, runtime_checkable + + +@runtime_checkable +class MatcherTestProtocol(Protocol): + """Protocol for test classes that test matchers.""" + + matches_matcher: ClassVar[Any] + matches_matches: ClassVar[Any] + matches_mismatches: ClassVar[Any] + str_examples: ClassVar[Any] + describe_examples: ClassVar[Any] + assertEqual: Callable[..., Any] + assertNotEqual: Callable[..., Any] + assertThat: Callable[..., Any] class TestMatchersInterface: - run_tests_with = FullStackRunTest + """Mixin class that provides test methods for matcher interfaces.""" - def test_matches_match(self): + def test_matches_match(self: MatcherTestProtocol) -> None: matcher = self.matches_matcher matches = self.matches_matches mismatches = self.matches_mismatches @@ -17,7 +31,7 @@ def test_matches_match(self): self.assertNotEqual(None, mismatch) self.assertNotEqual(None, getattr(mismatch, "describe", None)) - def test__str__(self): + def test__str__(self: MatcherTestProtocol) -> None: # [(expected, object to __str__)]. from testtools.matchers._doctest import DocTestMatches @@ -25,14 +39,14 @@ def test__str__(self): for expected, matcher in examples: self.assertThat(matcher, DocTestMatches(expected)) - def test_describe_difference(self): + def test_describe_difference(self: MatcherTestProtocol) -> None: # [(expected, matchee, matcher), ...] examples = self.describe_examples for difference, matchee, matcher in examples: mismatch = matcher.match(matchee) self.assertEqual(difference, mismatch.describe()) - def test_mismatch_details(self): + def test_mismatch_details(self: MatcherTestProtocol) -> None: # The mismatch object must provide get_details, which must return a # dictionary mapping names to Content objects. examples = self.describe_examples diff --git a/testtools/tests/matchers/test_basic.py b/testtools/tests/matchers/test_basic.py index f621045d..6332b883 100644 --- a/testtools/tests/matchers/test_basic.py +++ b/testtools/tests/matchers/test_basic.py @@ -114,16 +114,16 @@ def test_long_unicode_and_object(self): class TestEqualsInterface(TestCase, TestMatchersInterface): - matches_matcher = Equals(1) - matches_matches: ClassVar[list] = [1] - matches_mismatches: ClassVar[list] = [2] + matches_matcher: ClassVar = Equals(1) + matches_matches: ClassVar = [1] + matches_mismatches: ClassVar = [2] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("Equals(1)", Equals(1)), ("Equals('1')", Equals("1")), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ("2 != 1", 2, Equals(1)), ( ( @@ -138,45 +138,45 @@ class TestEqualsInterface(TestCase, TestMatchersInterface): class TestNotEqualsInterface(TestCase, TestMatchersInterface): - matches_matcher = NotEquals(1) - matches_matches: ClassVar[list] = [2] - matches_mismatches: ClassVar[list] = [1] + matches_matcher: ClassVar = NotEquals(1) + matches_matches: ClassVar = [2] + matches_mismatches: ClassVar = [1] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("NotEquals(1)", NotEquals(1)), ("NotEquals('1')", NotEquals("1")), ] - describe_examples: ClassVar[list] = [("1 == 1", 1, NotEquals(1))] + describe_examples: ClassVar = [("1 == 1", 1, NotEquals(1))] class TestIsInterface(TestCase, TestMatchersInterface): foo = object() bar = object() - matches_matcher = Is(foo) - matches_matches: ClassVar[list] = [foo] - matches_mismatches: ClassVar[list] = [bar, 1] + matches_matcher: ClassVar = Is(foo) + matches_matches: ClassVar = [foo] + matches_mismatches: ClassVar = [bar, 1] - str_examples: ClassVar[list] = [("Is(2)", Is(2))] + str_examples: ClassVar = [("Is(2)", Is(2))] - describe_examples: ClassVar[list] = [("2 is not 1", 2, Is(1))] + describe_examples: ClassVar = [("2 is not 1", 2, Is(1))] class TestIsInstanceInterface(TestCase, TestMatchersInterface): class Foo: pass - matches_matcher = IsInstance(Foo) - matches_matches: ClassVar[list] = [Foo()] - matches_mismatches: ClassVar[list] = [object(), 1, Foo] + matches_matcher: ClassVar = IsInstance(Foo) + matches_matches: ClassVar = [Foo()] + matches_mismatches: ClassVar = [object(), 1, Foo] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("IsInstance(str)", IsInstance(str)), ("IsInstance(str, int)", IsInstance(str, int)), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ("'foo' is not an instance of int", "foo", IsInstance(int)), ( "'foo' is not an instance of any of (int, type)", @@ -187,46 +187,46 @@ class Foo: class TestLessThanInterface(TestCase, TestMatchersInterface): - matches_matcher = LessThan(4) - matches_matches: ClassVar[list] = [-5, 3] - matches_mismatches: ClassVar[list] = [4, 5, 5000] + matches_matcher: ClassVar = LessThan(4) + matches_matches: ClassVar = [-5, 3] + matches_mismatches: ClassVar = [4, 5, 5000] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("LessThan(12)", LessThan(12)), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ("5 >= 4", 5, LessThan(4)), ("4 >= 4", 4, LessThan(4)), ] class TestGreaterThanInterface(TestCase, TestMatchersInterface): - matches_matcher = GreaterThan(4) - matches_matches: ClassVar[list] = [5, 8] - matches_mismatches: ClassVar[list] = [-2, 0, 4] + matches_matcher: ClassVar = GreaterThan(4) + matches_matches: ClassVar = [5, 8] + matches_mismatches: ClassVar = [-2, 0, 4] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("GreaterThan(12)", GreaterThan(12)), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ("4 <= 5", 4, GreaterThan(5)), ("4 <= 4", 4, GreaterThan(4)), ] class TestContainsInterface(TestCase, TestMatchersInterface): - matches_matcher = Contains("foo") - matches_matches: ClassVar[list] = ["foo", "afoo", "fooa"] - matches_mismatches: ClassVar[list] = ["f", "fo", "oo", "faoo", "foao"] + matches_matcher: ClassVar = Contains("foo") + matches_matches: ClassVar = ["foo", "afoo", "fooa"] + matches_mismatches: ClassVar = ["f", "fo", "oo", "faoo", "foao"] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("Contains(1)", Contains(1)), ("Contains('foo')", Contains("foo")), ] - describe_examples: ClassVar[list] = [("1 not in 2", 2, Contains(1))] + describe_examples: ClassVar = [("1 not in 2", 2, Contains(1))] class DoesNotStartWithTests(TestCase): @@ -352,21 +352,21 @@ def test_mismatch_sets_expected(self): class TestSameMembers(TestCase, TestMatchersInterface): - matches_matcher = SameMembers([1, 1, 2, 3, {"foo": "bar"}]) - matches_matches: ClassVar[list] = [ + matches_matcher: ClassVar = SameMembers([1, 1, 2, 3, {"foo": "bar"}]) + matches_matches: ClassVar = [ [1, 1, 2, 3, {"foo": "bar"}], [3, {"foo": "bar"}, 1, 2, 1], [3, 2, 1, {"foo": "bar"}, 1], (2, {"foo": "bar"}, 3, 1, 1), ] - matches_mismatches: ClassVar[list] = [ + matches_mismatches: ClassVar = [ {1, 2, 3}, [1, 1, 2, 3, 5], [1, 2, 3, {"foo": "bar"}], "foo", ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( ( "elements differ:\n" @@ -399,17 +399,17 @@ class TestSameMembers(TestCase, TestMatchersInterface): ), ] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("SameMembers([1, 2, 3])", SameMembers([1, 2, 3])), ] class TestMatchesRegex(TestCase, TestMatchersInterface): - matches_matcher = MatchesRegex("a|b") - matches_matches: ClassVar[list] = ["a", "b"] - matches_mismatches: ClassVar[list] = ["c"] + matches_matcher: ClassVar = MatchesRegex("a|b") + matches_matches: ClassVar = ["a", "b"] + matches_mismatches: ClassVar = ["c"] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("MatchesRegex('a|b')", MatchesRegex("a|b")), ("MatchesRegex('a|b', re.M)", MatchesRegex("a|b", re.M)), ("MatchesRegex('a|b', re.I|re.M)", MatchesRegex("a|b", re.I | re.M)), @@ -417,7 +417,7 @@ class TestMatchesRegex(TestCase, TestMatchersInterface): ("MatchesRegex({!r})".format("\xa7"), MatchesRegex("\xa7")), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ("'c' does not match /a|b/", "c", MatchesRegex("a|b")), ("'c' does not match /a\\d/", "c", MatchesRegex(r"a\d")), ( @@ -430,15 +430,15 @@ class TestMatchesRegex(TestCase, TestMatchersInterface): class TestHasLength(TestCase, TestMatchersInterface): - matches_matcher = HasLength(2) - matches_matches: ClassVar[list] = [[1, 2]] - matches_mismatches: ClassVar[list] = [[], [1], [3, 2, 1]] + matches_matcher: ClassVar = HasLength(2) + matches_matches: ClassVar = [[1, 2]] + matches_mismatches: ClassVar = [[], [1], [3, 2, 1]] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("HasLength(2)", HasLength(2)), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ("len([]) != 1", [], HasLength(1)), ] diff --git a/testtools/tests/matchers/test_const.py b/testtools/tests/matchers/test_const.py index 3b35fd10..1aed0335 100644 --- a/testtools/tests/matchers/test_const.py +++ b/testtools/tests/matchers/test_const.py @@ -10,23 +10,23 @@ class TestAlwaysInterface(TestMatchersInterface, TestCase): """:py:func:`~testtools.matchers.Always` always matches.""" - matches_matcher = Always() - matches_matches: ClassVar[list] = [42, object(), "hi mom"] - matches_mismatches: ClassVar[list] = [] + matches_matcher: ClassVar = Always() + matches_matches: ClassVar = [42, object(), "hi mom"] + matches_mismatches: ClassVar = [] - str_examples: ClassVar[list] = [("Always()", Always())] - describe_examples: ClassVar[list] = [] + str_examples: ClassVar = [("Always()", Always())] + describe_examples: ClassVar = [] class TestNeverInterface(TestMatchersInterface, TestCase): """:py:func:`~testtools.matchers.Never` never matches.""" - matches_matcher = Never() - matches_matches: ClassVar[list] = [] - matches_mismatches: ClassVar[list] = [42, object(), "hi mom"] + matches_matcher: ClassVar = Never() + matches_matches: ClassVar = [] + matches_mismatches: ClassVar = [42, object(), "hi mom"] - str_examples: ClassVar[list] = [("Never()", Never())] - describe_examples: ClassVar[list] = [("Inevitable mismatch on 42", 42, Never())] + str_examples: ClassVar = [("Never()", Never())] + describe_examples: ClassVar = [("Inevitable mismatch on 42", 42, Never())] def test_suite(): diff --git a/testtools/tests/matchers/test_datastructures.py b/testtools/tests/matchers/test_datastructures.py index 055df263..13253ee6 100644 --- a/testtools/tests/matchers/test_datastructures.py +++ b/testtools/tests/matchers/test_datastructures.py @@ -50,15 +50,15 @@ def __init__(self, x, y): self.x = x self.y = y - matches_matcher = MatchesStructure(x=Equals(1), y=Equals(2)) - matches_matches: ClassVar[list] = [SimpleClass(1, 2)] - matches_mismatches: ClassVar[list] = [ + matches_matcher: ClassVar = MatchesStructure(x=Equals(1), y=Equals(2)) + matches_matches: ClassVar = [SimpleClass(1, 2)] + matches_mismatches: ClassVar = [ SimpleClass(2, 2), SimpleClass(1, 1), SimpleClass(3, 3), ] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("MatchesStructure(x=Equals(1))", MatchesStructure(x=Equals(1))), ("MatchesStructure(y=Equals(2))", MatchesStructure(y=Equals(2))), ( @@ -67,7 +67,7 @@ def __init__(self, x, y): ), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( """\ Differences: [ @@ -214,19 +214,19 @@ def test_mismatch_and_two_too_many_values(self): class TestContainsAllInterface(TestCase, TestMatchersInterface): - matches_matcher = ContainsAll(["foo", "bar"]) - matches_matches: ClassVar[list] = [ + matches_matcher: ClassVar = ContainsAll(["foo", "bar"]) + matches_matches: ClassVar = [ ["foo", "bar"], ["foo", "z", "bar"], ["bar", "foo"], ] - matches_mismatches: ClassVar[list] = [["f", "g"], ["foo", "baz"], []] + matches_mismatches: ClassVar = [["f", "g"], ["foo", "baz"], []] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("MatchesAll(Contains('foo'), Contains('bar'))", ContainsAll(["foo", "bar"])), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( """Differences: [ 'baz' not in 'foo' diff --git a/testtools/tests/matchers/test_dict.py b/testtools/tests/matchers/test_dict.py index c0900219..eddda739 100644 --- a/testtools/tests/matchers/test_dict.py +++ b/testtools/tests/matchers/test_dict.py @@ -18,25 +18,25 @@ class TestMatchesAllDictInterface(TestCase, TestMatchersInterface): - matches_matcher = MatchesAllDict({"a": NotEquals(1), "b": NotEquals(2)}) - matches_matches: ClassVar[list] = [3, 4] - matches_mismatches: ClassVar[list] = [1, 2] + matches_matcher: ClassVar = MatchesAllDict({"a": NotEquals(1), "b": NotEquals(2)}) + matches_matches: ClassVar = [3, 4] + matches_mismatches: ClassVar = [1, 2] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("MatchesAllDict({'a': NotEquals(1), 'b': NotEquals(2)})", matches_matcher) ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ("""a: 1 == 1""", 1, matches_matcher), ] class TestKeysEqualEmpty(TestCase, TestMatchersInterface): - matches_matcher = KeysEqual() - matches_matches: ClassVar[list] = [ + matches_matcher: ClassVar = KeysEqual() + matches_matches: ClassVar = [ {}, ] - matches_mismatches: ClassVar[list] = [ + matches_mismatches: ClassVar = [ {"foo": 0, "bar": 1}, {"foo": 0}, {"bar": 1}, @@ -44,21 +44,21 @@ class TestKeysEqualEmpty(TestCase, TestMatchersInterface): {"a": None, "b": None, "c": None}, ] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("KeysEqual()", KeysEqual()), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ("[] does not match {1: 2}: Keys not equal", {1: 2}, matches_matcher), ] class TestKeysEqualWithList(TestCase, TestMatchersInterface): - matches_matcher = KeysEqual("foo", "bar") - matches_matches: ClassVar[list] = [ + matches_matcher: ClassVar = KeysEqual("foo", "bar") + matches_matches: ClassVar = [ {"foo": 0, "bar": 1}, ] - matches_mismatches: ClassVar[list] = [ + matches_mismatches: ClassVar = [ {}, {"foo": 0}, {"bar": 1}, @@ -66,11 +66,11 @@ class TestKeysEqualWithList(TestCase, TestMatchersInterface): {"a": None, "b": None, "c": None}, ] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("KeysEqual('foo', 'bar')", KeysEqual("foo", "bar")), ] - describe_examples: ClassVar[list] = [] + describe_examples: ClassVar = [] def test_description(self): matchee = {"foo": 0, "bar": 1, "baz": 2} @@ -83,34 +83,36 @@ def test_description(self): class TestKeysEqualWithDict(TestKeysEqualWithList): - matches_matcher = KeysEqual({"foo": 3, "bar": 4}) + matches_matcher: ClassVar = KeysEqual({"foo": 3, "bar": 4}) class TestSubDictOf(TestCase, TestMatchersInterface): - matches_matcher = _SubDictOf({"foo": "bar", "baz": "qux"}) + matches_matcher: ClassVar = _SubDictOf({"foo": "bar", "baz": "qux"}) - matches_matches: ClassVar[list] = [ + matches_matches: ClassVar = [ {"foo": "bar", "baz": "qux"}, {"foo": "bar"}, ] - matches_mismatches: ClassVar[list] = [ + matches_mismatches: ClassVar = [ {"foo": "bar", "baz": "qux", "cat": "dog"}, {"foo": "bar", "cat": "dog"}, ] - str_examples: ClassVar[list] = [] - describe_examples: ClassVar[list] = [] + str_examples: ClassVar = [] + describe_examples: ClassVar = [] class TestMatchesDict(TestCase, TestMatchersInterface): - matches_matcher = MatchesDict({"foo": Equals("bar"), "baz": Not(Equals("qux"))}) + matches_matcher: ClassVar = MatchesDict( + {"foo": Equals("bar"), "baz": Not(Equals("qux"))} + ) - matches_matches: ClassVar[list] = [ + matches_matches: ClassVar = [ {"foo": "bar", "baz": None}, {"foo": "bar", "baz": "quux"}, ] - matches_mismatches: ClassVar[list] = [ + matches_mismatches: ClassVar = [ {}, {"foo": "bar", "baz": "qux"}, {"foo": "bop", "baz": "qux"}, @@ -118,7 +120,7 @@ class TestMatchesDict(TestCase, TestMatchersInterface): {"foo": "bar", "cat": "dog"}, ] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ( "MatchesDict({{'baz': {}, 'foo': {}}})".format( Not(Equals("qux")), Equals("bar") @@ -127,7 +129,7 @@ class TestMatchesDict(TestCase, TestMatchersInterface): ), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( "Missing: {\n 'baz': Not(Equals('qux')),\n 'foo': Equals('bar'),\n}", {}, @@ -160,14 +162,16 @@ class TestMatchesDict(TestCase, TestMatchersInterface): class TestContainsDict(TestCase, TestMatchersInterface): - matches_matcher = ContainsDict({"foo": Equals("bar"), "baz": Not(Equals("qux"))}) + matches_matcher: ClassVar = ContainsDict( + {"foo": Equals("bar"), "baz": Not(Equals("qux"))} + ) - matches_matches: ClassVar[list] = [ + matches_matches: ClassVar = [ {"foo": "bar", "baz": None}, {"foo": "bar", "baz": "quux"}, {"foo": "bar", "baz": "quux", "cat": "dog"}, ] - matches_mismatches: ClassVar[list] = [ + matches_mismatches: ClassVar = [ {}, {"foo": "bar", "baz": "qux"}, {"foo": "bop", "baz": "qux"}, @@ -175,7 +179,7 @@ class TestContainsDict(TestCase, TestMatchersInterface): {"foo": "bar"}, ] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ( "ContainsDict({{'baz': {}, 'foo': {}}})".format( Not(Equals("qux")), Equals("bar") @@ -184,7 +188,7 @@ class TestContainsDict(TestCase, TestMatchersInterface): ), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( "Missing: {\n 'baz': Not(Equals('qux')),\n 'foo': Equals('bar'),\n}", {}, @@ -212,22 +216,24 @@ class TestContainsDict(TestCase, TestMatchersInterface): class TestContainedByDict(TestCase, TestMatchersInterface): - matches_matcher = ContainedByDict({"foo": Equals("bar"), "baz": Not(Equals("qux"))}) + matches_matcher: ClassVar = ContainedByDict( + {"foo": Equals("bar"), "baz": Not(Equals("qux"))} + ) - matches_matches: ClassVar[list] = [ + matches_matches: ClassVar = [ {}, {"foo": "bar"}, {"foo": "bar", "baz": "quux"}, {"baz": "quux"}, ] - matches_mismatches: ClassVar[list] = [ + matches_mismatches: ClassVar = [ {"foo": "bar", "baz": "quux", "cat": "dog"}, {"foo": "bar", "baz": "qux"}, {"foo": "bop", "baz": "qux"}, {"foo": "bar", "cat": "dog"}, ] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ( "ContainedByDict({{'baz': {}, 'foo': {}}})".format( Not(Equals("qux")), Equals("bar") @@ -236,7 +242,7 @@ class TestContainedByDict(TestCase, TestMatchersInterface): ), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( "Differences: {\n 'baz': 'qux' matches Equals('qux'),\n}", {"foo": "bar", "baz": "qux"}, diff --git a/testtools/tests/matchers/test_doctest.py b/testtools/tests/matchers/test_doctest.py index a1ead2b7..975d9733 100644 --- a/testtools/tests/matchers/test_doctest.py +++ b/testtools/tests/matchers/test_doctest.py @@ -13,14 +13,14 @@ class TestDocTestMatchesInterface(TestCase, TestMatchersInterface): - matches_matcher = DocTestMatches("Ran 1 test in ...s", doctest.ELLIPSIS) - matches_matches: ClassVar[list] = ["Ran 1 test in 0.000s", "Ran 1 test in 1.234s"] - matches_mismatches: ClassVar[list] = [ + matches_matcher: ClassVar = DocTestMatches("Ran 1 test in ...s", doctest.ELLIPSIS) + matches_matches: ClassVar = ["Ran 1 test in 0.000s", "Ran 1 test in 1.234s"] + matches_mismatches: ClassVar = [ "Ran 1 tests in 0.000s", "Ran 2 test in 0.000s", ] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ( "DocTestMatches('Ran 1 test in ...s\\n')", DocTestMatches("Ran 1 test in ...s"), @@ -28,7 +28,7 @@ class TestDocTestMatchesInterface(TestCase, TestMatchersInterface): ("DocTestMatches('foo\\n', flags=8)", DocTestMatches("foo", flags=8)), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( "Expected:\n Ran 1 tests in ...s\nGot:\n Ran 1 test in 0.123s\n", "Ran 1 test in 0.123s", @@ -38,15 +38,15 @@ class TestDocTestMatchesInterface(TestCase, TestMatchersInterface): class TestDocTestMatchesInterfaceUnicode(TestCase, TestMatchersInterface): - matches_matcher = DocTestMatches("\xa7...", doctest.ELLIPSIS) - matches_matches: ClassVar[list] = ["\xa7", "\xa7 more\n"] - matches_mismatches: ClassVar[list] = ["\\xa7", "more \xa7", "\n\xa7"] + matches_matcher: ClassVar = DocTestMatches("\xa7...", doctest.ELLIPSIS) + matches_matches: ClassVar = ["\xa7", "\xa7 more\n"] + matches_mismatches: ClassVar = ["\\xa7", "more \xa7", "\n\xa7"] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("DocTestMatches({!r})".format("\xa7\n"), DocTestMatches("\xa7")), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( "Expected:\n \xa7\nGot:\n a\n", "a", diff --git a/testtools/tests/matchers/test_exception.py b/testtools/tests/matchers/test_exception.py index 1d01266b..fa2ec1d8 100644 --- a/testtools/tests/matchers/test_exception.py +++ b/testtools/tests/matchers/test_exception.py @@ -25,22 +25,22 @@ def make_error(type, *args, **kwargs): class TestMatchesExceptionInstanceInterface(TestCase, TestMatchersInterface): - matches_matcher = MatchesException(ValueError("foo")) + matches_matcher: ClassVar = MatchesException(ValueError("foo")) error_foo = make_error(ValueError, "foo") error_bar = make_error(ValueError, "bar") error_base_foo = make_error(Exception, "foo") - matches_matches: ClassVar[list] = [error_foo] - matches_mismatches: ClassVar[list] = [error_bar, error_base_foo] + matches_matches: ClassVar = [error_foo] + matches_mismatches: ClassVar = [error_bar, error_base_foo] _e = "" - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ( f"MatchesException(Exception('foo'{_e}))", MatchesException(Exception("foo")), ) ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( f"{Exception!r} is not a {ValueError!r}", error_base_foo, @@ -55,17 +55,17 @@ class TestMatchesExceptionInstanceInterface(TestCase, TestMatchersInterface): class TestMatchesExceptionTypeInterface(TestCase, TestMatchersInterface): - matches_matcher = MatchesException(ValueError) + matches_matcher: ClassVar = MatchesException(ValueError) error_foo = make_error(ValueError, "foo") error_sub = make_error(UnicodeError, "bar") error_base_foo = make_error(Exception, "foo") - matches_matches: ClassVar[list] = [error_foo, error_sub] - matches_mismatches: ClassVar[list] = [error_base_foo] + matches_matches: ClassVar = [error_foo, error_sub] + matches_mismatches: ClassVar = [error_base_foo] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ (f"MatchesException({Exception!r})", MatchesException(Exception)) ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( f"{Exception!r} is not a {ValueError!r}", error_base_foo, @@ -75,72 +75,77 @@ class TestMatchesExceptionTypeInterface(TestCase, TestMatchersInterface): class TestMatchesExceptionTypeReInterface(TestCase, TestMatchersInterface): - matches_matcher = MatchesException(ValueError, "fo.") + matches_matcher: ClassVar = MatchesException(ValueError, "fo.") error_foo = make_error(ValueError, "foo") error_sub = make_error(UnicodeError, "foo") error_bar = make_error(ValueError, "bar") - matches_matches: ClassVar[list] = [error_foo, error_sub] - matches_mismatches: ClassVar[list] = [error_bar] + matches_matches: ClassVar = [error_foo, error_sub] + matches_mismatches: ClassVar = [error_bar] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ (f"MatchesException({Exception!r})", MatchesException(Exception, "fo.")) ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ("'bar' does not match /fo./", error_bar, MatchesException(ValueError, "fo.")), ] class TestMatchesExceptionTypeMatcherInterface(TestCase, TestMatchersInterface): - matches_matcher = MatchesException( + matches_matcher: ClassVar = MatchesException( ValueError, AfterPreprocessing(str, Equals("foo")) ) error_foo = make_error(ValueError, "foo") error_sub = make_error(UnicodeError, "foo") error_bar = make_error(ValueError, "bar") - matches_matches: ClassVar[list] = [error_foo, error_sub] - matches_mismatches: ClassVar[list] = [error_bar] + matches_matches: ClassVar = [error_foo, error_sub] + matches_mismatches: ClassVar = [error_bar] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ (f"MatchesException({Exception!r})", MatchesException(Exception, Equals("foo"))) ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ (f"{error_bar[1]!r} != 5", error_bar, MatchesException(ValueError, Equals(5))), ] class TestRaisesInterface(TestCase, TestMatchersInterface): - matches_matcher = Raises() + matches_matcher: ClassVar = Raises() + @staticmethod def boom(): raise Exception("foo") - matches_matches: ClassVar[list] = [boom] - matches_mismatches: ClassVar[list] = [lambda: None] + matches_matches: ClassVar = [boom] + matches_mismatches: ClassVar = [lambda: None] # Tricky to get function objects to render constantly, and the interfaces # helper uses assertEqual rather than (for instance) DocTestMatches. - str_examples: ClassVar[list] = [] + str_examples: ClassVar = [] - describe_examples: ClassVar[list] = [] + describe_examples: ClassVar = [] class TestRaisesExceptionMatcherInterface(TestCase, TestMatchersInterface): - matches_matcher = Raises(exception_matcher=MatchesException(Exception("foo"))) + matches_matcher: ClassVar = Raises( + exception_matcher=MatchesException(Exception("foo")) + ) + @staticmethod def boom_bar(): raise Exception("bar") + @staticmethod def boom_foo(): raise Exception("foo") - matches_matches: ClassVar[list] = [boom_foo] - matches_mismatches: ClassVar[list] = [lambda: None, boom_bar] + matches_matches: ClassVar = [boom_foo] + matches_mismatches: ClassVar = [lambda: None, boom_bar] # Tricky to get function objects to render constantly, and the interfaces # helper uses assertEqual rather than (for instance) DocTestMatches. - str_examples: ClassVar[list] = [] + str_examples: ClassVar = [] - describe_examples: ClassVar[list] = [] + describe_examples: ClassVar = [] class TestRaisesBaseTypes(TestCase): diff --git a/testtools/tests/matchers/test_filesystem.py b/testtools/tests/matchers/test_filesystem.py index 9651a88f..c8c8377f 100644 --- a/testtools/tests/matchers/test_filesystem.py +++ b/testtools/tests/matchers/test_filesystem.py @@ -4,6 +4,7 @@ import shutil import tarfile import tempfile +from typing import Any, Callable from testtools import TestCase from testtools.matchers import ( @@ -24,6 +25,9 @@ class PathHelpers: + # This is provided by TestCase when mixed in + addCleanup: Callable[..., Any] + def mkdtemp(self): directory = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, directory) diff --git a/testtools/tests/matchers/test_higherorder.py b/testtools/tests/matchers/test_higherorder.py index 498a4a47..382c3ea5 100644 --- a/testtools/tests/matchers/test_higherorder.py +++ b/testtools/tests/matchers/test_higherorder.py @@ -28,22 +28,22 @@ class TestAllMatch(TestCase, TestMatchersInterface): - matches_matcher = AllMatch(LessThan(10)) - matches_matches: ClassVar[list] = [ + matches_matcher: ClassVar = AllMatch(LessThan(10)) + matches_matches: ClassVar = [ [9, 9, 9], (9, 9), iter([9, 9, 9, 9, 9]), ] - matches_mismatches: ClassVar[list] = [ + matches_mismatches: ClassVar = [ [11, 9, 9], iter([9, 12, 9, 11]), ] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("AllMatch(LessThan(12))", AllMatch(LessThan(12))), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( "Differences: [\n11 >= 10\n10 >= 10\n]", [11, 9, 10], @@ -53,14 +53,14 @@ class TestAllMatch(TestCase, TestMatchersInterface): class TestAnyMatch(TestCase, TestMatchersInterface): - matches_matcher = AnyMatch(Equals("elephant")) - matches_matches: ClassVar[list] = [ + matches_matcher: ClassVar = AnyMatch(Equals("elephant")) + matches_matches: ClassVar = [ ["grass", "cow", "steak", "milk", "elephant"], (13, "elephant"), ["elephant", "elephant", "elephant"], {"hippo", "rhino", "elephant"}, ] - matches_mismatches: ClassVar[list] = [ + matches_mismatches: ClassVar = [ [], ["grass", "cow", "steak", "milk"], (13, 12, 10), @@ -68,11 +68,11 @@ class TestAnyMatch(TestCase, TestMatchersInterface): {"hippo", "rhino", "diplodocus"}, ] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("AnyMatch(Equals('elephant'))", AnyMatch(Equals("elephant"))), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( "Differences: [\n11 != 7\n9 != 7\n10 != 7\n]", [11, 9, 10], @@ -81,22 +81,24 @@ class TestAnyMatch(TestCase, TestMatchersInterface): ] -class TestAfterPreprocessing(TestCase, TestMatchersInterface): - def parity(x): - return x % 2 +# Module-level function to avoid PyPy compilation issues with staticmethod +def parity(x): + return x % 2 + - matches_matcher = AfterPreprocessing(parity, Equals(1)) - matches_matches: ClassVar[list] = [3, 5] - matches_mismatches: ClassVar[list] = [2] +class TestAfterPreprocessing(TestCase, TestMatchersInterface): + matches_matcher: ClassVar = AfterPreprocessing(parity, Equals(1)) + matches_matches: ClassVar = [3, 5] + matches_mismatches: ClassVar = [2] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ( "AfterPreprocessing(, Equals(1))", AfterPreprocessing(parity, Equals(1)), ), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( "0 != 1: after on 2", 2, @@ -107,18 +109,18 @@ def parity(x): class TestMatchersAnyInterface(TestCase, TestMatchersInterface): - matches_matcher = MatchesAny(DocTestMatches("1"), DocTestMatches("2")) - matches_matches: ClassVar[list] = ["1", "2"] - matches_mismatches: ClassVar[list] = ["3"] + matches_matcher: ClassVar = MatchesAny(DocTestMatches("1"), DocTestMatches("2")) + matches_matches: ClassVar = ["1", "2"] + matches_mismatches: ClassVar = ["3"] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ( "MatchesAny(DocTestMatches('1\\n'), DocTestMatches('2\\n'))", MatchesAny(DocTestMatches("1"), DocTestMatches("2")), ), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( """Differences: [ Expected: @@ -139,18 +141,18 @@ class TestMatchersAnyInterface(TestCase, TestMatchersInterface): class TestMatchesAllInterface(TestCase, TestMatchersInterface): - matches_matcher = MatchesAll(NotEquals(1), NotEquals(2)) - matches_matches: ClassVar[list] = [3, 4] - matches_mismatches: ClassVar[list] = [1, 2] + matches_matcher: ClassVar = MatchesAll(NotEquals(1), NotEquals(2)) + matches_matches: ClassVar = [3, 4] + matches_mismatches: ClassVar = [1, 2] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ( "MatchesAll(NotEquals(1), NotEquals(2))", MatchesAll(NotEquals(1), NotEquals(2)), ) ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( """Differences: [ 1 == 1 @@ -167,15 +169,15 @@ class TestMatchesAllInterface(TestCase, TestMatchersInterface): class TestAnnotate(TestCase, TestMatchersInterface): - matches_matcher = Annotate("foo", Equals(1)) - matches_matches: ClassVar[list] = [1] - matches_mismatches: ClassVar[list] = [2] + matches_matcher: ClassVar = Annotate("foo", Equals(1)) + matches_matches: ClassVar = [1] + matches_mismatches: ClassVar = [2] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("Annotate('foo', Equals(1))", Annotate("foo", Equals(1))) ] - describe_examples: ClassVar[list] = [("2 != 1: foo", 2, Annotate("foo", Equals(1)))] + describe_examples: ClassVar = [("2 != 1: foo", 2, Annotate("foo", Equals(1)))] def test_if_message_no_message(self): # Annotate.if_message returns the given matcher if there is no @@ -205,16 +207,16 @@ def test_forwards_details(self): class TestNotInterface(TestCase, TestMatchersInterface): - matches_matcher = Not(Equals(1)) - matches_matches: ClassVar[list] = [2] - matches_mismatches: ClassVar[list] = [1] + matches_matcher: ClassVar = Not(Equals(1)) + matches_matches: ClassVar = [2] + matches_mismatches: ClassVar = [1] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ("Not(Equals(1))", Not(Equals(1))), ("Not(Equals('1'))", Not(Equals("1"))), ] - describe_examples: ClassVar[list] = [("1 matches Equals(1)", 1, Not(Equals(1)))] + describe_examples: ClassVar = [("1 matches Equals(1)", 1, Not(Equals(1)))] def is_even(x): @@ -222,18 +224,18 @@ def is_even(x): class TestMatchesPredicate(TestCase, TestMatchersInterface): - matches_matcher = MatchesPredicate(is_even, "%s is not even") - matches_matches: ClassVar[list] = [2, 4, 6, 8] - matches_mismatches: ClassVar[list] = [3, 5, 7, 9] + matches_matcher: ClassVar = MatchesPredicate(is_even, "%s is not even") + matches_matches: ClassVar = [2, 4, 6, 8] + matches_mismatches: ClassVar = [3, 5, 7, 9] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ( "MatchesPredicate({!r}, {!r})".format(is_even, "%s is not even"), MatchesPredicate(is_even, "%s is not even"), ), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ("7 is not even", 7, MatchesPredicate(is_even, "%s is not even")), ] @@ -243,13 +245,13 @@ def between(x, low, high): class TestMatchesPredicateWithParams(TestCase, TestMatchersInterface): - matches_matcher = MatchesPredicateWithParams( + matches_matcher: ClassVar = MatchesPredicateWithParams( between, "{0} is not between {1} and {2}" )(1, 9) - matches_matches: ClassVar[list] = [2, 4, 6, 8] - matches_mismatches: ClassVar[list] = [0, 1, 9, 10] + matches_matches: ClassVar = [2, 4, 6, 8] + matches_mismatches: ClassVar = [0, 1, 9, 10] - str_examples: ClassVar[list] = [ + str_examples: ClassVar = [ ( "MatchesPredicateWithParams({!r}, {!r})({})".format( between, "{0} is not between {1} and {2}", "1, 2" @@ -264,7 +266,7 @@ class TestMatchesPredicateWithParams(TestCase, TestMatchersInterface): ), ] - describe_examples: ClassVar[list] = [ + describe_examples: ClassVar = [ ( "1 is not between 2 and 3", 1, diff --git a/testtools/tests/matchers/test_warnings.py b/testtools/tests/matchers/test_warnings.py index 2a640db2..e8bd9998 100644 --- a/testtools/tests/matchers/test_warnings.py +++ b/testtools/tests/matchers/test_warnings.py @@ -33,15 +33,15 @@ class TestWarningMessageCategoryTypeInterface(TestCase, TestMatchersInterface): In particular matching the ``category_type``. """ - matches_matcher = WarningMessage(category_type=DeprecationWarning) + matches_matcher: ClassVar = WarningMessage(category_type=DeprecationWarning) warning_foo = make_warning_message("foo", DeprecationWarning) warning_bar = make_warning_message("bar", SyntaxWarning) warning_base = make_warning_message("base", Warning) - matches_matches: ClassVar[list] = [warning_foo] - matches_mismatches: ClassVar[list] = [warning_bar, warning_base] + matches_matches: ClassVar = [warning_foo] + matches_mismatches: ClassVar = [warning_bar, warning_base] - str_examples: ClassVar[list] = [] - describe_examples: ClassVar[list] = [] + str_examples: ClassVar = [] + describe_examples: ClassVar = [] class TestWarningMessageMessageInterface(TestCase, TestMatchersInterface): @@ -50,16 +50,16 @@ class TestWarningMessageMessageInterface(TestCase, TestMatchersInterface): In particular matching the ``message``. """ - matches_matcher = WarningMessage( + matches_matcher: ClassVar = WarningMessage( category_type=DeprecationWarning, message=Equals("foo") ) warning_foo = make_warning_message("foo", DeprecationWarning) warning_bar = make_warning_message("bar", DeprecationWarning) - matches_matches: ClassVar[list] = [warning_foo] - matches_mismatches: ClassVar[list] = [warning_bar] + matches_matches: ClassVar = [warning_foo] + matches_mismatches: ClassVar = [warning_bar] - str_examples: ClassVar[list] = [] - describe_examples: ClassVar[list] = [] + str_examples: ClassVar = [] + describe_examples: ClassVar = [] class TestWarningMessageFilenameInterface(TestCase, TestMatchersInterface): @@ -68,16 +68,16 @@ class TestWarningMessageFilenameInterface(TestCase, TestMatchersInterface): In particular matching the ``filename``. """ - matches_matcher = WarningMessage( + matches_matcher: ClassVar = WarningMessage( category_type=DeprecationWarning, filename=Equals("a") ) warning_foo = make_warning_message("foo", DeprecationWarning, filename="a") warning_bar = make_warning_message("bar", DeprecationWarning, filename="b") - matches_matches: ClassVar[list] = [warning_foo] - matches_mismatches: ClassVar[list] = [warning_bar] + matches_matches: ClassVar = [warning_foo] + matches_mismatches: ClassVar = [warning_bar] - str_examples: ClassVar[list] = [] - describe_examples: ClassVar[list] = [] + str_examples: ClassVar = [] + describe_examples: ClassVar = [] class TestWarningMessageLineNumberInterface(TestCase, TestMatchersInterface): @@ -86,16 +86,16 @@ class TestWarningMessageLineNumberInterface(TestCase, TestMatchersInterface): In particular matching the ``lineno``. """ - matches_matcher = WarningMessage( + matches_matcher: ClassVar = WarningMessage( category_type=DeprecationWarning, lineno=Equals(42) ) warning_foo = make_warning_message("foo", DeprecationWarning, lineno=42) warning_bar = make_warning_message("bar", DeprecationWarning, lineno=21) - matches_matches: ClassVar[list] = [warning_foo] - matches_mismatches: ClassVar[list] = [warning_bar] + matches_matches: ClassVar = [warning_foo] + matches_mismatches: ClassVar = [warning_bar] - str_examples: ClassVar[list] = [] - describe_examples: ClassVar[list] = [] + str_examples: ClassVar = [] + describe_examples: ClassVar = [] class TestWarningMessageLineInterface(TestCase, TestMatchersInterface): @@ -104,14 +104,16 @@ class TestWarningMessageLineInterface(TestCase, TestMatchersInterface): In particular matching the ``line``. """ - matches_matcher = WarningMessage(category_type=DeprecationWarning, line=Equals("x")) + matches_matcher: ClassVar = WarningMessage( + category_type=DeprecationWarning, line=Equals("x") + ) warning_foo = make_warning_message("foo", DeprecationWarning, line="x") warning_bar = make_warning_message("bar", DeprecationWarning, line="y") - matches_matches: ClassVar[list] = [warning_foo] - matches_mismatches: ClassVar[list] = [warning_bar] + matches_matches: ClassVar = [warning_foo] + matches_mismatches: ClassVar = [warning_bar] - str_examples: ClassVar[list] = [] - describe_examples: ClassVar[list] = [] + str_examples: ClassVar = [] + describe_examples: ClassVar = [] class TestWarningsInterface(TestCase, TestMatchersInterface): @@ -120,19 +122,20 @@ class TestWarningsInterface(TestCase, TestMatchersInterface): Specifically without the optional argument. """ - matches_matcher = Warnings() + matches_matcher: ClassVar = Warnings() + @staticmethod def old_func(): warnings.warn("old_func is deprecated", DeprecationWarning, 2) - matches_matches: ClassVar[list] = [old_func] - matches_mismatches: ClassVar[list] = [lambda: None] + matches_matches: ClassVar = [old_func] + matches_mismatches: ClassVar = [lambda: None] # Tricky to get function objects to render constantly, and the interfaces # helper uses assertEqual rather than (for instance) DocTestMatches. - str_examples: ClassVar[list] = [] + str_examples: ClassVar = [] - describe_examples: ClassVar[list] = [] + describe_examples: ClassVar = [] class TestWarningsMatcherInterface(TestCase, TestMatchersInterface): @@ -141,23 +144,25 @@ class TestWarningsMatcherInterface(TestCase, TestMatchersInterface): Specifically with the optional matcher argument. """ - matches_matcher = Warnings( + matches_matcher: ClassVar = Warnings( warnings_matcher=MatchesListwise( [MatchesStructure(message=AfterPreprocessing(str, Contains("old_func")))] ) ) + @staticmethod def old_func(): warnings.warn("old_func is deprecated", DeprecationWarning, 2) + @staticmethod def older_func(): warnings.warn("older_func is deprecated", DeprecationWarning, 2) - matches_matches: ClassVar[list] = [old_func] - matches_mismatches: ClassVar[list] = [lambda: None, older_func] + matches_matches: ClassVar = [old_func] + matches_mismatches: ClassVar = [lambda: None, older_func] - str_examples: ClassVar[list] = [] - describe_examples: ClassVar[list] = [] + str_examples: ClassVar = [] + describe_examples: ClassVar = [] class TestWarningsMatcherNoWarningsInterface(TestCase, TestMatchersInterface): @@ -167,19 +172,21 @@ class TestWarningsMatcherNoWarningsInterface(TestCase, TestMatchersInterface): warnings. """ - matches_matcher = Warnings(warnings_matcher=HasLength(0)) + matches_matcher: ClassVar = Warnings(warnings_matcher=HasLength(0)) + @staticmethod def nowarning_func(): pass + @staticmethod def warning_func(): warnings.warn("warning_func is deprecated", DeprecationWarning, 2) - matches_matches: ClassVar[list] = [nowarning_func] - matches_mismatches: ClassVar[list] = [warning_func] + matches_matches: ClassVar = [nowarning_func] + matches_mismatches: ClassVar = [warning_func] - str_examples: ClassVar[list] = [] - describe_examples: ClassVar[list] = [] + str_examples: ClassVar = [] + describe_examples: ClassVar = [] class TestWarningMessage(TestCase): diff --git a/testtools/tests/test_assert_that.py b/testtools/tests/test_assert_that.py index 84fae077..b33d0944 100644 --- a/testtools/tests/test_assert_that.py +++ b/testtools/tests/test_assert_that.py @@ -1,4 +1,5 @@ from doctest import ELLIPSIS +from typing import Any, Callable from testtools import ( TestCase, @@ -19,6 +20,12 @@ class AssertThatTests: """A mixin containing shared tests for assertThat and assert_that.""" + # These are provided by TestCase when mixed in + assertRaises: Callable[..., Any] + assertEqual: Callable[..., Any] + assertFalse: Callable[..., Any] + failureException: Any # Inherited from TestCase + def assert_that_callable(self, *args, **kwargs): raise NotImplementedError @@ -54,12 +61,15 @@ def match(self, thing): return Mismatch(thing) def __str__(self): - calls.append(("__str__",)) + calls.append(("__str__", None)) return "a description" - class Test(type(self)): + # Create a test class that inherits from the actual test class + test_self = self + + class Test(test_self.__class__): # type: ignore[misc,name-defined] def test(self): - self.assert_that_callable("foo", Matcher()) + test_self.assert_that_callable("foo", Matcher()) result = Test("test").run() self.assertEqual( diff --git a/testtools/tests/test_compat.py b/testtools/tests/test_compat.py index d6ac9812..5397935a 100644 --- a/testtools/tests/test_compat.py +++ b/testtools/tests/test_compat.py @@ -25,6 +25,7 @@ class _FakeOutputStream: def __init__(self): self.writelog = [] + self.encoding = None # Optional encoding attribute def write(self, obj): self.writelog.append(obj) diff --git a/testtools/tests/test_content.py b/testtools/tests/test_content.py index d30bf5eb..85fb85a2 100644 --- a/testtools/tests/test_content.py +++ b/testtools/tests/test_content.py @@ -4,6 +4,7 @@ import os import tempfile import unittest +from typing import Any from testtools import TestCase from testtools.compat import ( @@ -192,7 +193,7 @@ def test_text_content_raises_TypeError_when_passed_bytes(self): self.assertRaises(TypeError, text_content, data) def test_text_content_raises_TypeError_when_passed_non_text(self): - bad_values = (None, list(), dict(), 42, 1.23) + bad_values: tuple[Any, ...] = (None, list(), dict(), 42, 1.23) for value in bad_values: self.assertThat( lambda: text_content(value), @@ -266,7 +267,7 @@ def test___init___sets_ivars(self): ) self.assertEqual(content_type, content.content_type) result = unittest.TestResult() - expected = result._exc_info_to_string(an_exc_info, self) + expected = result._exc_info_to_string(an_exc_info, self) # type: ignore[attr-defined] self.assertEqual(expected, "".join(list(content.iter_text()))) diff --git a/testtools/tests/test_fixturesupport.py b/testtools/tests/test_fixturesupport.py index a65ce70c..b4e4863c 100644 --- a/testtools/tests/test_fixturesupport.py +++ b/testtools/tests/test_fixturesupport.py @@ -8,7 +8,6 @@ content_type, ) from testtools.compat import _b -from testtools.helpers import try_import from testtools.matchers import ( Contains, Equals, @@ -17,8 +16,15 @@ ExtendedTestResult, ) -fixtures = try_import("fixtures") -LoggingFixture = try_import("fixtures.tests.helpers.LoggingFixture") +try: + import fixtures +except ImportError: + fixtures = None + +try: + from fixtures.tests.helpers import LoggingFixture +except ImportError: + LoggingFixture = None class TestFixtureSupport(TestCase): @@ -58,6 +64,8 @@ def test_foo(self): self.assertEqual(["called"], calls) def test_useFixture_details_captured(self): + assert fixtures is not None # Checked in setUp + class DetailsFixture(fixtures.Fixture): def setUp(self): fixtures.Fixture.setUp(self) @@ -92,6 +100,8 @@ def test_foo(self): ) def test_useFixture_multiple_details_captured(self): + assert fixtures is not None # Checked in setUp + class DetailsFixture(fixtures.Fixture): def setUp(self): fixtures.Fixture.setUp(self) @@ -115,6 +125,9 @@ def test_foo(self): def test_useFixture_details_captured_from_setUp(self): # Details added during fixture set-up are gathered even if setUp() # fails with an exception. + assert fixtures is not None # Checked in setUp + assert fixtures is not None # Checked in setUp + class BrokenFixture(fixtures.Fixture): def setUp(self): fixtures.Fixture.setUp(self) @@ -138,6 +151,8 @@ def test_useFixture_details_captured_from__setUp(self): # Newer Fixtures deprecates setUp() in favour of _setUp(). # https://bugs.launchpad.net/testtools/+bug/1469759 reports that # this is broken when gathering details from a broken _setUp(). + assert fixtures is not None # Checked in setUp + class BrokenFixture(fixtures.Fixture): def _setUp(self): fixtures.Fixture._setUp(self) @@ -169,6 +184,8 @@ def test_useFixture_original_exception_raised_if_gather_details_fails(self): # In bug #1368440 it was reported that when a fixture fails setUp # and gather_details errors on it, then the original exception that # failed is not reported. + assert fixtures is not None # Checked in setUp + class BrokenFixture(fixtures.Fixture): def getDetails(self): raise AttributeError("getDetails broke") diff --git a/testtools/tests/test_monkey.py b/testtools/tests/test_monkey.py index cbf68d4b..f1add9ef 100644 --- a/testtools/tests/test_monkey.py +++ b/testtools/tests/test_monkey.py @@ -57,7 +57,7 @@ def test_patch_non_existing(self): # the patch. self.monkey_patcher.add_patch(self.test_object, "doesntexist", "value") self.monkey_patcher.patch() - self.assertEqual(self.test_object.doesntexist, "value") + self.assertEqual(self.test_object.doesntexist, "value") # type: ignore[attr-defined] def test_restore_non_existing(self): # Restoring a value that didn't exist before the patch deletes the diff --git a/testtools/tests/test_run.py b/testtools/tests/test_run.py index e3965644..dac640f9 100644 --- a/testtools/tests/test_run.py +++ b/testtools/tests/test_run.py @@ -12,15 +12,21 @@ import testtools from testtools import TestCase, run, skipUnless from testtools.compat import _b -from testtools.helpers import try_import from testtools.matchers import ( Contains, DocTestMatches, MatchesRegex, ) -fixtures = try_import("fixtures") -testresources = try_import("testresources") +try: + import fixtures +except ImportError: + fixtures = None + +try: + import testresources +except ImportError: + testresources = None if fixtures: @@ -165,7 +171,7 @@ def test_run_custom_list(self): tests = [] class CaptureList(run.TestToolsTestRunner): - def list(self, test): + def list(self, test, loader=None): tests.append( {case.id() for case in testtools.testsuite.iterate_tests(test)} ) @@ -242,7 +248,7 @@ def test_run_list_failed_import(self): broken = self.useFixture(SampleTestFixture(broken=True)) out = io.StringIO() # XXX: http://bugs.python.org/issue22811 - unittest.defaultTestLoader._top_level_dir = None + unittest.defaultTestLoader._top_level_dir = None # type: ignore[attr-defined] exc = self.assertRaises( SystemExit, run.main, @@ -446,7 +452,7 @@ def test_issue_16662(self): pkg = self.useFixture(SampleLoadTestsPackage()) out = io.StringIO() # XXX: http://bugs.python.org/issue22811 - unittest.defaultTestLoader._top_level_dir = None + unittest.defaultTestLoader._top_level_dir = None # type: ignore[attr-defined] self.assertEqual( None, run.main(["prog", "discover", "-l", pkg.package.base], out) ) diff --git a/testtools/tests/test_runtest.py b/testtools/tests/test_runtest.py index a10b0bd8..78f4fe81 100644 --- a/testtools/tests/test_runtest.py +++ b/testtools/tests/test_runtest.py @@ -2,6 +2,8 @@ """Tests for the RunTest single test execution logic.""" +from typing import Any + from testtools import ( ExtendedToOriginalDecorator, RunTest, @@ -57,7 +59,12 @@ def _run_test_method(self, result): def test_run_no_result_manages_new_result(self): log = [] - run = RunTest(self.make_case(), lambda x: log.append(x) or x) + + def capture_and_return(x): + log.append(x) + return x + + run = RunTest(self.make_case(), capture_and_return) result = run.run() self.assertIsInstance(result.decorated, TestResult) @@ -65,7 +72,7 @@ def test__run_core_called(self): case = self.make_case() log = [] run = RunTest(case, lambda x: x) - run._run_core = lambda: log.append("foo") + run._run_core = lambda: log.append("foo") # type: ignore[method-assign] run.run() self.assertEqual(["foo"], log) @@ -121,7 +128,7 @@ def test__run_user_can_catch_Exception(self): def raises(): raise e - log = [] + log: list[Any] = [] run = RunTest(case, [(Exception, None)]) run.result = ExtendedTestResult() status = run._run_user(raises) @@ -250,7 +257,7 @@ def inner(): raise Exception("foo") run = RunTest(case, lambda x: x) - run._run_core = inner + run._run_core = inner # type: ignore[method-assign] self.assertThat( lambda: run.run(result), Raises(MatchesException(Exception("foo"))) ) diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index b4d41e59..b7e8be8b 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -7,6 +7,7 @@ import unittest from doctest import ELLIPSIS from pprint import pformat +from typing import Any from testtools import ( DecorateTestCaseResult, @@ -129,7 +130,7 @@ def test_str_is_id(self): def test_runs_as_success(self): # When run, a PlaceHolder test records a success. test = self.makePlaceHolder() - log = [] + log: list[tuple[str, ...]] = [] test.run(LoggingResult(log)) self.assertEqual( [ @@ -178,9 +179,9 @@ def test_supplies_timestamps(self): def test_call_is_run(self): # A PlaceHolder can be called, in which case it behaves like run. test = self.makePlaceHolder() - run_log = [] + run_log: list[tuple[str, ...]] = [] test.run(LoggingResult(run_log)) - call_log = [] + call_log: list[tuple[str, ...]] = [] test(LoggingResult(call_log)) self.assertEqual(run_log, call_log) @@ -274,9 +275,9 @@ def test_runs_as_error(self): def test_call_is_run(self): # A PlaceHolder can be called, in which case it behaves like run. test = self.makePlaceHolder() - run_log = [] + run_log: list[tuple[str, ...]] = [] test.run(LoggingResult(run_log)) - call_log = [] + call_log: list[tuple[str, ...]] = [] test(LoggingResult(call_log)) self.assertEqual(run_log, call_log) @@ -676,7 +677,7 @@ def match(self, foo): self.assertThat("foo", Matcher()) def test_assertThat_mismatch_raises_description(self): - calls = [] + calls: list[tuple[str, ...]] = [] class Mismatch: def __init__(self, thing): @@ -895,7 +896,7 @@ class TestAddCleanup(TestCase): def test_cleanup_run_after_tearDown(self): # Cleanup functions added with 'addCleanup' are called after tearDown # runs. - log = [] + log: list[str] = [] test = make_test_case( self.getUniqueString(), set_up=lambda _: log.append("setUp"), @@ -910,7 +911,7 @@ def test_add_cleanup_called_if_setUp_fails(self): # Cleanup functions added with 'addCleanup' are called even if setUp # fails. Note that tearDown has a different behavior: it is only # called when setUp succeeds. - log = [] + log: list[str] = [] def broken_set_up(ignored): log.append("brokenSetUp") @@ -938,7 +939,7 @@ def test_addCleanup_called_in_reverse_order(self): # # When this happens, we generally want to clean up the second resource # before the first one, since the second depends on the first. - log = [] + log: list[str] = [] test = make_test_case( self.getUniqueString(), set_up=lambda _: log.append("setUp"), @@ -956,7 +957,7 @@ def test_addCleanup_called_in_reverse_order(self): def test_tearDown_runs_on_cleanup_failure(self): # tearDown runs even if a cleanup function fails. - log = [] + log: list[str] = [] test = make_test_case( self.getUniqueString(), set_up=lambda _: log.append("setUp"), @@ -969,7 +970,7 @@ def test_tearDown_runs_on_cleanup_failure(self): def test_cleanups_continue_running_after_error(self): # All cleanups are always run, even if one or two of them fail. - log = [] + log: list[str] = [] test = make_test_case( self.getUniqueString(), set_up=lambda _: log.append("setUp"), @@ -990,7 +991,7 @@ def test_error_in_cleanups_are_captured(self): # If a cleanup raises an error, we want to record it and fail the the # test, even though we go on to run other cleanups. test = make_test_case(self.getUniqueString(), cleanups=[lambda _: 1 / 0]) - log = [] + log: list[tuple[str, ...]] = [] test.run(ExtendedTestResult(log)) self.assertThat( log, @@ -1036,7 +1037,7 @@ def raise_many(ignored): raise MultipleExceptions(exc_info1, exc_info2) test = make_test_case(self.getUniqueString(), cleanups=[raise_many]) - log = [] + log: list[tuple[str, ...]] = [] test.run(ExtendedTestResult(log)) self.assertThat( log, @@ -1077,7 +1078,7 @@ def test_multipleCleanupErrorsReported(self): lambda _: 1 / 0, ], ) - log = [] + log: list[tuple[str, ...]] = [] test.run(ExtendedTestResult(log)) self.assertThat( log, @@ -1120,7 +1121,7 @@ def test_multipleErrorsCoreAndCleanupReported(self): lambda _: 1 / 0, ], ) - log = [] + log: list[tuple[str, ...]] = [] test.run(ExtendedTestResult(log)) self.assertThat( log, @@ -1441,7 +1442,7 @@ def test_addSkip_different_exception(self): # No traceback is included if the skip exception is changed and a skip # is raised. class Case(TestCase): - skipException = ValueError + skipException = ValueError # type: ignore[assignment] def test(this): this.addDetail("foo", self.get_content()) @@ -1652,6 +1653,11 @@ class TestRunTwiceNondeterministic(TestCase): scenarios = nondeterministic_sample_cases_scenarios + # These are provided by scenarios + case: Any + expected_first_result: Any + expected_second_result: Any + def test_runTwice(self): test = self.case first_result = ExtendedTestResult() @@ -1682,7 +1688,7 @@ def test_can_use_skipTest(self): def test_skip_without_reason_works(self): class Test(TestCase): def test(self): - raise self.skipException() + raise self.skipException() # type: ignore[call-arg] case = Test("test") result = ExtendedTestResult() @@ -1699,7 +1705,7 @@ def setUp(self): def test_that_passes(self): pass - calls = [] + calls: list[tuple[str, ...]] = [] result = LoggingResult(calls) test = TestThatRaisesInSetUp("test_that_passes") test.run(result) @@ -1718,7 +1724,7 @@ class SkippingTest(TestCase): def test_that_raises_skipException(self): self.skipTest("skipping this test") - events = [] + events: list[tuple[str, ...]] = [] result = Python3TestResult(events) test = SkippingTest("test_that_raises_skipException") test.run(result) @@ -1734,12 +1740,12 @@ def test_that_raises_skipException(self): def test_different_skipException_in_test_method_calls_result_addSkip(self): class SkippingTest(TestCase): - skipException = ValueError + skipException = ValueError # type: ignore[assignment] def test_that_raises_skipException(self): self.skipTest("skipping this test") - events = [] + events: list[tuple[str, ...]] = [] result = Python3TestResult(events) test = SkippingTest("test_that_raises_skipException") test.run(result) @@ -1762,7 +1768,7 @@ def setUp(self): def test_that_raises_skipException(self): pass - events = [] + events: list[tuple[str, ...]] = [] result = Python3TestResult(events) test = SkippingTest("test_that_raises_skipException") test.run(result) @@ -1773,7 +1779,7 @@ class SkippingTest(TestCase): def test_that_raises_skipException(self): raise self.skipException("skipping this test") - events = [] + events: list[tuple[str, ...]] = [] result = Python3TestResult(events) test = SkippingTest("test_that_raises_skipException") test.run(result) @@ -1785,7 +1791,7 @@ class SkippingTest(TestCase): def test_that_is_decorated_with_skip(self): self.fail() - events = [] + events: list[tuple[str, ...]] = [] result = Python3TestResult(events) test = SkippingTest("test_that_is_decorated_with_skip") test.run(result) @@ -1797,7 +1803,7 @@ class SkippingTest(TestCase): def test_that_is_decorated_with_skipIf(self): self.fail() - events = [] + events: list[tuple[str, ...]] = [] result = Python3TestResult(events) test = SkippingTest("test_that_is_decorated_with_skipIf") test.run(result) @@ -1809,7 +1815,7 @@ class SkippingTest(TestCase): def test_that_is_decorated_with_skipUnless(self): self.fail() - events = [] + events: list[tuple[str, ...]] = [] result = Python3TestResult(events) test = SkippingTest("test_that_is_decorated_with_skipUnless") test.run(result) @@ -1825,13 +1831,13 @@ class SkippingTest(TestCase): class NotSkippingTest(TestCase): test_no_skip = skipIf(False, "skipping this test")(shared) - events = [] + events: list[tuple[str, ...]] = [] result = Python3TestResult(events) test = SkippingTest("test_skip") test.run(result) self.assertEqual("addSkip", events[1][0]) - events2 = [] + events2: list[tuple[str, ...]] = [] result2 = Python3TestResult(events2) test2 = NotSkippingTest("test_no_skip") test2.run(result2) @@ -1843,7 +1849,7 @@ class SkippingTest(TestCase): def test_that_is_decorated_with_skip(self): self.fail() - events = [] + events: list[tuple[str, ...]] = [] result = Python3TestResult(events) try: test = SkippingTest("test_that_is_decorated_with_skip") @@ -1858,7 +1864,7 @@ class SkippingTest(TestCase): def test_that_is_decorated_with_skipIf(self): self.fail() - events = [] + events: list[tuple[str, ...]] = [] result = Python3TestResult(events) try: test = SkippingTest("test_that_is_decorated_with_skipIf") @@ -1873,7 +1879,7 @@ class SkippingTest(TestCase): def test_that_is_decorated_with_skipUnless(self): self.fail() - events = [] + events: list[tuple[str, ...]] = [] result = Python3TestResult(events) try: test = SkippingTest("test_that_is_decorated_with_skipUnless") @@ -1911,10 +1917,10 @@ def test_skipped(self): self.fail() try: - test = SkippingTestCase("test_skipped") + test_case = SkippingTestCase("test_skipped") except unittest.SkipTest: self.fail("SkipTest raised") - self.check_test_does_not_run_setup(test, reason) + self.check_test_does_not_run_setup(test_case, reason) def check_test_does_not_run_setup(self, test, reason): result = test.run() @@ -1955,7 +1961,7 @@ class TestOnException(TestCase): run_test_with = FullStackRunTest def test_default_works(self): - events = [] + events: list[bool] = [] class Case(TestCase): def method(self): @@ -1967,7 +1973,7 @@ def method(self): self.assertThat(events, Equals([True])) def test_added_handler_works(self): - events = [] + events: list[tuple[str, ...]] = [] class Case(TestCase): def method(self): @@ -1979,7 +1985,7 @@ def method(self): self.assertThat(events, Equals([an_exc_info])) def test_handler_that_raises_is_not_caught(self): - events = [] + events: list[tuple[str, ...]] = [] class Case(TestCase): def method(self): @@ -2006,7 +2012,7 @@ def run_test(self, test_body): :return: Whatever ``test_body`` returns. """ - log = [] + log: list[tuple[str, ...]] = [] def wrapper(case): log.append(test_body(case)) @@ -2063,7 +2069,7 @@ def test_patch_nonexistent_attribute(self): # TestCase.patch can be used to patch a non-existent attribute. def test_body(case): case.patch(self, "doesntexist", "patched") - return self.doesntexist + return self.doesntexist # type: ignore[attr-defined] result = self.run_test(test_body) self.assertThat(result, Equals("patched")) @@ -2073,7 +2079,7 @@ def test_restore_nonexistent_attribute(self): # the test run, the attribute is then removed from the object. def test_body(case): case.patch(self, "doesntexist", "patched") - return self.doesntexist + return self.doesntexist # type: ignore[attr-defined] self.run_test(test_body) marker = object() @@ -2279,13 +2285,13 @@ def test_before_after_hooks(self): def test_other_attribute(self): orig = PlaceHolder("foo") - orig.thing = "fred" + orig.thing = "fred" # type: ignore[attr-defined] case = DecorateTestCaseResult(orig, self.make_result) - self.assertEqual("fred", case.thing) + self.assertEqual("fred", case.thing) # type: ignore[attr-defined] self.assertRaises(AttributeError, getattr, case, "other") - case.other = "barbara" - self.assertEqual("barbara", orig.other) - del case.thing + case.other = "barbara" # type: ignore[attr-defined] + self.assertEqual("barbara", orig.other) # type: ignore[attr-defined] + del case.thing # type: ignore[attr-defined] self.assertRaises(AttributeError, getattr, orig, "thing") diff --git a/testtools/tests/test_testresult.py b/testtools/tests/test_testresult.py index eb2f6ba3..ac87b0a5 100644 --- a/testtools/tests/test_testresult.py +++ b/testtools/tests/test_testresult.py @@ -15,6 +15,7 @@ import threading from itertools import chain, combinations from queue import Queue +from typing import Any from unittest import TestSuite from testtools import ( @@ -54,7 +55,6 @@ text_content, ) from testtools.content_type import UTF8_TEXT, ContentType -from testtools.helpers import try_import from testtools.matchers import ( AllMatch, Contains, @@ -86,7 +86,10 @@ run_with_stack_hidden, ) -testresources = try_import("testresources") +try: + import testresources +except ImportError: + testresources = None def _utcfromtimestamp(t): @@ -146,6 +149,11 @@ def make_exception_info(exceptionFactory, *args, **kwargs): class TestControlContract: """Stopping test runs.""" + # These are provided by the class that uses this mixin + makeResult: Any + assertFalse: Any + assertTrue: Any + def test_initially_not_shouldStop(self): # A result is not set to stop initially. result = self.makeResult() @@ -159,6 +167,13 @@ def test_stop_sets_shouldStop(self): class Python3Contract(TestControlContract): + # Inherit requirements from TestControlContract + # These are provided by the class that uses this mixin + makeResult: Any + assertTrue: Any + assertFalse: Any + assertEqual: Any + def test_fresh_result_is_successful(self): # A result is considered successful before any tests are run. result = self.makeResult() @@ -257,6 +272,10 @@ class TagsContract(Python3Contract): See the subunit docs for guidelines on how this is supposed to work. """ + # Inherit requirements from Python3Contract + # These are provided by the class that uses this mixin + assertEqual: Any + def test_no_tags_by_default(self): # Results initially have no tags. result = self.makeResult() @@ -557,6 +576,9 @@ def makeResult(self): class TestStreamResultContract: + # These are provided by the class that uses this mixin + addCleanup: Any + def _make_result(self): raise NotImplementedError(self._make_result) @@ -673,7 +695,7 @@ def _make_result(self): class TestStreamToQueueContract(TestCase, TestStreamResultContract): def _make_result(self): - queue = Queue() + queue: Queue[Any] = Queue() return StreamToQueue(queue, "foo") @@ -930,7 +952,7 @@ def test_discarding(self): class TestStreamToDict(TestCase): def test_hung_test(self): - tests = [] + tests: list[dict[str, Any]] = [] result = StreamToDict(tests.append) result.startTestRun() result.status("foo", "inprogress") @@ -950,7 +972,7 @@ def test_hung_test(self): ) def test_all_terminal_states_reported(self): - tests = [] + tests: list[dict[str, Any]] = [] result = StreamToDict(tests.append) result.startTestRun() result.status("success", "success") @@ -968,7 +990,7 @@ def test_all_terminal_states_reported(self): self.assertThat(tests, HasLength(6)) def test_files_reported(self): - tests = [] + tests: list[dict[str, Any]] = [] result = StreamToDict(tests.append) result.startTestRun() result.status( @@ -1000,7 +1022,7 @@ def test_files_reported(self): def test_bad_mime(self): # Testtools was making bad mime types, this tests that the specific # corruption is catered for. - tests = [] + tests: list[dict[str, Any]] = [] result = StreamToDict(tests.append) result.startTestRun() result.status( @@ -1020,7 +1042,7 @@ def test_bad_mime(self): ) def test_timestamps(self): - tests = [] + tests: list[dict[str, Any]] = [] result = StreamToDict(tests.append) result.startTestRun() result.status(test_id="foo", test_status="inprogress", timestamp="A") @@ -1032,7 +1054,7 @@ def test_timestamps(self): self.assertEqual(["C", None], tests[1]["timestamps"]) def test_files_skipped(self): - tests = [] + tests: list[dict[str, Any]] = [] result = StreamToDict(tests.append) result.startTestRun() result.status( @@ -1475,9 +1497,9 @@ class Module: now = datetime.datetime.now(utc) stubdatetime = Module() - stubdatetime.datetime = Module() - stubdatetime.datetime.now = lambda tz: now - testresult.real.datetime = stubdatetime + stubdatetime.datetime = Module() # type: ignore[attr-defined] + stubdatetime.datetime.now = lambda tz: now # type: ignore[attr-defined] + testresult.real.datetime = stubdatetime # type: ignore[assignment] # Calling _now() looks up the time. self.assertEqual(now, result._now()) then = now + datetime.timedelta(0, 1) @@ -1927,7 +1949,7 @@ class TestThreadSafeForwardingResult(TestCase): """Tests for `TestThreadSafeForwardingResult`.""" def make_results(self, n): - events = [] + events: list[tuple[str, ...]] = [] target = LoggingResult(events) semaphore = threading.Semaphore(1) return [ThreadsafeForwardingResult(target, semaphore) for i in range(n)], events @@ -2199,7 +2221,7 @@ def test_merge_unseen_gone_tag(self): # If an incoming "gone" tag isn't currently tagged one way or the # other, add it to the "gone" tags. current_tags = {"present"}, {"missing"} - changing_tags = set(), {"going"} + changing_tags: tuple[set[str], set[str]] = set(), {"going"} expected = {"present"}, {"missing", "going"} self.assertEqual(expected, _merge_tags(current_tags, changing_tags)) @@ -2207,13 +2229,13 @@ def test_merge_incoming_gone_tag_with_current_new_tag(self): # If one of the incoming "gone" tags is one of the existing "new" # tags, then it overrides the "new" tag, leaving it marked as "gone". current_tags = {"present", "going"}, {"missing"} - changing_tags = set(), {"going"} + changing_tags: tuple[set[str], set[str]] = set(), {"going"} expected = {"present"}, {"missing", "going"} self.assertEqual(expected, _merge_tags(current_tags, changing_tags)) def test_merge_unseen_new_tag(self): current_tags = {"present"}, {"missing"} - changing_tags = {"coming"}, set() + changing_tags: tuple[set[str], set[str]] = {"coming"}, set() expected = {"coming", "present"}, {"missing"} self.assertEqual(expected, _merge_tags(current_tags, changing_tags)) @@ -2221,7 +2243,7 @@ def test_merge_incoming_new_tag_with_current_gone_tag(self): # If one of the incoming "new" tags is currently marked as "gone", # then it overrides the "gone" tag, leaving it marked as "new". current_tags = {"present"}, {"coming", "missing"} - changing_tags = {"coming"}, set() + changing_tags: tuple[set[str], set[str]] = {"coming"}, set() expected = {"coming", "present"}, {"missing"} self.assertEqual(expected, _merge_tags(current_tags, changing_tags)) @@ -2449,7 +2471,7 @@ def test_add_rule_do_start_stop_run_after_startTestRun(self): class TestStreamToQueue(TestCase): def make_result(self): - queue = Queue() + queue: Queue[Any] = Queue() return queue, StreamToQueue(queue, "foo") def test_status(self): @@ -2853,7 +2875,7 @@ def foo(self): bar = 1 - self.result = OtherExtendedResult() + self.result = OtherExtendedResult() # type: ignore[assignment] self.make_converter() self.assertEqual(1, self.converter.bar) self.assertEqual(2, self.converter.foo()) @@ -3273,7 +3295,7 @@ def setUp(self): self.log = [] self.result = TestByTestResult(self.on_test) now = iter(range(5)) - self.result._now = lambda: next(now) + self.result._now = lambda: next(now) # type: ignore[method-assign] def assertCalled(self, **kwargs): defaults = { diff --git a/testtools/tests/test_testsuite.py b/testtools/tests/test_testsuite.py index 220e70f1..1c82d847 100644 --- a/testtools/tests/test_testsuite.py +++ b/testtools/tests/test_testsuite.py @@ -5,6 +5,7 @@ import doctest import unittest from pprint import pformat +from typing import Union from testtools import ( ConcurrentStreamTestSuite, @@ -14,13 +15,15 @@ TestCase, iterate_tests, ) -from testtools.helpers import try_import from testtools.matchers import DocTestMatches, Equals from testtools.testresult.doubles import StreamResult as LoggingStream from testtools.tests.helpers import LoggingResult from testtools.testsuite import FixtureSuite, sorted_tests -FunctionFixture = try_import("fixtures.FunctionFixture") +try: + from fixtures import FunctionFixture +except ImportError: + FunctionFixture = None class Sample(TestCase): @@ -48,13 +51,13 @@ def __call__(self): run = __call__ - original_suite = unittest.TestSuite([BrokenTest()]) + original_suite = unittest.TestSuite([BrokenTest()]) # type: ignore[list-item] suite = ConcurrentTestSuite(original_suite, self.split_suite) suite.run(TestByTestResult(on_test)) self.assertEqual([("broken-runner", "error", {"traceback"})], log) def test_trivial(self): - log = [] + log: list[tuple[str, ...]] = [] result = LoggingResult(log) test1 = Sample("test_method1") test2 = Sample("test_method2") @@ -62,11 +65,13 @@ def test_trivial(self): suite = ConcurrentTestSuite(original_suite, self.split_suite) suite.run(result) # log[0] is the timestamp for the first test starting. - test1 = log[1][1] - test2 = log[-1][1] - self.assertIsInstance(test1, Sample) - self.assertIsInstance(test2, Sample) - self.assertNotEqual(test1.id(), test2.id()) + test1_from_log = log[1][1] + test2_from_log = log[-1][1] + self.assertIsInstance(test1_from_log, Sample) + self.assertIsInstance(test2_from_log, Sample) + assert isinstance(test1_from_log, Sample) # For mypy + assert isinstance(test2_from_log, Sample) # For mypy + self.assertNotEqual(test1_from_log.id(), test2_from_log.id()) def test_wrap_result(self): # ConcurrentTestSuite has a hook for wrapping the per-thread result. @@ -76,7 +81,7 @@ def wrap_result(thread_safe_result, thread_number): wrap_log.append((thread_safe_result.result.decorated, thread_number)) return thread_safe_result - result_log = [] + result_log: list[tuple[str, ...]] = [] result = LoggingResult(result_log) test1 = Sample("test_method1") test2 = Sample("test_method2") @@ -315,7 +320,7 @@ def test_notrun(self): # Test discovery uses the default suite from unittest (unless users # deliberately change things, in which case they keep both pieces). suite = unittest.TestSuite([Skips("test_notrun")]) - log = [] + log: list[tuple[str, ...]] = [] result = LoggingResult(log) suite.run(result) self.assertEqual(["addSkip"], [item[0] for item in log]) @@ -334,7 +339,7 @@ def test_simple(self): # Test discovery uses the default suite from unittest (unless users # deliberately change things, in which case they keep both pieces). suite = unittest.TestSuite([Simples("test_simple")]) - log = [] + log: list[tuple[str, ...]] = [] result = LoggingResult(log) suite.run(result) self.assertEqual( @@ -349,7 +354,7 @@ def setUp(self): self.skipTest("Need fixtures") def test_fixture_suite(self): - log = [] + log: list[Union[int, str]] = [] class Sample(TestCase): def test_one(self): @@ -366,7 +371,7 @@ def test_two(self): self.assertEqual(["setUp", 1, 2, "tearDown"], log) def test_fixture_suite_sort(self): - log = [] + log: list[Union[int, str]] = [] class Sample(TestCase): def test_one(self): @@ -391,7 +396,7 @@ class Subclass(unittest.TestSuite): def sort_tests(self): self._tests = sorted_tests(self, True) - input_suite = Subclass([b, a]) + input_suite = Subclass([b, a]) # type: ignore[list-item] suite = sorted_tests(input_suite) self.assertEqual([a, b], list(iterate_tests(suite))) self.assertEqual([input_suite], list(iter(suite))) @@ -403,7 +408,7 @@ def test_custom_suite_without_sort_tests_works(self): class Subclass(unittest.TestSuite): pass - input_suite = Subclass([b, a]) + input_suite = Subclass([b, a]) # type: ignore[list-item] suite = sorted_tests(input_suite) self.assertEqual([b, a], list(iterate_tests(suite))) self.assertEqual([input_suite], list(iter(suite))) @@ -411,14 +416,14 @@ class Subclass(unittest.TestSuite): def test_sorts_simple_suites(self): a = PlaceHolder("a") b = PlaceHolder("b") - suite = sorted_tests(unittest.TestSuite([b, a])) + suite = sorted_tests(unittest.TestSuite([b, a])) # type: ignore[list-item] self.assertEqual([a, b], list(iterate_tests(suite))) def test_duplicate_simple_suites(self): a = PlaceHolder("a") b = PlaceHolder("b") c = PlaceHolder("a") - self.assertRaises(ValueError, sorted_tests, unittest.TestSuite([a, b, c])) + self.assertRaises(ValueError, sorted_tests, unittest.TestSuite([a, b, c])) # type: ignore[list-item] def test_multiple_duplicates(self): # If there are multiple duplicates on a test suite, we report on them @@ -428,7 +433,9 @@ def test_multiple_duplicates(self): c = PlaceHolder("a") d = PlaceHolder("b") error = self.assertRaises( - ValueError, sorted_tests, unittest.TestSuite([a, b, c, d]) + ValueError, + sorted_tests, + unittest.TestSuite([a, b, c, d]), # type: ignore[list-item] ) self.assertThat( str(error), diff --git a/testtools/tests/twistedsupport/_helpers.py b/testtools/tests/twistedsupport/_helpers.py index 4700e851..8238bbb5 100644 --- a/testtools/tests/twistedsupport/_helpers.py +++ b/testtools/tests/twistedsupport/_helpers.py @@ -4,10 +4,19 @@ "NeedsTwistedTestCase", ] +from typing import TYPE_CHECKING, Optional + from testtools import TestCase -from testtools.helpers import try_import -defer = try_import("twisted.internet.defer") +if TYPE_CHECKING: + from types import ModuleType + + defer: Optional[ModuleType] +else: + try: + from twisted.internet import defer + except ImportError: + defer = None class NeedsTwistedTestCase(TestCase): diff --git a/testtools/tests/twistedsupport/test_deferred.py b/testtools/tests/twistedsupport/test_deferred.py index 3d3ab39d..93b585df 100644 --- a/testtools/tests/twistedsupport/test_deferred.py +++ b/testtools/tests/twistedsupport/test_deferred.py @@ -2,7 +2,6 @@ """Tests for testtools._deferred.""" -from testtools.helpers import try_import from testtools.matchers import ( Equals, MatchesException, @@ -11,11 +10,25 @@ from ._helpers import NeedsTwistedTestCase -DeferredNotFired = try_import("testtools.twistedsupport._deferred.DeferredNotFired") -extract_result = try_import("testtools.twistedsupport._deferred.extract_result") +try: + from testtools.twistedsupport._deferred import DeferredNotFired +except ImportError: + DeferredNotFired = None # type: ignore[misc,assignment] -defer = try_import("twisted.internet.defer") -Failure = try_import("twisted.python.failure.Failure") +try: + from testtools.twistedsupport._deferred import extract_result +except ImportError: + extract_result = None # type: ignore[assignment] + +try: + from twisted.internet import defer +except ImportError: + defer = None # type: ignore[assignment] + +try: + from twisted.python.failure import Failure +except ImportError: + Failure = None # type: ignore[misc,assignment] class TestExtractResult(NeedsTwistedTestCase): diff --git a/testtools/tests/twistedsupport/test_matchers.py b/testtools/tests/twistedsupport/test_matchers.py index e292a481..e5aed883 100644 --- a/testtools/tests/twistedsupport/test_matchers.py +++ b/testtools/tests/twistedsupport/test_matchers.py @@ -2,6 +2,8 @@ """Tests for Deferred matchers.""" +from typing import Any + from twisted.internet import defer from twisted.python.failure import Failure @@ -85,9 +87,9 @@ def test_failed_does_not_match(self): def test_success_after_assertion(self): # We can create a Deferred, assert that it hasn't fired, then fire it # and collect the result. - deferred = defer.Deferred() + deferred: defer.Deferred[Any] = defer.Deferred() self.assertThat(deferred, has_no_result()) - results = [] + results: list[Any] = [] deferred.addCallback(results.append) marker = object() deferred.callback(marker) @@ -96,9 +98,9 @@ def test_success_after_assertion(self): def test_failure_after_assertion(self): # We can create a Deferred, assert that it hasn't fired, then fire it # with a failure and collect the result. - deferred = defer.Deferred() + deferred: defer.Deferred[Any] = defer.Deferred() self.assertThat(deferred, has_no_result()) - results = [] + results: list[Any] = [] deferred.addErrback(results.append) fail = make_failure(RuntimeError("arbitrary failure")) deferred.errback(fail) @@ -130,7 +132,7 @@ def test_different_succeeded_result_fails(self): def test_not_fired_fails(self): # A Deferred that has not yet fired fails to match. - deferred = defer.Deferred() + deferred: defer.Deferred[Any] = defer.Deferred() arbitrary_matcher = Is(None) self.assertThat( self.match(arbitrary_matcher, deferred), @@ -143,7 +145,7 @@ def test_not_fired_fails(self): def test_failing_fails(self): # A Deferred that has fired with a failure fails to match. - deferred = defer.Deferred() + deferred: defer.Deferred[Any] = defer.Deferred() fail = make_failure(RuntimeError("arbitrary failure")) deferred.errback(fail) arbitrary_matcher = Is(None) @@ -206,7 +208,7 @@ def test_success_fails(self): def test_no_result_fails(self): # A Deferred that has not fired fails to match. - deferred = defer.Deferred() + deferred: defer.Deferred[Any] = defer.Deferred() matcher = Is(None) # Can be any matcher self.assertThat( self.match(matcher, deferred), diff --git a/testtools/tests/twistedsupport/test_runtest.py b/testtools/tests/twistedsupport/test_runtest.py index be5595aa..e2150af2 100644 --- a/testtools/tests/twistedsupport/test_runtest.py +++ b/testtools/tests/twistedsupport/test_runtest.py @@ -4,14 +4,15 @@ import os import signal -from typing import ClassVar +from typing import Any, ClassVar + +from testscenarios import multiply_scenarios from testtools import ( TestCase, TestResult, skipIf, ) -from testtools.helpers import try_import from testtools.matchers import ( AfterPreprocessing, Contains, @@ -33,156 +34,212 @@ from ._helpers import NeedsTwistedTestCase -DebugTwisted = try_import("testtools.twistedsupport._deferreddebug.DebugTwisted") +try: + from testtools.twistedsupport._deferreddebug import DebugTwisted +except ImportError: + DebugTwisted = None # type: ignore[assignment,misc] -assert_fails_with = try_import("testtools.twistedsupport.assert_fails_with") -AsynchronousDeferredRunTest = try_import( - "testtools.twistedsupport.AsynchronousDeferredRunTest" -) -flush_logged_errors = try_import("testtools.twistedsupport.flush_logged_errors") -SynchronousDeferredRunTest = try_import( - "testtools.twistedsupport.SynchronousDeferredRunTest" -) +try: + from testtools.twistedsupport import assert_fails_with +except ImportError: + assert_fails_with = None # type: ignore[assignment] + +try: + from testtools.twistedsupport import AsynchronousDeferredRunTest +except ImportError: + AsynchronousDeferredRunTest = None # type: ignore[assignment,misc] + +try: + from testtools.twistedsupport import flush_logged_errors +except ImportError: + flush_logged_errors = None # type: ignore[assignment] + +try: + from testtools.twistedsupport import SynchronousDeferredRunTest +except ImportError: + SynchronousDeferredRunTest = None # type: ignore[assignment,misc] + +try: + from twisted.internet import defer +except ImportError: + defer = None # type: ignore[assignment] + +try: + from twisted.python import failure +except ImportError: + failure = None # type: ignore[assignment] + +try: + from twisted.python import log +except ImportError: + log = None # type: ignore[assignment] + +try: + from twisted.internet.base import DelayedCall +except ImportError: + DelayedCall = None # type: ignore[assignment,misc] + +try: + from testtools.twistedsupport._runtest import _get_global_publisher_and_observers +except ImportError: + _get_global_publisher_and_observers = None # type: ignore[assignment] -defer = try_import("twisted.internet.defer") -failure = try_import("twisted.python.failure") -log = try_import("twisted.python.log") -DelayedCall = try_import("twisted.internet.base.DelayedCall") -_get_global_publisher_and_observers = try_import( - "testtools.twistedsupport._runtest._get_global_publisher_and_observers" -) +# Flattened test classes to avoid PyPy compilation crash with nested classes +# Prefixed with _ to avoid pytest discovery -class X: - """Tests that we run as part of our tests, nested to avoid discovery.""" - # XXX: After testing-cabal/testtools#165 lands, fix up all of these to be - # scenario tests for RunTest. +class _XBase(TestCase): + def setUp(self): + super().setUp() + self.calls = ["setUp"] + self.addCleanup(self.calls.append, "clean-up") - class Base(TestCase): - def setUp(self): - super(X.Base, self).setUp() - self.calls = ["setUp"] - self.addCleanup(self.calls.append, "clean-up") + def test_something(self): + self.calls.append("test") - def test_something(self): - self.calls.append("test") + def tearDown(self): + self.calls.append("tearDown") + super().tearDown() - def tearDown(self): - self.calls.append("tearDown") - super(X.Base, self).tearDown() - class BaseExceptionRaised(Base): - expected_calls: ClassVar[list] = ["setUp", "tearDown", "clean-up"] - expected_results: ClassVar[list] = [("addError", SystemExit)] +class _XBaseExceptionRaised(_XBase): + expected_calls: ClassVar[list] = ["setUp", "tearDown", "clean-up"] + expected_results: ClassVar[list] = [("addError", SystemExit)] - def test_something(self): - raise SystemExit(0) + def test_something(self): + raise SystemExit(0) - class ErrorInSetup(Base): - expected_calls: ClassVar[list] = ["setUp", "clean-up"] - expected_results: ClassVar[list] = [("addError", RuntimeError)] - def setUp(self): - super(X.ErrorInSetup, self).setUp() - raise RuntimeError("Error in setUp") +class _XErrorInSetup(_XBase): + expected_calls: ClassVar[list] = ["setUp", "clean-up"] + expected_results: ClassVar[list] = [("addError", RuntimeError)] - class ErrorInTest(Base): - expected_calls: ClassVar[list] = ["setUp", "tearDown", "clean-up"] - expected_results: ClassVar[list] = [("addError", RuntimeError)] + def setUp(self): + super().setUp() + raise RuntimeError("Error in setUp") - def test_something(self): - raise RuntimeError("Error in test") - class FailureInTest(Base): - expected_calls: ClassVar[list] = ["setUp", "tearDown", "clean-up"] - expected_results: ClassVar[list] = [("addFailure", AssertionError)] +class _XErrorInTest(_XBase): + expected_calls: ClassVar[list] = ["setUp", "tearDown", "clean-up"] + expected_results: ClassVar[list] = [("addError", RuntimeError)] - def test_something(self): - self.fail("test failed") + def test_something(self): + raise RuntimeError("Error in test") - class ErrorInTearDown(Base): - expected_calls: ClassVar[list] = ["setUp", "test", "clean-up"] - expected_results: ClassVar[list] = [("addError", RuntimeError)] - def tearDown(self): - raise RuntimeError("Error in tearDown") +class _XFailureInTest(_XBase): + expected_calls: ClassVar[list] = ["setUp", "tearDown", "clean-up"] + expected_results: ClassVar[list] = [("addFailure", AssertionError)] - class ErrorInCleanup(Base): - expected_calls: ClassVar[list] = ["setUp", "test", "tearDown", "clean-up"] - expected_results: ClassVar[list] = [("addError", ZeroDivisionError)] + def test_something(self): + self.fail("test failed") - def test_something(self): - self.calls.append("test") - self.addCleanup(lambda: 1 / 0) - class ExpectThatFailure(Base): - """Calling expectThat with a failing match fails the test.""" +class _XErrorInTearDown(_XBase): + expected_calls: ClassVar[list] = ["setUp", "test", "clean-up"] + expected_results: ClassVar[list] = [("addError", RuntimeError)] - expected_calls: ClassVar[list] = ["setUp", "test", "tearDown", "clean-up"] - expected_results: ClassVar[list] = [("addFailure", AssertionError)] + def tearDown(self): + raise RuntimeError("Error in tearDown") - def test_something(self): - self.calls.append("test") - self.expectThat(object(), Is(object())) - class TestIntegration(NeedsTwistedTestCase): - def assertResultsMatch(self, test, result): - events = list(result._events) - self.assertEqual(("startTest", test), events.pop(0)) - for expected_result in test.expected_results: - result = events.pop(0) - if len(expected_result) == 1: - self.assertEqual((expected_result[0], test), result) - else: - self.assertEqual((expected_result[0], test), result[:2]) - error_type = expected_result[1] - self.assertIn(error_type.__name__, str(result[2])) - self.assertEqual([("stopTest", test)], events) +class _XErrorInCleanup(_XBase): + expected_calls: ClassVar[list] = ["setUp", "test", "tearDown", "clean-up"] + expected_results: ClassVar[list] = [("addError", ZeroDivisionError)] - def test_runner(self): - result = ExtendedTestResult() - test = self.test_factory("test_something", runTest=self.runner) - if self.test_factory is X.BaseExceptionRaised: - self.assertRaises(SystemExit, test.run, result) + def test_something(self): + self.calls.append("test") + self.addCleanup(lambda: 1 / 0) + + +class _XExpectThatFailure(_XBase): + """Calling expectThat with a failing match fails the test.""" + + expected_calls: ClassVar[list] = ["setUp", "test", "tearDown", "clean-up"] + expected_results: ClassVar[list] = [("addFailure", AssertionError)] + + def test_something(self): + self.calls.append("test") + self.expectThat(object(), Is(object())) + + +class _XTestIntegration(NeedsTwistedTestCase): + # These attributes are set dynamically in test generation + test_factory: Any = None + runner: Any = None + + def assertResultsMatch(self, test, result): + events = list(result._events) + self.assertEqual(("startTest", test), events.pop(0)) + for expected_result in test.expected_results: + result = events.pop(0) + if len(expected_result) == 1: + self.assertEqual((expected_result[0], test), result) else: - test.run(result) - self.assertEqual(test.calls, self.test_factory.expected_calls) - self.assertResultsMatch(test, result) - - -def make_integration_tests(): - from unittest import TestSuite - - from testtools import clone_test_with_new_id - - runners = [ - ("RunTest", RunTest), - ("SynchronousDeferredRunTest", SynchronousDeferredRunTest), - ("AsynchronousDeferredRunTest", AsynchronousDeferredRunTest), - ] - - tests = [ - X.BaseExceptionRaised, - X.ErrorInSetup, - X.ErrorInTest, - X.ErrorInTearDown, - X.FailureInTest, - X.ErrorInCleanup, - X.ExpectThatFailure, - ] - base_test = X.TestIntegration("test_runner") - integration_tests = [] - for runner_name, runner in runners: - for test in tests: - new_test = clone_test_with_new_id( - base_test, - f"{base_test.id()}({runner_name}, {test.__name__})", - ) - new_test.test_factory = test - new_test.runner = runner - integration_tests.append(new_test) - return TestSuite(integration_tests) + self.assertEqual((expected_result[0], test), result[:2]) + error_type = expected_result[1] + self.assertIn(error_type.__name__, str(result[2])) + self.assertEqual([("stopTest", test)], events) + + def test_runner(self): + result = ExtendedTestResult() + test = self.test_factory("test_something", runTest=self.runner) + if self.test_factory is _XBaseExceptionRaised: + self.assertRaises(SystemExit, test.run, result) + else: + test.run(result) + self.assertEqual(test.calls, self.test_factory.expected_calls) + self.assertResultsMatch(test, result) + + +class TestRunTestIntegration(NeedsTwistedTestCase): + """Integration tests for different runner and test case combinations.""" + + # These attributes are provided by testscenarios + runner: Any + test_factory: Any + + scenarios = multiply_scenarios( + [ # Runner scenarios + ("RunTest", {"runner": RunTest}), + ("SynchronousDeferredRunTest", {"runner": SynchronousDeferredRunTest}), + ("AsynchronousDeferredRunTest", {"runner": AsynchronousDeferredRunTest}), + ], + [ # Test case scenarios + ("BaseExceptionRaised", {"test_factory": _XBaseExceptionRaised}), + ("ErrorInSetup", {"test_factory": _XErrorInSetup}), + ("ErrorInTest", {"test_factory": _XErrorInTest}), + ("ErrorInTearDown", {"test_factory": _XErrorInTearDown}), + ("FailureInTest", {"test_factory": _XFailureInTest}), + ("ErrorInCleanup", {"test_factory": _XErrorInCleanup}), + ("ExpectThatFailure", {"test_factory": _XExpectThatFailure}), + ], + ) + + def assertResultsMatch(self, test, result): + events = list(result._events) + self.assertEqual(("startTest", test), events.pop(0)) + for expected_result in test.expected_results: + result = events.pop(0) + if len(expected_result) == 1: + self.assertEqual((expected_result[0], test), result) + else: + self.assertEqual((expected_result[0], test), result[:2]) + error_type = expected_result[1] + self.assertIn(error_type.__name__, str(result[2])) + self.assertEqual([("stopTest", test)], events) + + def test_runner(self): + """Test that each runner handles each test case scenario correctly.""" + result = ExtendedTestResult() + test = self.test_factory("test_something", runTest=self.runner) + if self.test_factory is _XBaseExceptionRaised: + self.assertRaises(SystemExit, test.run, result) + else: + test.run(result) + self.assertEqual(test.calls, self.test_factory.expected_calls) + self.assertResultsMatch(test, result) class TestSynchronousDeferredRunTest(NeedsTwistedTestCase): @@ -268,9 +325,10 @@ def test_setUp_returns_deferred_that_fires_later(self): # setUp can return a Deferred that might fire at any time. # AsynchronousDeferredRunTest will not go on to running the test until # the Deferred returned by setUp actually fires. - call_log = [] + call_log: list[Any] = [] marker = object() - d = defer.Deferred().addCallback(call_log.append) + d: defer.Deferred[Any] = defer.Deferred() + d.addCallback(call_log.append) class SomeCase(TestCase): def setUp(self): @@ -299,12 +357,12 @@ def test_calls_setUp_test_tearDown_in_sequence(self): # Deferreds. AsynchronousDeferredRunTest will make sure that each of # these are run in turn, only going on to the next stage once the # Deferred from the previous stage has fired. - call_log = [] - a = defer.Deferred() + call_log: list[Any] = [] + a: defer.Deferred[Any] = defer.Deferred() a.addCallback(lambda x: call_log.append("a")) - b = defer.Deferred() + b: defer.Deferred[Any] = defer.Deferred() b.addCallback(lambda x: call_log.append("b")) - c = defer.Deferred() + c: defer.Deferred[Any] = defer.Deferred() c.addCallback(lambda x: call_log.append("c")) class SomeCase(TestCase): @@ -355,7 +413,7 @@ def test_whatever(self): pass test = SomeCase("test_whatever") - call_log = [] + call_log: list[Any] = [] a = defer.Deferred().addCallback(lambda x: call_log.append("a")) b = defer.Deferred().addCallback(lambda x: call_log.append("b")) c = defer.Deferred().addCallback(lambda x: call_log.append("c")) @@ -411,6 +469,8 @@ def test_exports_reactor(self): timeout = self.make_timeout() class SomeCase(TestCase): + reactor: Any # Set dynamically by runner + def test_cruft(self): self.assertIs(reactor, self.reactor) @@ -761,7 +821,7 @@ def test_something(self): def test_do_not_log_to_twisted(self): # If suppress_twisted_logging is True, we don't log anything to the # default Twisted loggers. - messages = [] + messages: list[Any] = [] publisher, _ = _get_global_publisher_and_observers() publisher.addObserver(messages.append) self.addCleanup(publisher.removeObserver, messages.append) @@ -779,7 +839,7 @@ def test_something(self): def test_log_to_twisted(self): # If suppress_twisted_logging is False, we log to the default Twisted # loggers. - messages = [] + messages: list[Any] = [] publisher, _ = _get_global_publisher_and_observers() publisher.addObserver(messages.append) @@ -968,7 +1028,7 @@ class TestNoTwistedLogObservers(NeedsTwistedTestCase): def _get_logged_messages(self, function, *args, **kwargs): """Run ``function`` and return ``(ret, logged_messages)``.""" - messages = [] + messages: list[Any] = [] publisher, _ = _get_global_publisher_and_observers() publisher.addObserver(messages.append) try: @@ -1029,7 +1089,7 @@ def test_logged_messages_go_to_observer(self): # that observer while the fixture is active. from testtools.twistedsupport._runtest import _TwistedLogObservers - messages = [] + messages: list[Any] = [] class SomeTest(TestCase): def test_something(self): @@ -1105,5 +1165,6 @@ def test_suite(): def load_tests(loader, tests, pattern): - tests.addTest(make_integration_tests()) - return tests + from testscenarios import load_tests_apply_scenarios + + return load_tests_apply_scenarios(loader, tests, pattern) diff --git a/testtools/tests/twistedsupport/test_spinner.py b/testtools/tests/twistedsupport/test_spinner.py index 7601505a..9a7d8a35 100644 --- a/testtools/tests/twistedsupport/test_spinner.py +++ b/testtools/tests/twistedsupport/test_spinner.py @@ -4,9 +4,9 @@ import os import signal +from typing import Any from testtools import skipIf -from testtools.helpers import try_import from testtools.matchers import ( Equals, Is, @@ -16,17 +16,27 @@ from ._helpers import NeedsTwistedTestCase -_spinner = try_import("testtools.twistedsupport._spinner") +try: + from testtools.twistedsupport import _spinner +except ImportError: + _spinner = None # type: ignore[assignment] -defer = try_import("twisted.internet.defer") -Failure = try_import("twisted.python.failure.Failure") +try: + from twisted.internet import defer +except ImportError: + defer = None # type: ignore[assignment] + +try: + from twisted.python.failure import Failure +except ImportError: + Failure = None # type: ignore[assignment,misc] class TestNotReentrant(NeedsTwistedTestCase): def test_not_reentrant(self): # A function decorated as not being re-entrant will raise a # _spinner.ReentryError if it is called while it is running. - calls = [] + calls: list[Any] = [] @_spinner.not_reentrant def log_something(): @@ -38,7 +48,7 @@ def log_something(): self.assertEqual(1, len(calls)) def test_deeper_stack(self): - calls = [] + calls: list[Any] = [] @_spinner.not_reentrant def g(): @@ -95,7 +105,7 @@ def make_timeout(self): def test_function_called(self): # run_in_reactor actually calls the function given to it. - calls = [] + calls: list[Any] = [] marker = object() self.make_spinner().run(self.make_timeout(), calls.append, marker) self.assertThat(calls, Equals([marker])) @@ -117,7 +127,7 @@ def test_exception_reraised(self): def test_keyword_arguments(self): # run_in_reactor passes keyword arguments on. - calls = [] + calls: list[Any] = [] def function(*a, **kw): return calls.extend([a, kw]) @@ -146,8 +156,10 @@ def test_deferred_value_returned(self): self.assertThat(result, Is(marker)) def test_preserve_signal_handler(self): - signals = ["SIGINT", "SIGTERM", "SIGCHLD"] - signals = list(filter(None, (getattr(signal, name, None) for name in signals))) + signal_names = ["SIGINT", "SIGTERM", "SIGCHLD"] + signals: list[int] = list( + filter(None, (getattr(signal, name, None) for name in signal_names)) + ) for sig in signals: self.addCleanup(signal.signal, sig, signal.getsignal(sig)) new_hdlrs = list(lambda *a: None for _ in signals) @@ -326,7 +338,7 @@ def test_fires_after_timeout(self): reactor = self.make_reactor() spinner1 = self.make_spinner(reactor) timeout = self.make_timeout() - deferred1 = defer.Deferred() + deferred1: defer.Deferred[Any] = defer.Deferred() self.expectThat( lambda: spinner1.run(timeout, lambda: deferred1), Raises(MatchesException(_spinner.TimeoutError)), @@ -336,7 +348,7 @@ def test_fires_after_timeout(self): # reactor keeps spinning. We don't care that it's a callback of # deferred1 per se, only that it strictly fires afterwards. marker = object() - deferred2 = defer.Deferred() + deferred2: defer.Deferred[Any] = defer.Deferred() deferred1.addCallback( lambda ignored: reactor.callLater(0, deferred2.callback, marker) ) diff --git a/testtools/testsuite.py b/testtools/testsuite.py index 5e82bed9..4a35811e 100644 --- a/testtools/testsuite.py +++ b/testtools/testsuite.py @@ -16,6 +16,7 @@ from collections import Counter from pprint import pformat from queue import Queue +from typing import Any import testtools @@ -51,8 +52,7 @@ def __init__(self, suite, make_tests, wrap_result=None): """ super().__init__([suite]) self.make_tests = make_tests - if wrap_result: - self._wrap_result = wrap_result + self._custom_wrap_result = wrap_result def _wrap_result(self, thread_safe_result, thread_number): """Wrap a thread-safe result before sending it test results. @@ -60,9 +60,11 @@ def _wrap_result(self, thread_safe_result, thread_number): You can either override this in a subclass or pass your own ``wrap_result`` in to the constructor. The latter is preferred. """ + if self._custom_wrap_result: + return self._custom_wrap_result(thread_safe_result, thread_number) return thread_safe_result - def run(self, result): + def run(self, result, debug=False): """Run the tests concurrently. This calls out to the provided make_tests helper, and then serialises @@ -78,7 +80,7 @@ def run(self, result): tests = self.make_tests(self) try: threads = {} - queue = Queue() + queue: Queue[Any] = Queue() semaphore = threading.Semaphore(1) for i, test in enumerate(tests): process_result = self._wrap_result( @@ -126,7 +128,7 @@ def __init__(self, make_tests): super().__init__() self.make_tests = make_tests - def run(self, result): + def run(self, result, debug=False): """Run the tests concurrently. This calls out to the provided make_tests helper to determine the @@ -150,7 +152,7 @@ def run(self, result): tests = self.make_tests() try: threads = {} - queue = Queue() + queue: Queue[Any] = Queue() for test, route_code in tests: to_queue = testtools.StreamToQueue(queue, route_code) process_result = testtools.ExtendedToStreamDecorator( @@ -200,10 +202,10 @@ def __init__(self, fixture, tests): super().__init__(tests) self._fixture = fixture - def run(self, result): + def run(self, result, debug=False): self._fixture.setUp() try: - super().run(result) + super().run(result, debug) finally: self._fixture.cleanUp() diff --git a/testtools/twistedsupport/_runtest.py b/testtools/twistedsupport/_runtest.py index 484814e4..1e094489 100644 --- a/testtools/twistedsupport/_runtest.py +++ b/testtools/twistedsupport/_runtest.py @@ -28,6 +28,7 @@ def test_something(self): import io import sys +from typing import Any from fixtures import CompoundFixture, Fixture from twisted.internet import defer @@ -47,11 +48,11 @@ def test_something(self): try: from twisted.logger import globalLogPublisher except ImportError: - globalLogPublisher = None + globalLogPublisher = None # type: ignore[assignment] from twisted.python import log try: - from twisted.trial.unittest import _LogObserver + from twisted.trial.unittest import _LogObserver # type: ignore[attr-defined] except ImportError: from twisted.trial._synctest import _LogObserver @@ -305,7 +306,7 @@ def __call__(self, case, handlers=None, last_resort=None): return AsynchronousDeferredRunTestFactory() @defer.inlineCallbacks - def _run_cleanups(self): + def _run_cleanups(self, result=None): """Run the cleanups on the test case. We expect that the cleanups on the test case can also return @@ -336,7 +337,7 @@ def _run_deferred(self): call addSuccess on the result, because there's reactor clean up that we needs to be done afterwards. """ - fails = [] + fails: list[Any] = [] def fail_if_exception_caught(exception_caught): if self.exception_caught == exception_caught: