From aca432f59e04047ce16e4be4818c185cab412467 Mon Sep 17 00:00:00 2001 From: Saurabh B Date: Wed, 30 Apr 2025 11:30:56 +0200 Subject: [PATCH 1/4] Add support for custom test case and runner in both DocTestSuite and DocFileSuite --- Doc/library/doctest.rst | 30 ++++++++++++- Lib/doctest.py | 37 +++++++++++++--- Lib/test/test_doctest/test_doctest.py | 62 +++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 9 deletions(-) diff --git a/Doc/library/doctest.rst b/Doc/library/doctest.rst index b86fef9fd6f310..97a417b380341c 100644 --- a/Doc/library/doctest.rst +++ b/Doc/library/doctest.rst @@ -1038,7 +1038,7 @@ There are two main functions for creating :class:`unittest.TestSuite` instances from text files and modules with doctests: -.. function:: DocFileSuite(*paths, module_relative=True, package=None, setUp=None, tearDown=None, globs=None, optionflags=0, parser=DocTestParser(), encoding=None) +.. function:: DocFileSuite(*paths, module_relative=True, package=None, setUp=None, tearDown=None, globs=None, optionflags=0, parser=DocTestParser(), encoding=None, test_case=DocFileCase, runner=DocTestRunner) Convert doctest tests from one or more text files to a :class:`unittest.TestSuite`. @@ -1102,11 +1102,24 @@ from text files and modules with doctests: Optional argument *encoding* specifies an encoding that should be used to convert the file to unicode. + Optional argument *test_case* specifies the :class:`DocFileCase` class (or a + subclass) that should be used to create test cases. By default, :class:`DocFileCase` + is used. This allows for custom test case classes that can add additional behavior + or attributes to the test cases. + + Optional argument *runner* specifies the :class:`DocTestRunner` class (or a + subclass) that should be used to run the tests. By default, :class:`DocTestRunner` + is used. + The global ``__file__`` is added to the globals provided to doctests loaded from a text file using :func:`DocFileSuite`. + .. versionchanged:: 3.13 + Added *test_case* and *runner* parameters to support user specified test case + and runner in DocFileSuite. + -.. function:: DocTestSuite(module=None, globs=None, extraglobs=None, test_finder=None, setUp=None, tearDown=None, optionflags=0, checker=None) +.. function:: DocTestSuite(module=None, globs=None, extraglobs=None, test_finder=None, setUp=None, tearDown=None, optionflags=0, checker=None, test_case=DocTestCase, runner=DocTestRunner) Convert doctest tests for a module to a :class:`unittest.TestSuite`. @@ -1134,12 +1147,25 @@ from text files and modules with doctests: Optional arguments *setUp*, *tearDown*, and *optionflags* are the same as for function :func:`DocFileSuite` above. + Optional argument *test_case* specifies the :class:`DocTestCase` class (or a + subclass) that should be used to create test cases. By default, :class:`DocTestCase` + is used. This allows for custom test case classes that can add additional behavior + or attributes to the test cases. + + Optional argument *runner* specifies the :class:`DocTestRunner` class (or a + subclass) that should be used to run the tests. By default, :class:`DocTestRunner` + is used. + This function uses the same search technique as :func:`testmod`. .. versionchanged:: 3.5 :func:`DocTestSuite` returns an empty :class:`unittest.TestSuite` if *module* contains no docstrings instead of raising :exc:`ValueError`. + .. versionchanged:: 3.13 + Added *runner* and *test_case* parameters to support custom test runners + and test case classes in DocTestSuite. + .. exception:: failureException When doctests which have been converted to unit tests by :func:`DocFileSuite` diff --git a/Lib/doctest.py b/Lib/doctest.py index e02e73ed722f7e..72193343305b33 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -2275,7 +2275,7 @@ def set_unittest_reportflags(flags): class DocTestCase(unittest.TestCase): def __init__(self, test, optionflags=0, setUp=None, tearDown=None, - checker=None): + checker=None, runner=DocTestRunner): unittest.TestCase.__init__(self) self._dt_optionflags = optionflags @@ -2283,6 +2283,7 @@ def __init__(self, test, optionflags=0, setUp=None, tearDown=None, self._dt_test = test self._dt_setUp = setUp self._dt_tearDown = tearDown + self._dt_runner = runner def setUp(self): test = self._dt_test @@ -2312,7 +2313,7 @@ def runTest(self): # so add the default reporting flags optionflags |= _unittest_reportflags - runner = DocTestRunner(optionflags=optionflags, + runner = self._dt_runner(optionflags=optionflags, checker=self._dt_checker, verbose=False) try: @@ -2460,7 +2461,7 @@ def _removeTestAtIndex(self, index): def DocTestSuite(module=None, globs=None, extraglobs=None, test_finder=None, - **options): + test_case=DocTestCase, runner=DocTestRunner, **options): """ Convert doctest tests for a module to a unittest test suite. @@ -2494,6 +2495,12 @@ def DocTestSuite(module=None, globs=None, extraglobs=None, test_finder=None, optionflags A set of doctest option flags expressed as an integer. + + test_case + A custom optional DocTestCase class to use for test cases. + + runner + A custom optional DocTestRunner class to use for running tests. """ if test_finder is None: @@ -2519,7 +2526,7 @@ def DocTestSuite(module=None, globs=None, extraglobs=None, test_finder=None, if filename[-4:] == ".pyc": filename = filename[:-1] test.filename = filename - suite.addTest(DocTestCase(test, **options)) + suite.addTest(test_case(test, runner=runner, **options)) return suite @@ -2538,7 +2545,8 @@ def format_failure(self, err): def DocFileTest(path, module_relative=True, package=None, globs=None, parser=DocTestParser(), - encoding=None, **options): + encoding=None, test_case=DocFileCase, + runner=DocTestRunner, **options): if globs is None: globs = {} else: @@ -2560,7 +2568,7 @@ def DocFileTest(path, module_relative=True, package=None, # Convert it to a test, and wrap it in a DocFileCase. test = parser.get_doctest(doc, globs, name, path, 0) - return DocFileCase(test, **options) + return test_case(test, runner=runner, **options) def DocFileSuite(*paths, **kw): """A unittest suite for one or more doctest files. @@ -2617,6 +2625,12 @@ def DocFileSuite(*paths, **kw): encoding An encoding that will be used to convert the files to unicode. + + test_case + A custom DocFileCase subclass to use for the tests. + + runner + A custom DocTestRunner subclass to use for running the tests. """ suite = _DocTestSuite() @@ -2626,8 +2640,17 @@ def DocFileSuite(*paths, **kw): if kw.get('module_relative', True): kw['package'] = _normalize_module(kw.get('package')) + test_case = kw.pop('test_case', DocFileCase) + runner = kw.pop('runner', DocTestRunner) + for path in paths: - suite.addTest(DocFileTest(path, **kw)) + test_instance = DocFileTest( + path=path, + test_case=test_case, + runner=runner, + **kw + ) + suite.addTest(test_instance) return suite diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index a4a49298bab3be..55a54365648a20 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -2383,6 +2383,36 @@ def test_DocTestSuite(): modified the test globals, which are a copy of the sample_doctest module dictionary. The test globals are automatically cleared for us after a test. + + We can also provide a custom test case class: + + >>> class CustomDocTestCase(doctest.DocTestCase): + ... def __init__(self, test, **options): + ... super().__init__(test, **options) + ... self.custom_attr = "custom_value" + ... def runTest(self): + ... self.assertEqual(self.custom_attr, "custom_value") + ... super().runTest() + + >>> suite = doctest.DocTestSuite('test.test_doctest.sample_doctest', + ... test_case=CustomDocTestCase) + >>> result = suite.run(unittest.TestResult()) + >>> result + + + We can also provide both a custom test case class and a custom runner class: + + >>> class CustomDocTestRunner(doctest.DocTestRunner): + ... def __init__(self, **options): + ... super().__init__(**options) + ... self.custom_attr = "custom_runner" + >>> suite = doctest.DocTestSuite('test.test_doctest.sample_doctest', + ... test_case=CustomDocTestCase, + ... runner=CustomDocTestRunner) + >>> result = suite.run(unittest.TestResult()) + >>> result + + """ def test_DocFileSuite(): @@ -2543,6 +2573,38 @@ def test_DocFileSuite(): >>> suite.run(unittest.TestResult()) + We can also provide a custom test case class: + + >>> class CustomDocTestCase(doctest.DocFileCase): + ... def __init__(self, test, **options): + ... super().__init__(test, **options) + ... self.custom_attr = "custom_value" + ... def runTest(self): + ... self.assertEqual(self.custom_attr, "custom_value") + ... super().runTest() + + >>> suite = doctest.DocFileSuite('test_doctest.txt', + ... globs={'favorite_color': 'blue'}, + ... test_case=CustomDocTestCase) + >>> result = suite.run(unittest.TestResult()) + >>> result + + + We can also provide both a custom test case class and a custom runner class: + + >>> class CustomDocTestRunner(doctest.DocTestRunner): + ... def __init__(self, **options): + ... super().__init__(**options) + ... self.custom_attr = "custom_runner" + + >>> suite = doctest.DocFileSuite('test_doctest.txt', + ... globs={'favorite_color': 'blue'}, + ... test_case=CustomDocTestCase, + ... runner=CustomDocTestRunner) + >>> result = suite.run(unittest.TestResult()) + >>> result + + """ def test_trailing_space_in_test(): From 6493d0026f38b095557ea731fb815b85c57a1cd6 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 12:01:47 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-04-30-12-01-45.gh-issue-43657.YzJLCZ.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-04-30-12-01-45.gh-issue-43657.YzJLCZ.rst diff --git a/Misc/NEWS.d/next/Library/2025-04-30-12-01-45.gh-issue-43657.YzJLCZ.rst b/Misc/NEWS.d/next/Library/2025-04-30-12-01-45.gh-issue-43657.YzJLCZ.rst new file mode 100644 index 00000000000000..da7b4c6a65675d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-30-12-01-45.gh-issue-43657.YzJLCZ.rst @@ -0,0 +1 @@ +Allow custom test case and runner for DocTestSuite and DocFileSuite From 72101d5934ea63e79ddd46195cb1e5d9ce47c80e Mon Sep 17 00:00:00 2001 From: Saurabh B Date: Wed, 30 Apr 2025 14:54:23 +0200 Subject: [PATCH 3/4] Fix for missing class reference in docs --- Doc/library/doctest.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/doctest.rst b/Doc/library/doctest.rst index 97a417b380341c..a531267606aedc 100644 --- a/Doc/library/doctest.rst +++ b/Doc/library/doctest.rst @@ -1102,8 +1102,8 @@ from text files and modules with doctests: Optional argument *encoding* specifies an encoding that should be used to convert the file to unicode. - Optional argument *test_case* specifies the :class:`DocFileCase` class (or a - subclass) that should be used to create test cases. By default, :class:`DocFileCase` + Optional argument *test_case* specifies the :class:`!DocFileCase` class (or a + subclass) that should be used to create test cases. By default, :class:`!DocFileCase` is used. This allows for custom test case classes that can add additional behavior or attributes to the test cases. @@ -1147,8 +1147,8 @@ from text files and modules with doctests: Optional arguments *setUp*, *tearDown*, and *optionflags* are the same as for function :func:`DocFileSuite` above. - Optional argument *test_case* specifies the :class:`DocTestCase` class (or a - subclass) that should be used to create test cases. By default, :class:`DocTestCase` + Optional argument *test_case* specifies the :class:`!DocTestCase` class (or a + subclass) that should be used to create test cases. By default, :class:`!DocTestCase` is used. This allows for custom test case classes that can add additional behavior or attributes to the test cases. From 505248c1592346e419a444933eacc177a8bbb08a Mon Sep 17 00:00:00 2001 From: Saurabh B Date: Wed, 30 Apr 2025 15:09:25 +0200 Subject: [PATCH 4/4] Improve news entry for gh-43657 --- .../next/Library/2025-04-30-12-01-45.gh-issue-43657.YzJLCZ.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-04-30-12-01-45.gh-issue-43657.YzJLCZ.rst b/Misc/NEWS.d/next/Library/2025-04-30-12-01-45.gh-issue-43657.YzJLCZ.rst index da7b4c6a65675d..4bb8487be7d0df 100644 --- a/Misc/NEWS.d/next/Library/2025-04-30-12-01-45.gh-issue-43657.YzJLCZ.rst +++ b/Misc/NEWS.d/next/Library/2025-04-30-12-01-45.gh-issue-43657.YzJLCZ.rst @@ -1 +1 @@ -Allow custom test case and runner for DocTestSuite and DocFileSuite +Add support for custom test case and runner classes in :func:`doctest.DocTestSuite` and :func:`doctest.DocFileSuite`. This allows users to extend doctest functionality by providing their own test case and runner implementations through the new ``test_case`` and ``runner`` parameters.