Issue 17457: extend test discovery to support namespace packages

This commit is contained in:
Michael Foord 2013-11-23 13:29:23 +00:00
parent 8933521b3d
commit e28bb15054
4 changed files with 150 additions and 11 deletions

View File

@ -61,8 +61,9 @@ class TestLoader(object):
def loadTestsFromTestCase(self, testCaseClass): def loadTestsFromTestCase(self, testCaseClass):
"""Return a suite of all tests cases contained in testCaseClass""" """Return a suite of all tests cases contained in testCaseClass"""
if issubclass(testCaseClass, suite.TestSuite): if issubclass(testCaseClass, suite.TestSuite):
raise TypeError("Test cases should not be derived from TestSuite." \ raise TypeError("Test cases should not be derived from "
" Maybe you meant to derive from TestCase?") "TestSuite. Maybe you meant to derive from "
"TestCase?")
testCaseNames = self.getTestCaseNames(testCaseClass) testCaseNames = self.getTestCaseNames(testCaseClass)
if not testCaseNames and hasattr(testCaseClass, 'runTest'): if not testCaseNames and hasattr(testCaseClass, 'runTest'):
testCaseNames = ['runTest'] testCaseNames = ['runTest']
@ -200,6 +201,8 @@ class TestLoader(object):
self._top_level_dir = top_level_dir self._top_level_dir = top_level_dir
is_not_importable = False is_not_importable = False
is_namespace = False
tests = []
if os.path.isdir(os.path.abspath(start_dir)): if os.path.isdir(os.path.abspath(start_dir)):
start_dir = os.path.abspath(start_dir) start_dir = os.path.abspath(start_dir)
if start_dir != top_level_dir: if start_dir != top_level_dir:
@ -213,14 +216,51 @@ class TestLoader(object):
else: else:
the_module = sys.modules[start_dir] the_module = sys.modules[start_dir]
top_part = start_dir.split('.')[0] top_part = start_dir.split('.')[0]
start_dir = os.path.abspath(os.path.dirname((the_module.__file__))) try:
start_dir = os.path.abspath(
os.path.dirname((the_module.__file__)))
except AttributeError:
# 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(
'don\'t know how to discover from {!r}'
.format(the_module)) from None
if set_implicit_top: if set_implicit_top:
self._top_level_dir = self._get_directory_containing_module(top_part) 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: if is_not_importable:
raise ImportError('Start directory is not importable: %r' % start_dir) raise ImportError('Start directory is not importable: %r' % start_dir)
if not is_namespace:
tests = list(self._find_tests(start_dir, pattern)) tests = list(self._find_tests(start_dir, pattern))
return self.suiteClass(tests) return self.suiteClass(tests)
@ -254,7 +294,7 @@ class TestLoader(object):
# override this method to use alternative matching strategy # override this method to use alternative matching strategy
return fnmatch(path, pattern) 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.""" """Used by discovery. Yields test suites it loads."""
paths = sorted(os.listdir(start_dir)) paths = sorted(os.listdir(start_dir))
@ -287,7 +327,8 @@ class TestLoader(object):
raise ImportError(msg % (mod_name, module_dir, expected_dir)) raise ImportError(msg % (mod_name, module_dir, expected_dir))
yield self.loadTestsFromModule(module) yield self.loadTestsFromModule(module)
elif os.path.isdir(full_path): 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'))):
continue continue
load_tests = None load_tests = None
@ -304,7 +345,8 @@ class TestLoader(object):
# tests loaded from package file # tests loaded from package file
yield tests yield tests
# recurse into the package # recurse into the package
yield from self._find_tests(full_path, pattern) yield from self._find_tests(full_path, pattern,
namespace=namespace)
else: else:
try: try:
yield load_tests(self, tests, pattern) yield load_tests(self, tests, pattern)

View File

@ -1,6 +1,8 @@
import os import os
import re import re
import sys import sys
import types
import builtins
from test import support from test import support
import unittest import unittest
@ -173,7 +175,7 @@ class TestDiscovery(unittest.TestCase):
self.addCleanup(restore_isdir) self.addCleanup(restore_isdir)
_find_tests_args = [] _find_tests_args = []
def _find_tests(start_dir, pattern): def _find_tests(start_dir, pattern, namespace=None):
_find_tests_args.append((start_dir, pattern)) _find_tests_args.append((start_dir, pattern))
return ['tests'] return ['tests']
loader._find_tests = _find_tests loader._find_tests = _find_tests
@ -436,7 +438,7 @@ class TestDiscovery(unittest.TestCase):
expectedPath = os.path.abspath(os.path.dirname(unittest.test.__file__)) expectedPath = os.path.abspath(os.path.dirname(unittest.test.__file__))
self.wasRun = False self.wasRun = False
def _find_tests(start_dir, pattern): def _find_tests(start_dir, pattern, namespace=None):
self.wasRun = True self.wasRun = True
self.assertEqual(start_dir, expectedPath) self.assertEqual(start_dir, expectedPath)
return tests return tests
@ -446,5 +448,79 @@ class TestDiscovery(unittest.TestCase):
self.assertEqual(suite._tests, tests) self.assertEqual(suite._tests, tests)
def test_discovery_from_dotted_path_builtin_modules(self):
loader = unittest.TestLoader()
listdir = os.listdir
os.listdir = lambda _: ['test_this_does_not_exist.py']
isfile = os.path.isfile
isdir = os.path.isdir
os.path.isdir = lambda _: False
orig_sys_path = sys.path[:]
def restore():
os.path.isfile = isfile
os.path.isdir = isdir
os.listdir = listdir
sys.path[:] = orig_sys_path
self.addCleanup(restore)
with self.assertRaises(TypeError) as cm:
loader.discover('sys')
self.assertEqual(str(cm.exception),
'Can not use builtin modules '
'as dotted module names')
def test_discovery_from_dotted_namespace_packages(self):
loader = unittest.TestLoader()
orig_import = __import__
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
def cleanup():
builtins.__import__ = orig_import
self.addCleanup(cleanup)
builtins.__import__ = _import
_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
suite = loader.discover('package')
self.assertEqual(suite, ['/a/tests', '/b/tests'])
def test_discovery_failed_discovery(self):
loader = unittest.TestLoader()
package = types.ModuleType('package')
orig_import = __import__
def _import(packagename, *args, **kwargs):
sys.modules[packagename] = package
return package
def cleanup():
builtins.__import__ = orig_import
self.addCleanup(cleanup)
builtins.__import__ = _import
with self.assertRaises(TypeError) as cm:
loader.discover('package')
self.assertEqual(str(cm.exception),
'don\'t know how to discover from {!r}'
.format(package))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -479,6 +479,9 @@ Core and Builtins
Library Library
------- -------
- Issue #17457: unittest test discovery now works with namespace packages.
Patch by Claudiu Popa.
- Issue #18235: Fix the sysconfig variables LDSHARED and BLDSHARED under AIX. - Issue #18235: Fix the sysconfig variables LDSHARED and BLDSHARED under AIX.
Patch by David Edelsohn. Patch by David Edelsohn.

18
Misc/python-wing5.wpr Normal file
View File

@ -0,0 +1,18 @@
#!wing
#!version=5.0
##################################################################
# Wing IDE project file #
##################################################################
[project attributes]
proj.directory-list = [{'dirloc': loc('..'),
'excludes': [u'.hg',
u'Lib/unittest/__pycache__',
u'Lib/unittest/test/__pycache__',
u'Lib/__pycache__',
u'build',
u'Doc/build'],
'filter': '*',
'include_hidden': False,
'recursive': True,
'watch_for_changes': True}]
proj.file-type = 'shared'