8000 bpo-23882: unittest: Drop PEP 420 support from discovery. (GH-29745) · python/cpython@0b2b9d2 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0b2b9d2

Browse files
authored
bpo-23882: unittest: Drop PEP 420 support from discovery. (GH-29745)
1 parent 1bee9a4 commit 0b2b9d2

File tree

5 files changed

+44
-78
lines changed

5 files changed

+44
-78
lines changed

Doc/library/unittest.rst

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,7 @@ Test Discovery
266266

267267
Unittest supports simple test discovery. In order to be compatible with test
268268
discovery, all of the test files must be :ref:`modules <tut-modules>` or
269-
:ref:`packages <tut-packages>` (including :term:`namespace packages
270-
<namespace package>`) importable from the top-level directory of
269+
:ref:`packages <tut-packages>` importable from the top-level directory of
271270
the project (this means that their filenames must be valid :ref:`identifiers
272271
<identifiers>`).
273272

@@ -340,6 +339,24 @@ the `load_tests protocol`_.
340339
directory too (e.g.
341340
``python -m unittest discover -s root/namespace -t root``).
342341

342+
.. versionchanged:: 3.11
343+
Python 3.11 dropped the :term:`namespace packages <namespace package>`
344+
support. It has been broken since Python 3.7. Start directory and
345+
subdirectories containing tests must be regular package that have
346+
``__init__.py`` file.
347+
348+
Directories containing start directory still can be a namespace package.
349+
In this case, you need to specify start directory as dotted package name,
350+
and target directory explicitly. For example::
351+
352+
# proj/ <-- current directory
353+
# namespace/
354+
# mypkg/
355+
# __init__.py
356+
# test_mypkg.py
357+
358+
python -m unittest discover -s namespace.mypkg -t .
359+
343360

344361
.. _organizing-tests:
345362

@@ -1858,6 +1875,10 @@ Loading and running tests
18581875
whether their path matches *pattern*, because it is impossible for
18591876
a package name to match the default pattern.
18601877

1878+
.. versionchanged:: 3.11
1879+
*start_dir* can not be a :term:`namespace packages <namespace package>`.
1880+
It has been broken since Python 3.7 and Python 3.11 officially remove it.
1881+
18611882

18621883
The following attributes of a :class:`TestLoader` can be configured either by
18631884
subclassing or assignment on an instance:

Doc/whatsnew/3.11.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,10 @@ Removed
542542

543543
(Contributed by Hugo van Kemenade in :issue:`45320`.)
544544

545+
* Remove namespace package support from unittest discovery. It was introduced in
546+
Python 3.4 but has been broken since Python 3.7.
547+
(Contributed by Inada Naoki in :issue:`23882`.)
548+
545549

546550
Porting to Python 3.11
547551
======================

Lib/unittest/loader.py

Lines changed: 13 additions & 43 deletions
EF5E
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,6 @@ def discover(self, start_dir, pattern='test*.py', top_level_dir=None):
264264
self._top_level_dir = top_level_dir
265265

266266
is_not_importable = False
267-
is_namespace = False
268-
tests = []
269267
if os.path.isdir(os.path.abspath(start_dir)):
270268
start_dir = os.path.abspath(start_dir)
271269
if start_dir != top_level_dir:
@@ -281,50 +279,25 @@ def discover(self, start_dir, pattern='test*.py', top_level_dir=None):
281279
top_part = start_dir.split('.')[0]
282280
try:
283281
start_dir = os.path.abspath(
284-
os.path.dirname((the_module.__file__)))
282+
os.path.dirname((the_module.__file__)))
285283
except AttributeError:
286-
# look for namespace packages
287-
try:
288-
spec = the_module.__spec__
289-
except AttributeError:
290-
spec = None
291-
292-
if spec and spec.loader is None:
293-
if spec.submodule_search_locations is not None:
294-
is_namespace = True
295-
296-
for path in the_module.__path__:
297-
if (not set_implicit_top and
298-
not path.startswith(top_level_dir)):
299-
continue
300-
self._top_level_dir = \
301-
(path.split(the_module.__name__
302-
.replace(".", os.path.sep))[0])
303-
tests.extend(self._find_tests(path,
304-
pattern,
305-
namespace=True))
306-
elif the_module.__name__ in sys.builtin_module_names:
284+
if the_module.__name__ in sys.builtin_module_names:
307285
# builtin module
308286
raise TypeError('Can not use builtin modules '
309287
'as dotted module names') from None
310288
else:
311289
raise TypeError(
312-
'don\'t know how to discover from {!r}'
313-
.format(the_module)) from None
290+
F438 f"don't know how to discover from {the_module!r}"
291+
) from None
314292

315293
if set_implicit_top:
316-
if not is_namespace:
317-
self._top_level_dir = \
318-
self._get_directory_containing_module(top_part)
319-
sys.path.remove(top_level_dir)
320-
else:
321-
sys.path.remove(top_level_dir)
294+
self._top_level_dir = self._get_directory_containing_module(top_part)
295+
sys.path.remove(top_level_dir)
322296

323297
if is_not_importable:
324298
raise ImportError('Start directory is not importable: %r' % start_dir)
325299

326-
if not is_namespace:
327-
tests = list(self._find_tests(start_dir, pattern))
300+
tests = list(self._find_tests(start_dir, pattern))
328301
return self.suiteClass(tests)
329302

330303
def _get_directory_containing_module(self, module_name):
@@ -359,7 +332,7 @@ def _match_path(self, path, full_path, pattern):
359332
# override this method to use alternative matching strategy
360333
return fnmatch(path, pattern)
361334

362-
def _find_tests(self, start_dir, pattern, namespace=False):
335+
def _find_tests(self, start_dir, pattern):
363336
"""Used by discovery. Yields test suites it loads."""
364337
# Handle the __init__ in this package
365338
name = self._get_name_from_path(start_dir)
@@ -368,8 +341,7 @@ def _find_tests(self, start_dir, pattern, namespace=False):
368341
if name != '.' and name not in self._loading_packages:
369342
# name is in self._loading_packages while we have called into
370343
# loadTestsFromModule with name.
371-
tests, should_recurse = self._find_test_path(
372-
start_dir, pattern, namespace)
344+
tests, should_recurse = self._find_test_path(start_dir, pattern)
373345
if tests is not None:
374346
yield tests
375347
if not should_recurse:
@@ -380,20 +352,19 @@ def _find_tests(self, start_dir, pattern, namespace=False):
380352
paths = sorted(os.listdir(start_dir))
381353
for path in paths:
382354
full_path = os.path.join(start_dir, path)
383-
tests, should_recurse = self._find_test_path(
384-
full_path, pattern, namespace)
355+
tests, should_recurse = self._find_test_path(full_path, pattern)
385356
if tests is not None:
386357
yield tests
387358
if should_recurse:
388359
# we found a package that didn't use load_tests.
389360
name = self._get_name_from_path(full_path)
390361
self._loading_packages.add(name)
391362
try:
392-
yield from self._find_tests(full_path, pattern, namespace)
363+
yield from self._find_tests(full_path, pattern)
393364
finally:
394365
self._loading_packages.discard(name)
395366

396-
def _find_test_path(self, full_path, pattern, namespace=False):
367+
def _find_test_path(self, full_path, pattern):
397368
"""Used by discovery.
398369
399370
Loads tests from a single file, or a directories' __init__.py when
@@ -437,8 +408,7 @@ def _find_test_path(self, full_path, pattern, namespace=False):
437408
msg % (mod_name, module_dir, expected_dir))
438409
return self.loadTestsFromModule(module, pattern=pattern), False
439410
elif os.path.isdir(full_path):
440-
if (not namespace and
441-
not os.path.isfile(os.path.join(full_path, '__init__.py'))):
411+
if not os.path.isfile(os.path.join(full_path, '__init__.py')):
442412
return None, False
443413

444414
load_tests = None

Lib/unittest/test/test_discovery.py

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ def restore_isdir():
396396
self.addCleanup(restore_isdir)
397397

398398
_find_tests_args = []
399-
def _find_tests(start_dir, pattern, namespace=None):
399+
def _find_tests(start_dir, pattern):
400400
_find_tests_args.append((start_dir, pattern))
401401
return ['tests']
402402
loader._find_tests = _find_tests
@@ -792,7 +792,7 @@ def test_discovery_from_dotted_path(self):
792792
expectedPath = os.path.abspath(os.path.dirname(unittest.test.__file__))
793793

794794
self.wasRun = False
795-
def _find_tests(start_dir, pattern, namespace=None):
795+
def _find_tests(start_dir, pattern):
796796
self.wasRun = True
797797
self.assertEqual(start_dir, expectedPath)
798798
return tests
@@ -825,37 +825,6 @@ def restore():
825825
'Can not use builtin modules '
826826
'as dotted module names')
827827

828-
def test_discovery_from_dotted_namespace_packages(self):
829-
loader = unittest.TestLoader()
830-
831-
package = types.ModuleType('package')
832-
package.__path__ = ['/a', '/b']
833-
package.__spec__ = types.SimpleNamespace(
834-
loader=None,
835-
submodule_search_locations=['/a', '/b']
836-
)
837-
838-
def _import(packagename, *args, **kwargs):
839-
sys.modules[packagename] = package
840-
return package
841-
842-
_find_tests_args = []
843-
def _find_tests(start_dir, pattern, namespace=None):
844-
_find_tests_args.append((start_dir, pattern))
845-
return ['%s/tests' % start_dir]
846-
847-
loader._find_tests = _find_tests
848-
loader.suiteClass = list
849-
850-
with unittest.mock.patch('builtins.__import__', _import):
851-
# Since loader.discover() can modify sys.path, restore it when done.
852-
with import_helper.DirsOnSysPath():
853-
# Make sure to remove 'package' from sys.modules when done.
854-
with test.test_importlib.util.uncache('package'):
855-
suite = loader.discover('package')
856-
857-
self.assertEqual(suite, ['/a/tests', '/b/tests'])
858-
859828
def test_discovery_failed_discovery(self):
860829
loader = unittest.TestLoader()
861830
package = types.ModuleType('package')
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Remove namespace package (PEP 420) support from unittest discovery. It was
2+
introduced in Python 3.4 but has been broken since Python 3.7.

0 commit comments

Comments
 (0)
0