From 7315af0ccbbd56221ea3085a93859504edde28ba Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 7 Sep 2024 13:19:38 -0400 Subject: [PATCH 01/11] Revert "bpo-23882: unittest: Drop PEP 420 support from discovery. (GH-29745)" This reverts commit 0b2b9d251374c5ed94265e28039f82b37d039e3e. --- Doc/library/unittest.rst | 25 +---------- Doc/whatsnew/3.11.rst | 4 -- Lib/test/test_unittest/test_discovery.py | 35 ++++++++++++++- Lib/unittest/loader.py | 57 ++++++++++++++++++------ 4 files changed, 79 insertions(+), 42 deletions(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index c49aba69b12126..536c4474c0a70c 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -272,7 +272,8 @@ Test Discovery Unittest supports simple test discovery. In order to be compatible with test discovery, all of the test files must be :ref:`modules ` or -:ref:`packages ` importable from the top-level directory of +:ref:`packages ` (including :term:`namespace packages +`) importable from the top-level directory of the project (this means that their filenames must be valid :ref:`identifiers `). @@ -345,24 +346,6 @@ the `load_tests protocol`_. directory too (e.g. ``python -m unittest discover -s root/namespace -t root``). -.. versionchanged:: 3.11 - :mod:`unittest` dropped the :term:`namespace packages ` - support in Python 3.11. It has been broken since Python 3.7. Start directory and - subdirectories containing tests must be regular package that have - ``__init__.py`` file. - - Directories containing start directory still can be a namespace package. - In this case, you need to specify start directory as dotted package name, - and target directory explicitly. For example:: - - # proj/ <-- current directory - # namespace/ - # mypkg/ - # __init__.py - # test_mypkg.py - - python -m unittest discover -s namespace.mypkg -t . - .. _organizing-tests: @@ -1928,10 +1911,6 @@ Loading and running tests whether their path matches *pattern*, because it is impossible for a package name to match the default pattern. - .. versionchanged:: 3.11 - *start_dir* can not be a :term:`namespace packages `. - It has been broken since Python 3.7 and Python 3.11 officially remove it. - .. versionchanged:: 3.13 *top_level_dir* is only stored for the duration of *discover* call. diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index d59f24406c9483..6525d1c4ca100f 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -2028,10 +2028,6 @@ Removed C APIs are :ref:`listed separately `. * Removed the deprecated :meth:`!split` method of :class:`!_tkinter.TkappType`. (Contributed by Erlend E. Aasland in :issue:`38371`.) -* Removed namespace package support from :mod:`unittest` discovery. - It was introduced in Python 3.4 but has been broken since Python 3.7. - (Contributed by Inada Naoki in :issue:`23882`.) - * Removed the undocumented private :meth:`!float.__set_format__` method, previously known as :meth:`!float.__setformat__` in Python 3.7. Its docstring said: "You probably don't want to use this function. diff --git a/Lib/test/test_unittest/test_discovery.py b/Lib/test/test_unittest/test_discovery.py index a44b18406c08be..b5d5b2d8284fca 100644 --- a/Lib/test/test_unittest/test_discovery.py +++ b/Lib/test/test_unittest/test_discovery.py @@ -395,7 +395,7 @@ def restore_isdir(): self.addCleanup(restore_isdir) _find_tests_args = [] - def _find_tests(start_dir, pattern): + def _find_tests(start_dir, pattern, namespace=None): _find_tests_args.append((start_dir, pattern)) return ['tests'] loader._find_tests = _find_tests @@ -815,7 +815,7 @@ def test_discovery_from_dotted_path(self): expectedPath = os.path.abspath(os.path.dirname(test.test_unittest.__file__)) self.wasRun = False - def _find_tests(start_dir, pattern): + def _find_tests(start_dir, pattern, namespace=None): self.wasRun = True self.assertEqual(start_dir, expectedPath) return tests @@ -848,6 +848,37 @@ def restore(): 'Can not use builtin modules ' 'as dotted module names') + def test_discovery_from_dotted_namespace_packages(self): + loader = unittest.TestLoader() + + package = types.ModuleType('package') + package.__path__ = ['/a', '/b'] + package.__spec__ = types.SimpleNamespace( + loader=None, + submodule_search_locations=['/a', '/b'] + ) + + def _import(packagename, *args, **kwargs): + sys.modules[packagename] = package + return package + + _find_tests_args = [] + def _find_tests(start_dir, pattern, namespace=None): + _find_tests_args.append((start_dir, pattern)) + return ['%s/tests' % start_dir] + + loader._find_tests = _find_tests + loader.suiteClass = list + + with unittest.mock.patch('builtins.__import__', _import): + # Since loader.discover() can modify sys.path, restore it when done. + with import_helper.DirsOnSysPath(): + # Make sure to remove 'package' from sys.modules when done. + with test.test_importlib.util.uncache('package'): + suite = loader.discover('package') + + self.assertEqual(suite, ['/a/tests', '/b/tests']) + def test_discovery_failed_discovery(self): from test.test_importlib import util diff --git a/Lib/unittest/loader.py b/Lib/unittest/loader.py index 22797b83a68bc8..c557e222941e21 100644 --- a/Lib/unittest/loader.py +++ b/Lib/unittest/loader.py @@ -274,6 +274,8 @@ def discover(self, start_dir, pattern='test*.py', top_level_dir=None): self._top_level_dir = top_level_dir is_not_importable = False + is_namespace = False + tests = [] if os.path.isdir(os.path.abspath(start_dir)): start_dir = os.path.abspath(start_dir) if start_dir != top_level_dir: @@ -289,25 +291,51 @@ def discover(self, start_dir, pattern='test*.py', top_level_dir=None): top_part = start_dir.split('.')[0] try: start_dir = os.path.abspath( - os.path.dirname((the_module.__file__))) + os.path.dirname((the_module.__file__))) except AttributeError: - if the_module.__name__ in sys.builtin_module_names: + # look for namespace packages + try: + spec = the_module.__spec__ + except AttributeError: + spec = None + + if spec and spec.loader is None: + if spec.submodule_search_locations is not None: + is_namespace = True + + for path in the_module.__path__: + if (not set_implicit_top and + not path.startswith(top_level_dir)): + continue + self._top_level_dir = \ + (path.split(the_module.__name__ + .replace(".", os.path.sep))[0]) + tests.extend(self._find_tests(path, + pattern, + namespace=True)) + elif the_module.__name__ in sys.builtin_module_names: # builtin module raise TypeError('Can not use builtin modules ' 'as dotted module names') from None else: raise TypeError( - f"don't know how to discover from {the_module!r}" - ) from None + 'don\'t know how to discover from {!r}' + .format(the_module)) from None if set_implicit_top: - self._top_level_dir = self._get_directory_containing_module(top_part) - sys.path.remove(top_level_dir) + if not is_namespace: + self._top_level_dir = \ + self._get_directory_containing_module(top_part) + sys.path.remove(top_level_dir) + else: + sys.path.remove(top_level_dir) if is_not_importable: raise ImportError('Start directory is not importable: %r' % start_dir) - tests = list(self._find_tests(start_dir, pattern)) + if not is_namespace: + tests = list(self._find_tests(start_dir, pattern)) + self._top_level_dir = original_top_level_dir return self.suiteClass(tests) @@ -343,7 +371,7 @@ def _match_path(self, path, full_path, pattern): # override this method to use alternative matching strategy return fnmatch(path, pattern) - def _find_tests(self, start_dir, pattern): + def _find_tests(self, start_dir, pattern, namespace=False): """Used by discovery. Yields test suites it loads.""" # Handle the __init__ in this package name = self._get_name_from_path(start_dir) @@ -352,7 +380,8 @@ def _find_tests(self, start_dir, pattern): if name != '.' and name not in self._loading_packages: # name is in self._loading_packages while we have called into # loadTestsFromModule with name. - tests, should_recurse = self._find_test_path(start_dir, pattern) + tests, should_recurse = self._find_test_path( + start_dir, pattern, namespace) if tests is not None: yield tests if not should_recurse: @@ -363,7 +392,8 @@ def _find_tests(self, start_dir, pattern): paths = sorted(os.listdir(start_dir)) for path in paths: full_path = os.path.join(start_dir, path) - tests, should_recurse = self._find_test_path(full_path, pattern) + tests, should_recurse = self._find_test_path( + full_path, pattern, namespace) if tests is not None: yield tests if should_recurse: @@ -371,11 +401,11 @@ def _find_tests(self, start_dir, pattern): name = self._get_name_from_path(full_path) self._loading_packages.add(name) try: - yield from self._find_tests(full_path, pattern) + yield from self._find_tests(full_path, pattern, namespace) finally: self._loading_packages.discard(name) - def _find_test_path(self, full_path, pattern): + def _find_test_path(self, full_path, pattern, namespace=False): """Used by discovery. Loads tests from a single file, or a directories' __init__.py when @@ -419,7 +449,8 @@ def _find_test_path(self, full_path, pattern): msg % (mod_name, module_dir, expected_dir)) return self.loadTestsFromModule(module, pattern=pattern), False elif os.path.isdir(full_path): - if not os.path.isfile(os.path.join(full_path, '__init__.py')): + if (not namespace and + not os.path.isfile(os.path.join(full_path, '__init__.py'))): return None, False load_tests = None From 8344889261030bd7c790fb18fbbaf3f471ec197e Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 7 Sep 2024 13:27:30 -0400 Subject: [PATCH 02/11] gh-80958: Restore unittest discovery of PEP 420 namespace packages --- Lib/test/test_unittest/test_discovery.py | 8 +++- Lib/unittest/loader.py | 43 ++++++++----------- ...4-09-07-13-57-49.gh-issue-80958.fVYnqV.rst | 1 + 3 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-09-07-13-57-49.gh-issue-80958.fVYnqV.rst diff --git a/Lib/test/test_unittest/test_discovery.py b/Lib/test/test_unittest/test_discovery.py index b5d5b2d8284fca..cc910adc4d6e2f 100644 --- a/Lib/test/test_unittest/test_discovery.py +++ b/Lib/test/test_unittest/test_discovery.py @@ -4,6 +4,7 @@ import sys import types import pickle +from importlib._bootstrap_external import NamespaceLoader from test import support from test.support import import_helper @@ -852,10 +853,13 @@ def test_discovery_from_dotted_namespace_packages(self): loader = unittest.TestLoader() package = types.ModuleType('package') + package.__name__ = "tests" package.__path__ = ['/a', '/b'] + package.__file__ = None package.__spec__ = types.SimpleNamespace( - loader=None, - submodule_search_locations=['/a', '/b'] + name=package.__name__, + loader=NamespaceLoader(package.__name__, package.__path__, None), + submodule_search_locations=['/a', '/b'] ) def _import(packagename, *args, **kwargs): diff --git a/Lib/unittest/loader.py b/Lib/unittest/loader.py index c557e222941e21..d1c36ee38b15fe 100644 --- a/Lib/unittest/loader.py +++ b/Lib/unittest/loader.py @@ -288,47 +288,42 @@ def discover(self, start_dir, pattern='test*.py', top_level_dir=None): is_not_importable = True else: the_module = sys.modules[start_dir] - top_part = start_dir.split('.')[0] - try: - start_dir = os.path.abspath( - os.path.dirname((the_module.__file__))) - except AttributeError: + if not hasattr(the_module, "__file__") or the_module.__file__ is None: # look for namespace packages try: spec = the_module.__spec__ except AttributeError: spec = None - if spec and spec.loader is None: - if spec.submodule_search_locations is not None: - is_namespace = True - - for path in the_module.__path__: - if (not set_implicit_top and - not path.startswith(top_level_dir)): - continue - self._top_level_dir = \ - (path.split(the_module.__name__ - .replace(".", os.path.sep))[0]) - tests.extend(self._find_tests(path, - pattern, - namespace=True)) + if spec and spec.submodule_search_locations is not None: + is_namespace = True + + for path in the_module.__path__: + if (not set_implicit_top and + not path.startswith(top_level_dir)): + continue + self._top_level_dir = \ + (path.split(the_module.__name__ + .replace(".", os.path.sep))[0]) + tests.extend(self._find_tests(path, pattern, namespace=True)) elif the_module.__name__ in sys.builtin_module_names: # builtin module raise TypeError('Can not use builtin modules ' 'as dotted module names') from None else: raise TypeError( - 'don\'t know how to discover from {!r}' - .format(the_module)) from None + f"don't know how to discover from {the_module!r}" + ) from None + + else: + top_part = start_dir.split('.')[0] + start_dir = os.path.abspath(os.path.dirname((the_module.__file__))) if set_implicit_top: if not is_namespace: self._top_level_dir = \ self._get_directory_containing_module(top_part) - sys.path.remove(top_level_dir) - else: - sys.path.remove(top_level_dir) + sys.path.remove(top_level_dir) if is_not_importable: raise ImportError('Start directory is not importable: %r' % start_dir) diff --git a/Misc/NEWS.d/next/Library/2024-09-07-13-57-49.gh-issue-80958.fVYnqV.rst b/Misc/NEWS.d/next/Library/2024-09-07-13-57-49.gh-issue-80958.fVYnqV.rst new file mode 100644 index 00000000000000..1650d3b30018fe --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-09-07-13-57-49.gh-issue-80958.fVYnqV.rst @@ -0,0 +1 @@ +Restore support for unittest discovery of PEP 420 namespace packages. From 525c5bc63ecaf688b478d2a9e631ecc68183723c Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 7 Sep 2024 14:40:41 -0400 Subject: [PATCH 03/11] Attempt to address WASI failure --- Lib/test/test_unittest/test_discovery.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_unittest/test_discovery.py b/Lib/test/test_unittest/test_discovery.py index cc910adc4d6e2f..cbfc5fa3b4cae9 100644 --- a/Lib/test/test_unittest/test_discovery.py +++ b/Lib/test/test_unittest/test_discovery.py @@ -11,6 +11,7 @@ import unittest import unittest.mock import test.test_unittest +from test.test_importlib import util as test_util class TestableTestProgram(unittest.TestProgram): @@ -878,7 +879,7 @@ def _find_tests(start_dir, pattern, namespace=None): # Since loader.discover() can modify sys.path, restore it when done. with import_helper.DirsOnSysPath(): # Make sure to remove 'package' from sys.modules when done. - with test.test_importlib.util.uncache('package'): + with test_util.uncache('package'): suite = loader.discover('package') self.assertEqual(suite, ['/a/tests', '/b/tests']) From 26b090f715881e9b02f9d85feebfb0058b367f92 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 8 Sep 2024 09:10:04 -0400 Subject: [PATCH 04/11] fixup! gh-80958: Restore unittest discovery of PEP 420 namespace packages Improve discovery --- Lib/unittest/loader.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/unittest/loader.py b/Lib/unittest/loader.py index d1c36ee38b15fe..32e945c8ea610e 100644 --- a/Lib/unittest/loader.py +++ b/Lib/unittest/loader.py @@ -321,8 +321,13 @@ def discover(self, start_dir, pattern='test*.py', top_level_dir=None): if set_implicit_top: if not is_namespace: - self._top_level_dir = \ - self._get_directory_containing_module(top_part) + if sys.modules[top_part].__file__ is None: + self._top_level_dir = os.path.dirname(the_module.__file__) + if self._top_level_dir not in sys.path: + sys.path.insert(0, self._top_level_dir) + else: + self._top_level_dir = \ + self._get_directory_containing_module(top_part) sys.path.remove(top_level_dir) if is_not_importable: From 378f855e51257650d36cd0a76d0284a47a29cf6f Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 8 Sep 2024 17:26:19 -0400 Subject: [PATCH 05/11] Prevent discovering submodules under start_dir --- .../namespace_test_pkg/bar/__init__.py | 0 .../namespace_test_pkg/bar/test_bar.py | 5 +++++ .../namespace_test_pkg/noop/no2/__init__.py | 0 .../namespace_test_pkg/noop/no2/test_no2.py | 5 +++++ .../namespace_test_pkg/noop/test_noop.py | 5 +++++ .../test_unittest/namespace_test_pkg/test_foo.py | 5 +++++ Lib/test/test_unittest/test_discovery.py | 14 ++++++++++++++ Lib/unittest/loader.py | 4 ++-- 8 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 Lib/test/test_unittest/namespace_test_pkg/bar/__init__.py create mode 100644 Lib/test/test_unittest/namespace_test_pkg/bar/test_bar.py create mode 100644 Lib/test/test_unittest/namespace_test_pkg/noop/no2/__init__.py create mode 100644 Lib/test/test_unittest/namespace_test_pkg/noop/no2/test_no2.py create mode 100644 Lib/test/test_unittest/namespace_test_pkg/noop/test_noop.py create mode 100644 Lib/test/test_unittest/namespace_test_pkg/test_foo.py diff --git a/Lib/test/test_unittest/namespace_test_pkg/bar/__init__.py b/Lib/test/test_unittest/namespace_test_pkg/bar/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/Lib/test/test_unittest/namespace_test_pkg/bar/test_bar.py b/Lib/test/test_unittest/namespace_test_pkg/bar/test_bar.py new file mode 100644 index 00000000000000..05b184d9eba685 --- /dev/null +++ b/Lib/test/test_unittest/namespace_test_pkg/bar/test_bar.py @@ -0,0 +1,5 @@ +import unittest + +class PassingTest(unittest.TestCase): + def test_true(self): + self.assertTrue(True) diff --git a/Lib/test/test_unittest/namespace_test_pkg/noop/no2/__init__.py b/Lib/test/test_unittest/namespace_test_pkg/noop/no2/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/Lib/test/test_unittest/namespace_test_pkg/noop/no2/test_no2.py b/Lib/test/test_unittest/namespace_test_pkg/noop/no2/test_no2.py new file mode 100644 index 00000000000000..05b184d9eba685 --- /dev/null +++ b/Lib/test/test_unittest/namespace_test_pkg/noop/no2/test_no2.py @@ -0,0 +1,5 @@ +import unittest + +class PassingTest(unittest.TestCase): + def test_true(self): + self.assertTrue(True) diff --git a/Lib/test/test_unittest/namespace_test_pkg/noop/test_noop.py b/Lib/test/test_unittest/namespace_test_pkg/noop/test_noop.py new file mode 100644 index 00000000000000..05b184d9eba685 --- /dev/null +++ b/Lib/test/test_unittest/namespace_test_pkg/noop/test_noop.py @@ -0,0 +1,5 @@ +import unittest + +class PassingTest(unittest.TestCase): + def test_true(self): + self.assertTrue(True) diff --git a/Lib/test/test_unittest/namespace_test_pkg/test_foo.py b/Lib/test/test_unittest/namespace_test_pkg/test_foo.py new file mode 100644 index 00000000000000..05b184d9eba685 --- /dev/null +++ b/Lib/test/test_unittest/namespace_test_pkg/test_foo.py @@ -0,0 +1,5 @@ +import unittest + +class PassingTest(unittest.TestCase): + def test_true(self): + self.assertTrue(True) diff --git a/Lib/test/test_unittest/test_discovery.py b/Lib/test/test_unittest/test_discovery.py index cbfc5fa3b4cae9..38c9779daaf87d 100644 --- a/Lib/test/test_unittest/test_discovery.py +++ b/Lib/test/test_unittest/test_discovery.py @@ -884,6 +884,20 @@ def _find_tests(start_dir, pattern, namespace=None): self.assertEqual(suite, ['/a/tests', '/b/tests']) + def test_discovery_start_dir_is_namespace(self): + """Subdirectory discovery not affected if start_dir is a namespace pkg.""" + loader = unittest.TestLoader() + with ( + import_helper.DirsOnSysPath(os.path.join(os.path.dirname(__file__))), + test_util.uncache('namespace_test_pkg') + ): + suite = loader.discover('namespace_test_pkg') + self.assertEqual( + {list(suite)[0]._tests[0].__module__ for suite in suite._tests if list(suite)}, + # files under namespace_test_pkg.noop not discovered. + {'namespace_test_pkg.test_foo', 'namespace_test_pkg.bar.test_bar'}, + ) + def test_discovery_failed_discovery(self): from test.test_importlib import util diff --git a/Lib/unittest/loader.py b/Lib/unittest/loader.py index 32e945c8ea610e..a52950dad224ee 100644 --- a/Lib/unittest/loader.py +++ b/Lib/unittest/loader.py @@ -393,7 +393,7 @@ def _find_tests(self, start_dir, pattern, namespace=False): for path in paths: full_path = os.path.join(start_dir, path) tests, should_recurse = self._find_test_path( - full_path, pattern, namespace) + full_path, pattern, False) if tests is not None: yield tests if should_recurse: @@ -401,7 +401,7 @@ def _find_tests(self, start_dir, pattern, namespace=False): name = self._get_name_from_path(full_path) self._loading_packages.add(name) try: - yield from self._find_tests(full_path, pattern, namespace) + yield from self._find_tests(full_path, pattern, False) finally: self._loading_packages.discard(name) From 99cf5ea3cbb76bf74f2cb1db70f8a3cbd5f89ce4 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 8 Sep 2024 17:46:59 -0400 Subject: [PATCH 06/11] fixup! Prevent discovering submodules under start_dir --- Makefile.pre.in | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile.pre.in b/Makefile.pre.in index 46733d0cb44f72..2ceb5c344bc983 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -2538,6 +2538,10 @@ TESTSUBDIRS= idlelib/idle_test \ test/test_tools \ test/test_ttk \ test/test_unittest \ + test/test_unittest/namespace_test_pkg \ + test/test_unittest/namespace_test_pkg/bar \ + test/test_unittest/namespace_test_pkg/noop \ + test/test_unittest/namespace_test_pkg/noop/no2 \ test/test_unittest/testmock \ test/test_warnings \ test/test_warnings/data \ From 42ea64216a8e2fc11123e441b2038d9c7b655fee Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 21 Oct 2024 08:34:20 -0400 Subject: [PATCH 07/11] Revert some doc edits, add versionchanged --- Doc/library/unittest.rst | 9 +++++++-- Doc/whatsnew/3.11.rst | 4 ++++ Doc/whatsnew/3.14.rst | 9 +++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index 536c4474c0a70c..26b35567d63b29 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -272,8 +272,7 @@ Test Discovery Unittest supports simple test discovery. In order to be compatible with test discovery, all of the test files must be :ref:`modules ` or -:ref:`packages ` (including :term:`namespace packages -`) importable from the top-level directory of +:ref:`packages ` importable from the top-level directory of the project (this means that their filenames must be valid :ref:`identifiers `). @@ -1911,9 +1910,15 @@ Loading and running tests whether their path matches *pattern*, because it is impossible for a package name to match the default pattern. + .. versionchanged:: 3.11 + *start_dir* can not be a :term:`namespace packages `. + It has been broken since Python 3.7, and Python 3.11 officially removes it. + .. versionchanged:: 3.13 *top_level_dir* is only stored for the duration of *discover* call. + .. versionchanged:: 3.14 + *start_dir* can once again be a :term:`namespace packages `. The following attributes of a :class:`TestLoader` can be configured either by subclassing or assignment on an instance: diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 6525d1c4ca100f..d59f24406c9483 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -2028,6 +2028,10 @@ Removed C APIs are :ref:`listed separately `. * Removed the deprecated :meth:`!split` method of :class:`!_tkinter.TkappType`. (Contributed by Erlend E. Aasland in :issue:`38371`.) +* Removed namespace package support from :mod:`unittest` discovery. + It was introduced in Python 3.4 but has been broken since Python 3.7. + (Contributed by Inada Naoki in :issue:`23882`.) + * Removed the undocumented private :meth:`!float.__set_format__` method, previously known as :meth:`!float.__setformat__` in Python 3.7. Its docstring said: "You probably don't want to use this function. diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index e1bd52370d776c..14c35ce0d6037a 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -233,6 +233,15 @@ symtable (Contributed by Bénédikt Tran in :gh:`120029`.) + +unittest +-------- + +* Restore support for unittest discovery of + :term:`namespace packages `. It was removed in Python + 3.11. + (Contributed by Jacob Walls in :gh:`80958`.) + .. Add improved modules above alphabetically, not here at the end. Optimizations From be8d0e777ad8fe2370b3b0f1e3b0aab357d6847f Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 21 Oct 2024 08:44:36 -0400 Subject: [PATCH 08/11] Fix typo --- Doc/whatsnew/3.14.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 43d4cf86845797..c5793652f7e3a8 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -423,7 +423,7 @@ unittest -------- * Restore support for unittest discovery of - :term:`namespace packages `. It was removed in Python + :term:`namespace packages `. It was removed in Python 3.11. (Contributed by Jacob Walls in :gh:`80958`.) From 57733059d98ef8ca027c8776f4881af2fbebedb5 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 22 Oct 2024 19:53:42 +0900 Subject: [PATCH 09/11] update docs --- Doc/library/unittest.rst | 34 +++++++++++++++---- Doc/whatsnew/3.14.rst | 6 ++-- ...4-09-07-13-57-49.gh-issue-80958.fVYnqV.rst | 2 +- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index 26b35567d63b29..7e5314e35b6902 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -340,10 +340,32 @@ Test modules and packages can customize test loading and discovery by through the `load_tests protocol`_. .. versionchanged:: 3.4 - Test discovery supports :term:`namespace packages ` - for the start directory. Note that you need to specify the top level - directory too (e.g. - ``python -m unittest discover -s root/namespace -t root``). + Test discovery supports :term:`namespace packages `. + +.. versionchanged:: 3.11 + Test discovery dropped the :term:`namespace packages ` + support in Python 3.11. It has been broken since Python 3.7. Start directory and + subdirectories containing tests must be regular package that have + ``__init__.py`` file. + + If the start directory is the dotted name of the package, the ancestor packages + can be namespace packages. + +.. versionchanged:: 3.14 + Test discovery supports :term:`namespace package` as start directory again. + See following example:: + + # Directory tree: + # proj/ + # namespace/ + # mypkg/ + # __init__.py + # test_mypkg.py + + # Supported since Python 3.4 + python -m unittest discover -s namespace.mypkg -t proj + # Supported since Python 3.14. mypkg must not be namespace package. + python -m unittest discover -s namespace -t proj .. _organizing-tests: @@ -1897,10 +1919,8 @@ Loading and running tests Modules that raise :exc:`SkipTest` on import are recorded as skips, not errors. - .. versionchanged:: 3.4 *start_dir* can be a :term:`namespace packages `. - .. versionchanged:: 3.4 Paths are sorted before being imported so that execution order is the same even if the underlying file system's ordering is not dependent on file name. @@ -1918,7 +1938,7 @@ Loading and running tests *top_level_dir* is only stored for the duration of *discover* call. .. versionchanged:: 3.14 - *start_dir* can once again be a :term:`namespace packages `. + *start_dir* can once again be a :term:`namespace package`. The following attributes of a :class:`TestLoader` can be configured either by subclassing or assignment on an instance: diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index c5793652f7e3a8..8468d825769c9d 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -412,7 +412,6 @@ symtable (Contributed by Bénédikt Tran in :gh:`120029`.) - unicodedata ----------- @@ -422,9 +421,8 @@ unicodedata unittest -------- -* Restore support for unittest discovery of - :term:`namespace packages `. It was removed in Python - 3.11. +* unittest discovery supports :term:`namespace package` as start + directory again. It was removed in Python 3.11. (Contributed by Jacob Walls in :gh:`80958`.) diff --git a/Misc/NEWS.d/next/Library/2024-09-07-13-57-49.gh-issue-80958.fVYnqV.rst b/Misc/NEWS.d/next/Library/2024-09-07-13-57-49.gh-issue-80958.fVYnqV.rst index 1650d3b30018fe..f0edd7b1ac6e8b 100644 --- a/Misc/NEWS.d/next/Library/2024-09-07-13-57-49.gh-issue-80958.fVYnqV.rst +++ b/Misc/NEWS.d/next/Library/2024-09-07-13-57-49.gh-issue-80958.fVYnqV.rst @@ -1 +1 @@ -Restore support for unittest discovery of PEP 420 namespace packages. +unittest discovery supports PEP 420 namespace packages as start directory again. From e3e210ef6b0da1515b5bae9c2410f011e9c8cff3 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 22 Oct 2024 08:02:44 -0400 Subject: [PATCH 10/11] [skip ci] Fix typo --- Doc/library/unittest.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index 7e5314e35b6902..4e67b5f13a8823 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -364,7 +364,7 @@ the `load_tests protocol`_. # Supported since Python 3.4 python -m unittest discover -s namespace.mypkg -t proj - # Supported since Python 3.14. mypkg must not be namespace package. + # Supported since Python 3.14. mypkg must not be a namespace package. python -m unittest discover -s namespace -t proj From 2103ba79a64ce0e3ddf6859756f1ae2744daa5a0 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 23 Oct 2024 00:24:26 +0900 Subject: [PATCH 11/11] update doc --- Doc/library/unittest.rst | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index 4e67b5f13a8823..38bad9405597dd 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -344,28 +344,17 @@ the `load_tests protocol`_. .. versionchanged:: 3.11 Test discovery dropped the :term:`namespace packages ` - support in Python 3.11. It has been broken since Python 3.7. Start directory and - subdirectories containing tests must be regular package that have - ``__init__.py`` file. + support. It has been broken since Python 3.7. + Start directory and its subdirectories containing tests must be regular + package that have ``__init__.py`` file. If the start directory is the dotted name of the package, the ancestor packages can be namespace packages. .. versionchanged:: 3.14 Test discovery supports :term:`namespace package` as start directory again. - See following example:: - - # Directory tree: - # proj/ - # namespace/ - # mypkg/ - # __init__.py - # test_mypkg.py - - # Supported since Python 3.4 - python -m unittest discover -s namespace.mypkg -t proj - # Supported since Python 3.14. mypkg must not be a namespace package. - python -m unittest discover -s namespace -t proj + To avoid scanning directories unrelated to Python, + tests are not searched in subdirectories that do not contain ``__init__.py``. .. _organizing-tests: