Skip to content

Commit

Permalink
pythongh-59215: unittest: restore _top_level_dir at end of discovery (p…
Browse files Browse the repository at this point in the history
  • Loading branch information
ZackerySpytz committed Apr 3, 2024
1 parent ea94b3b commit fc5f68e
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 6 deletions.
15 changes: 10 additions & 5 deletions Doc/library/unittest.rst
Expand Up @@ -1880,8 +1880,8 @@ Loading and running tests
Python identifiers) will be loaded.

All test modules must be importable from the top level of the project. If
the start directory is not the top level directory then the top level
directory must be specified separately.
the start directory is not the top level directory then *top_level_dir*
must be specified separately.

If importing a module fails, for example due to a syntax error, then
this will be recorded as a single error and discovery will continue. If
Expand All @@ -1901,9 +1901,11 @@ Loading and running tests
package.

The pattern is deliberately not stored as a loader attribute so that
packages can continue discovery themselves. *top_level_dir* is stored so
``load_tests`` does not need to pass this argument in to
``loader.discover()``.
packages can continue discovery themselves.

*top_level_dir* is stored internally, and used as a default to any
nested calls to ``discover()``. That is, if a package's ``load_tests``
calls ``loader.discover()``, it does not need to pass this argument.

*start_dir* can be a dotted module name as well as a directory.

Expand All @@ -1930,6 +1932,9 @@ Loading and running tests
*start_dir* can not be a :term:`namespace packages <namespace package>`.
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.


The following attributes of a :class:`TestLoader` can be configured either by
subclassing or assignment on an instance:
Expand Down
26 changes: 25 additions & 1 deletion Lib/test/test_unittest/test_discovery.py
Expand Up @@ -406,10 +406,34 @@ def _find_tests(start_dir, pattern):
top_level_dir = os.path.abspath('/foo/bar')
start_dir = os.path.abspath('/foo/bar/baz')
self.assertEqual(suite, "['tests']")
self.assertEqual(loader._top_level_dir, top_level_dir)
self.assertEqual(loader._top_level_dir, os.path.abspath('/foo'))
self.assertEqual(_find_tests_args, [(start_dir, 'pattern')])
self.assertIn(top_level_dir, sys.path)

def test_discover_should_not_persist_top_level_dir_between_calls(self):
original_isfile = os.path.isfile
original_isdir = os.path.isdir
original_sys_path = sys.path[:]
def restore():
os.path.isfile = original_isfile
os.path.isdir = original_isdir
sys.path[:] = original_sys_path
self.addCleanup(restore)

os.path.isfile = lambda path: True
os.path.isdir = lambda path: True
loader = unittest.TestLoader()
loader.suiteClass = str
dir = '/foo/bar'
top_level_dir = '/foo'

loader.discover(dir, top_level_dir=top_level_dir)
self.assertEqual(loader._top_level_dir, None)

loader._top_level_dir = dir2 = '/previous/dir'
loader.discover(dir, top_level_dir=top_level_dir)
self.assertEqual(loader._top_level_dir, dir2)

def test_discover_start_dir_is_package_calls_package_load_tests(self):
# This test verifies that the package load_tests in a package is indeed
# invoked when the start_dir is a package (and not the top level).
Expand Down
2 changes: 2 additions & 0 deletions Lib/unittest/loader.py
Expand Up @@ -254,6 +254,7 @@ def discover(self, start_dir, pattern='test*.py', top_level_dir=None):
Paths are sorted before being imported to ensure reproducible execution
order even on filesystems with non-alphabetical ordering like ext3/4.
"""
original_top_level_dir = self._top_level_dir
set_implicit_top = False
if top_level_dir is None and self._top_level_dir is not None:
# make top_level_dir optional if called from load_tests in a package
Expand Down Expand Up @@ -307,6 +308,7 @@ def discover(self, start_dir, pattern='test*.py', top_level_dir=None):
raise ImportError('Start directory is not importable: %r' % start_dir)

tests = list(self._find_tests(start_dir, pattern))
self._top_level_dir = original_top_level_dir
return self.suiteClass(tests)

def _get_directory_containing_module(self, module_name):
Expand Down
@@ -0,0 +1,3 @@
:meth:`unittest.TestLoader.discover` now saves the original value of
``unittest.TestLoader._top_level_dir`` and restores it at the end of the
call.

0 comments on commit fc5f68e

Please sign in to comment.