diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 578d9091..d3a4aea4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.8, 3.9, "3.10", 3.11, 3.12, pypy3.9, pypy3.10] + python-version: ["3.9", "3.10", "3.11", "3.12", "pypy3.9", "pypy3.10"] steps: - uses: actions/checkout@v4 @@ -32,8 +32,7 @@ jobs: - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install --upgrade setuptools wheel setuptools_scm + python -m pip install --upgrade pip wheel python -m pip install sphinx python -m pip install ".[test,twisted,dev]" diff --git a/NEWS b/NEWS index f8b4a641..b2fa855f 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,37 @@ testtools NEWS Changes and improvements to testtools_, grouped by release. +2.8.0 +~~~~~ + +Changes +------- + +* Drop support for Python 3.8. (Stephen Finucane) + +* Remove a number of deprecated classes and methods. (Stephen Finucane) + + * ``testtools.matchers`` + + * ``AfterPreproccessing`` (use ``AfterPreprocessing``) + + * ``testtools.testcase`` + + * ``TestSkipped`` (use ``unittest.SkipTest``) + * ``TestCase.skip`` (use ``TestCase.skipTest``) + * ``TestCase.failUnlessEqual`` (use ``TestCase.assertEqual``) + * ``TestCase.assertEquals`` (use ``TestCase.assertEqual``) + * ``TestCase.failUnlessRaises`` (use ``TestCase.assertRaises``) + * ``TestCase.assertItemsEqual`` (use ``TestCase.assertCountEqual``) + + * ``testtools.testresult.real`` + + * ``domap`` (no replacement) + + * ``testtools.twistedsupport._runtest`` + + * ``run_with_log_observers`` (no replacement) + 2.7.2 ~~~~~ diff --git a/pyproject.toml b/pyproject.toml index a205adca..a617189f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,18 +1,5 @@ -[tool.mypy] -warn_redundant_casts = true -warn_unused_configs = true -check_untyped_defs = true - -[[tool.mypy.overrides]] -module = [ - "fixtures.*", - "testresources.*", - "testscenarios.*", -] -ignore_missing_imports = true - [build-system] -requires = ["setuptools>=61", "hatchling", "hatch_vcs"] +requires = ["hatchling", "hatch_vcs"] build-backend = "hatchling.build" [project] @@ -27,7 +14,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -39,22 +25,16 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Testing", ] -dependencies = ["setuptools; python_version>='3.12'"] dynamic = ["version"] -requires-python = ">=3.8" +requires-python = ">=3.9" [project.urls] Homepage = "https://github.com/testing-cabal/testtools" -[tool.setuptools] -include-package-data = false - -[tool.setuptools.packages.find] -include = ["testtools*"] -exclude = ["man*"] - -[tool.files] -packages = "testtools" +[project.optional-dependencies] +test = ["testscenarios", "testresources"] +twisted = ["Twisted", "fixtures"] +dev = ["ruff==0.11.2"] [tool.hatch.version] source = "vcs" @@ -68,7 +48,15 @@ version = {version!r} __version__ = {version_tuple!r} """ -[project.optional-dependencies] -test = ["testscenarios", "testresources"] -twisted = ["Twisted", "fixtures"] -dev = ["ruff==0.11.2"] +[tool.mypy] +warn_redundant_casts = true +warn_unused_configs = true +check_untyped_defs = true + +[[tool.mypy.overrides]] +module = [ + "fixtures.*", + "testresources.*", + "testscenarios.*", +] +ignore_missing_imports = true diff --git a/setup.py b/setup.py deleted file mode 100755 index f187f2cd..00000000 --- a/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env python3 -import setuptools - -setuptools.setup() diff --git a/testtools/__init__.py b/testtools/__init__.py index 92cb3104..7421c8a6 100644 --- a/testtools/__init__.py +++ b/testtools/__init__.py @@ -96,6 +96,32 @@ iterate_tests, ) + +def __get_git_version(): + import os + import subprocess + + cwd = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) + + try: + out = subprocess.check_output( + ["git", "describe"], stderr=subprocess.STDOUT, cwd=cwd + ) + except (OSError, subprocess.CalledProcessError): + return None + + try: + version = out.strip().decode("utf-8") + except UnicodeDecodeError: + return None + + if "-" in version: # after tag + # convert version-N-githash to version.postN+githash + return version.replace("-", ".post", 1).replace("-g", "+git", 1) + else: + return version + + # same format as sys.version_info: "A tuple containing the five components of # the version number: major, minor, micro, releaselevel, and serial. All # values except releaselevel are integers; the release level is 'alpha', @@ -109,21 +135,13 @@ # Otherwise it is major.minor.micro~$(revno). try: - # If setuptools_scm is installed (e.g. in a development environment with - # an editable install), then use it to determine the version dynamically. - from setuptools_scm import get_version - - # This will fail with LookupError if the package is not installed in - # editable mode or if Git is not installed. - version = get_version(root="..", relative_to=__file__) - __version__ = tuple([int(v) if v.isdigit() else v for v in version.split(".")]) -except (ImportError, LookupError): - # As a fallback, use the version that is hard-coded in the file. - try: - from ._version import __version__, version - except ModuleNotFoundError: - # The user is probably trying to run this without having installed - # the package, so complain. - raise RuntimeError( - "Testtools is not correctly installed. Please install it with pip." - ) + from ._version import __version__, version +except ModuleNotFoundError: + # package is not installed + if version := __get_git_version(): + # we're in a git repo + __version__ = tuple([int(v) if v.isdigit() else v for v in version.split(".")]) + else: + # we're working with a tarball or similar + version = "0.0.0" + __version__ = (0, 0, 0) diff --git a/testtools/matchers/_basic.py b/testtools/matchers/_basic.py index dc840fd9..4e4b1453 100644 --- a/testtools/matchers/_basic.py +++ b/testtools/matchers/_basic.py @@ -18,7 +18,6 @@ import operator from pprint import pformat import re -import warnings from ..compat import ( text_repr, @@ -71,24 +70,6 @@ def __init__(self, actual, mismatch_string, reference, reference_on_right=True): self._reference = reference self._reference_on_right = reference_on_right - @property - def expected(self): - warnings.warn( - f"{self.__class__.__name__}.expected deprecated after 1.8.1", - DeprecationWarning, - stacklevel=2, - ) - return self._reference - - @property - def other(self): - warnings.warn( - f"{self.__class__.__name__}.other deprecated after 1.8.1", - DeprecationWarning, - stacklevel=2, - ) - return self._actual - def describe(self): actual = repr(self._actual) reference = repr(self._reference) diff --git a/testtools/matchers/_exception.py b/testtools/matchers/_exception.py index 535c7629..02e58c82 100644 --- a/testtools/matchers/_exception.py +++ b/testtools/matchers/_exception.py @@ -9,7 +9,7 @@ import sys from ._basic import MatchesRegex -from ._higherorder import AfterPreproccessing +from ._higherorder import AfterPreprocessing from ._impl import ( Matcher, Mismatch, @@ -47,7 +47,7 @@ def __init__(self, exception, value_re=None): Matcher.__init__(self) self.expected = exception if isinstance(value_re, str): - value_re = AfterPreproccessing(str, MatchesRegex(value_re), False) + value_re = AfterPreprocessing(str, MatchesRegex(value_re), False) self.value_re = value_re expected_type = type(self.expected) self._is_instance = not any( diff --git a/testtools/matchers/_higherorder.py b/testtools/matchers/_higherorder.py index 2de64a60..a14df81b 100644 --- a/testtools/matchers/_higherorder.py +++ b/testtools/matchers/_higherorder.py @@ -212,11 +212,6 @@ def match(self, value): return matcher.match(after) -# This is the old, deprecated. spelling of the name, kept for backwards -# compatibility. -AfterPreproccessing = AfterPreprocessing - - class AllMatch: """Matches if all provided values match the given matcher.""" diff --git a/testtools/testcase.py b/testtools/testcase.py index b8722df7..4f4923b3 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -22,7 +22,6 @@ import types import unittest from unittest.case import SkipTest -import warnings from testtools.compat import reraise from testtools import content @@ -50,18 +49,6 @@ ) -class TestSkipped(SkipTest): - """Raised within TestCase.run() when a test is skipped.""" - - def __init__(self, *args, **kwargs): - warnings.warn( - "Use SkipTest from unittest instead.", - DeprecationWarning, - stacklevel=2, - ) - super().__init__(*args, **kwargs) - - class _UnexpectedSuccess(Exception): """An unexpected success was raised. @@ -281,8 +268,10 @@ def _reset(self): def __eq__(self, other): eq = getattr(unittest.TestCase, "__eq__", None) - if eq is not None and not unittest.TestCase.__eq__(self, other): - return False + if eq is not None: + eq_ = unittest.TestCase.__eq__(self, other) + if eq_ is NotImplemented or not eq_: + return False return self.__dict__ == getattr(other, "__dict__", None) # We need to explicitly set this since we're overriding __eq__ @@ -293,17 +282,6 @@ def __repr__(self): # We add id to the repr because it makes testing testtools easier. return f"<{self.id()} id=0x{id(self):0x}>" - def _deprecate(original_func): - def deprecated_func(*args, **kwargs): - warnings.warn( - "Please use {0} instead.".format(original_func.__name__), - DeprecationWarning, - stacklevel=2, - ) - return original_func(*args, **kwargs) - - return deprecated_func - def addDetail(self, name, content_object): """Add a detail to be reported with this test's outcome. @@ -355,15 +333,6 @@ def skipTest(self, reason): """ raise self.skipException(reason) - def skip(self, reason): - """DEPRECATED: Use skipTest instead.""" - warnings.warn( - "Only valid in 1.8.1 and earlier. Use skipTest instead.", - DeprecationWarning, - stacklevel=2, - ) - self.skipTest(reason) - def _formatTypes(self, classOrIterable): """Format a class or a bunch of classes for display in an error.""" className = getattr(classOrIterable, "__name__", None) @@ -418,8 +387,6 @@ def assertEqual(self, expected, observed, message=""): matcher = _FlippedEquals(expected) self.assertThat(observed, matcher, message) - failUnlessEqual = assertEquals = _deprecate(assertEqual) - def assertIn(self, needle, haystack, message=""): """Assert that needle is in haystack.""" self.assertThat(haystack, Contains(needle), message) @@ -495,8 +462,6 @@ def match(self, matchee): self.assertThat(our_callable, matcher) return capture.matchee - failUnlessRaises = _deprecate(assertRaises) - def assertThat(self, matchee, matcher, message="", verbose=False): """Assert that matchee is matched by matcher. @@ -508,8 +473,6 @@ def assertThat(self, matchee, matcher, message="", verbose=False): if mismatch_error is not None: raise mismatch_error - assertItemsEqual = _deprecate(unittest.TestCase.assertCountEqual) - def addDetailUniqueName(self, name, content_object): """Add a detail to the test, but ensure it's name is unique. diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 5958d04c..39d620b0 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -28,7 +28,6 @@ from operator import methodcaller import sys import unittest -import warnings from testtools.compat import _b from testtools.content import ( @@ -436,21 +435,6 @@ def status( """ -def domap(function, *sequences): - """A strict version of 'map' that's guaranteed to run on all inputs. - - DEPRECATED since testtools 1.8.1: Internal code should use _strict_map. - External code should look for other solutions for their strict mapping - needs. - """ - warnings.warn( - "domap deprecated since 1.8.1. Please implement your own strict map.", - DeprecationWarning, - stacklevel=2, - ) - return _strict_map(function, *sequences) - - def _strict_map(function, *sequences): return list(map(function, *sequences)) diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index 644537bd..909f5148 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -41,7 +41,6 @@ attr, Nullary, WithAttributes, - TestSkipped, ) from testtools.testresult.doubles import ( Python26TestResult, @@ -304,7 +303,7 @@ def test_identicalIsEqual(self): def test_nonIdenticalInUnequal(self): # TestCase's are not equal if they are not identical. - self.assertNotEqual(TestCase(methodName="run"), TestCase(methodName="skip")) + self.assertNotEqual(TestCase(methodName="run"), TestCase(methodName="skipTest")) class TestAssertions(TestCase): @@ -804,16 +803,12 @@ def test_assertEqual_nice_formatting(self): ] ) self.assertFails(expected_error, self.assertEqual, a, b, message) - self.assertFails(expected_error, self.assertEquals, a, b, message) - self.assertFails(expected_error, self.failUnlessEqual, a, b, message) def test_assertEqual_formatting_no_message(self): a = "cat" b = "dog" expected_error = "'cat' != 'dog'" self.assertFails(expected_error, self.assertEqual, a, b) - self.assertFails(expected_error, self.assertEquals, a, b) - self.assertFails(expected_error, self.failUnlessEqual, a, b) def test_assertEqual_non_ascii_str_with_newlines(self): message = "Be careful mixing unicode and bytes" @@ -1817,8 +1812,8 @@ def test_that_is_decorated_with_skip(self): result = Python26TestResult(events) try: test = SkippingTest("test_that_is_decorated_with_skip") - except TestSkipped: - self.fail("TestSkipped raised") + except unittest.SkipTest: + self.fail("SkipTest raised") test.run(result) self.assertEqual("addSuccess", events[1][0]) @@ -1832,8 +1827,8 @@ def test_that_is_decorated_with_skipIf(self): result = Python26TestResult(events) try: test = SkippingTest("test_that_is_decorated_with_skipIf") - except TestSkipped: - self.fail("TestSkipped raised") + except unittest.SkipTest: + self.fail("SkipTest raised") test.run(result) self.assertEqual("addSuccess", events[1][0]) @@ -1847,8 +1842,8 @@ def test_that_is_decorated_with_skipUnless(self): result = Python26TestResult(events) try: test = SkippingTest("test_that_is_decorated_with_skipUnless") - except TestSkipped: - self.fail("TestSkipped raised") + except unittest.SkipTest: + self.fail("SkipTest raised") test.run(result) self.assertEqual("addSuccess", events[1][0]) @@ -1882,8 +1877,8 @@ def test_skipped(self): try: test = SkippingTestCase("test_skipped") - except TestSkipped: - self.fail("TestSkipped raised") + except unittest.SkipTest: + self.fail("SkipTest raised") self.check_test_does_not_run_setup(test, reason) def check_test_does_not_run_setup(self, test, reason): diff --git a/testtools/tests/twistedsupport/test_runtest.py b/testtools/tests/twistedsupport/test_runtest.py index f8faf7c6..8209b847 100644 --- a/testtools/tests/twistedsupport/test_runtest.py +++ b/testtools/tests/twistedsupport/test_runtest.py @@ -961,19 +961,6 @@ def check_result(failure): ) -class TestRunWithLogObservers(NeedsTwistedTestCase): - def test_restores_observers(self): - from testtools.twistedsupport._runtest import run_with_log_observers - from twisted.python import log - - # Make sure there's at least one observer. This reproduces bug - # #926189. - log.addObserver(lambda *args: None) - observers = list(log.theLogPublisher.observers) - run_with_log_observers([], lambda: None) - self.assertEqual(observers, log.theLogPublisher.observers) - - class TestNoTwistedLogObservers(NeedsTwistedTestCase): """Tests for _NoTwistedLogObservers.""" diff --git a/testtools/tests/twistedsupport/test_spinner.py b/testtools/tests/twistedsupport/test_spinner.py index d3a0761c..772341b3 100644 --- a/testtools/tests/twistedsupport/test_spinner.py +++ b/testtools/tests/twistedsupport/test_spinner.py @@ -155,7 +155,7 @@ def test_preserve_signal_handler(self): signal.signal(sig, hdlr) spinner = self.make_spinner() spinner.run(self.make_timeout(), lambda: None) - self.assertItemsEqual(new_hdlrs, list(map(signal.getsignal, signals))) + self.assertCountEqual(new_hdlrs, list(map(signal.getsignal, signals))) def test_timeout(self): # If the function takes too long to run, we raise a diff --git a/testtools/twistedsupport/_runtest.py b/testtools/twistedsupport/_runtest.py index a343e68a..a00a5eca 100644 --- a/testtools/twistedsupport/_runtest.py +++ b/testtools/twistedsupport/_runtest.py @@ -27,7 +27,6 @@ def test_something(self): ] import io -import warnings import sys from fixtures import CompoundFixture, Fixture @@ -182,18 +181,6 @@ def _setUp(self): ) -def run_with_log_observers(observers, function, *args, **kwargs): - """Run 'function' with the given Twisted log observers.""" - warnings.warn( - "run_with_log_observers is deprecated since 1.8.2.", - DeprecationWarning, - stacklevel=2, - ) - with _NoTwistedLogObservers(): - with _TwistedLogObservers(observers): - return function(*args, **kwargs) - - # Observer of the Twisted log that we install during tests. # # This is a global so that users can call flush_logged_errors errors in their diff --git a/testtools/utils.py b/testtools/utils.py deleted file mode 100644 index 4df3cd7f..00000000 --- a/testtools/utils.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2008-2010 testtools developers. See LICENSE for details. - -"""Utilities for dealing with stuff in unittest. - -Legacy - deprecated - use testtools.testsuite.iterate_tests -""" - -import warnings - -warnings.warn( - "Please import iterate_tests from testtools.testsuite - " - "testtools.utils is deprecated.", - DeprecationWarning, - stacklevel=2, -) diff --git a/tox.ini b/tox.ini index 68364d4a..c633e27f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310,py311,py312,pypy3 +envlist = py39,py310,py311,py312,py313,pypy3 minversion = 4.2 [testenv]