Skip to content

Commit

Permalink
Issue 17457: extend test discovery to support namespace packages
Browse files Browse the repository at this point in the history
  • Loading branch information
mfoord committed Nov 23, 2013
1 parent 8933521 commit e28bb15
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 11 deletions.
60 changes: 51 additions & 9 deletions Lib/unittest/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ class TestLoader(object):
def loadTestsFromTestCase(self, testCaseClass):
"""Return a suite of all tests cases contained in testCaseClass"""
if issubclass(testCaseClass, suite.TestSuite):
raise TypeError("Test cases should not be derived from TestSuite." \
" Maybe you meant to derive from TestCase?")
raise TypeError("Test cases should not be derived from "
"TestSuite. Maybe you meant to derive from "
"TestCase?")
testCaseNames = self.getTestCaseNames(testCaseClass)
if not testCaseNames and hasattr(testCaseClass, 'runTest'):
testCaseNames = ['runTest']
Expand Down Expand Up @@ -200,6 +201,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:
Expand All @@ -213,15 +216,52 @@ def discover(self, start_dir, pattern='test*.py', top_level_dir=None):
else:
the_module = sys.modules[start_dir]
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:
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))
return self.suiteClass(tests)

def _get_directory_containing_module(self, module_name):
Expand Down Expand Up @@ -254,7 +294,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."""
paths = sorted(os.listdir(start_dir))

Expand Down Expand Up @@ -287,7 +327,8 @@ def _find_tests(self, start_dir, pattern):
raise ImportError(msg % (mod_name, module_dir, expected_dir))
yield self.loadTestsFromModule(module)
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

load_tests = None
Expand All @@ -304,7 +345,8 @@ def _find_tests(self, start_dir, pattern):
# tests loaded from package file
yield tests
# recurse into the package
yield from self._find_tests(full_path, pattern)
yield from self._find_tests(full_path, pattern,
namespace=namespace)
else:
try:
yield load_tests(self, tests, pattern)
Expand Down
80 changes: 78 additions & 2 deletions Lib/unittest/test/test_discovery.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
import re
import sys
import types
import builtins
from test import support

import unittest
Expand Down Expand Up @@ -173,7 +175,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
Expand Down Expand Up @@ -436,7 +438,7 @@ def test_discovery_from_dotted_path(self):
expectedPath = os.path.abspath(os.path.dirname(unittest.test.__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
Expand All @@ -446,5 +448,79 @@ def _find_tests(start_dir, pattern):
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__':
unittest.main()
3 changes: 3 additions & 0 deletions Misc/NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,9 @@ Core and Builtins
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.
Patch by David Edelsohn.

Expand Down
18 changes: 18 additions & 0 deletions Misc/python-wing5.wpr
Original file line number Diff line number Diff line change
@@ -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'

0 comments on commit e28bb15

Please sign in to comment.