From 3962d15a2c593d2e47d240ab6f0c5352caca3464 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Wed, 1 Apr 2026 12:56:27 +0200 Subject: [PATCH] Fix Python 3.15 compatibility issues - Replace deprecated SourceFileLoader.load_module() with exec_module() - Add addSubTest() method to _AdaptedReporter for Python 3.15 doctest support - Update test expectations for Python 3.15 doctest changes: - Different success/failure counts - Changed error output format --- src/twisted/trial/reporter.py | 13 +++++++++++++ src/twisted/trial/runner.py | 6 +++++- src/twisted/trial/test/test_doctest.py | 9 +++++++-- src/twisted/trial/test/test_reporter.py | 17 ++++++++++++----- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/twisted/trial/reporter.py b/src/twisted/trial/reporter.py index 98ad7eb1aa6..bfdaca5e907 100644 --- a/src/twisted/trial/reporter.py +++ b/src/twisted/trial/reporter.py @@ -390,6 +390,19 @@ def stopTest(self, test): """ return self._originalReporter.stopTest(self.testAdapter(test)) + def addSubTest(self, test, subtest, outcome): + """ + See L{unittest.TestResult.addSubTest}. + + Called by the doctest runner in Python 3.15+ when reporting sub-test results. + """ + test = self.testAdapter(test) + if hasattr(self._originalReporter, "addSubTest"): + return self._originalReporter.addSubTest(test, subtest, outcome) + # If the underlying reporter doesn't support subtests, treat as error/success + if outcome is not None: + return self._originalReporter.addError(test, outcome) + @implementer(itrial.IReporter) class Reporter(TestResult): diff --git a/src/twisted/trial/runner.py b/src/twisted/trial/runner.py index 6d4fdb96d18..6d780bb3545 100644 --- a/src/twisted/trial/runner.py +++ b/src/twisted/trial/runner.py @@ -760,7 +760,11 @@ def loadFile(self, fileName, recurse=False): """ name = reflect.filenameToModuleName(fileName) try: - module = SourceFileLoader(name, fileName).load_module() + loader = SourceFileLoader(name, fileName) + spec = importlib.util.spec_from_loader(name, loader) + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) return self.loadAnything(module, recurse=recurse) except OSError: raise ValueError(f"{fileName} is not a Python file.") diff --git a/src/twisted/trial/test/test_doctest.py b/src/twisted/trial/test/test_doctest.py index 05d57d44e06..5f6a1c17fe8 100644 --- a/src/twisted/trial/test/test_doctest.py +++ b/src/twisted/trial/test/test_doctest.py @@ -4,6 +4,7 @@ """ Test Twisted's doctest support. """ +import sys import unittest as pyunit from twisted.trial import itrial, reporter, runner, unittest @@ -40,8 +41,12 @@ def _testRun(self, suite: pyunit.TestSuite) -> None: """ result = reporter.TestResult() suite.run(result) - self.assertEqual(5, result.successes) - self.assertEqual(2, len(result.failures)) + # Python 3.15+ counts doctest examples differently, resulting in more successes + # and also consolidates some failures + expected_successes = 7 if sys.version_info >= (3, 15) else 5 + expected_failures = 1 if sys.version_info >= (3, 15) else 2 + self.assertEqual(expected_successes, result.successes) + self.assertEqual(expected_failures, len(result.failures)) def test_expectedResults(self, count: int = 1) -> None: """ diff --git a/src/twisted/trial/test/test_reporter.py b/src/twisted/trial/test/test_reporter.py index 76172e7a6de..441704c7662 100644 --- a/src/twisted/trial/test/test_reporter.py +++ b/src/twisted/trial/test/test_reporter.py @@ -205,12 +205,19 @@ def test_doctestError(self): suite = unittest.decorate(self.loader.loadDoctests(erroneous), itrial.ITestCase) output = self.getOutput(suite) path = "twisted.trial.test.erroneous.unexpectedException" - for substring in ["1/0", "ZeroDivisionError", "Exception raised:", path]: + # Python 3.15+ changed doctest error output format + if sys.version_info >= (3, 15): + substrings = ["1/0", "ZeroDivisionError", path] + else: + substrings = ["1/0", "ZeroDivisionError", "Exception raised:", path] + for substring in substrings: self.assertSubstring(substring, output) - self.assertTrue( - re.search("Fail(ed|ure in) example:", output), - "Couldn't match 'Failure in example: ' " "or 'Failed example: '", - ) + # Python 3.15+ no longer uses "Failed example:" format + if sys.version_info < (3, 15): + self.assertTrue( + re.search("Fail(ed|ure in) example:", output), + "Couldn't match 'Failure in example: ' " "or 'Failed example: '", + ) expect = [self.doubleSeparator, re.compile(r"\[(ERROR|FAIL)\]")] self.stringComparison(expect, output.splitlines())