aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/Lib/unittest/loader.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/unittest/loader.py')
-rw-r--r--Lib/unittest/loader.py122
1 files changed, 94 insertions, 28 deletions
diff --git a/Lib/unittest/loader.py b/Lib/unittest/loader.py
index 808c50eb668..811bedf928a 100644
--- a/Lib/unittest/loader.py
+++ b/Lib/unittest/loader.py
@@ -6,6 +6,7 @@ import sys
import traceback
import types
import functools
+import warnings
from fnmatch import fnmatch
@@ -20,19 +21,22 @@ VALID_MODULE_NAME = re.compile(r'[_a-z]\w*\.py$', re.IGNORECASE)
def _make_failed_import_test(name, suiteClass):
- message = 'Failed to import test module: %s\n%s' % (name, traceback.format_exc())
+ message = 'Failed to import test module: %s\n%s' % (
+ name, traceback.format_exc())
return _make_failed_test('ModuleImportFailure', name, ImportError(message),
- suiteClass)
+ suiteClass, message)
def _make_failed_load_tests(name, exception, suiteClass):
- return _make_failed_test('LoadTestsFailure', name, exception, suiteClass)
+ message = 'Failed to call load_tests:\n%s' % (traceback.format_exc(),)
+ return _make_failed_test(
+ 'LoadTestsFailure', name, exception, suiteClass, message)
-def _make_failed_test(classname, methodname, exception, suiteClass):
+def _make_failed_test(classname, methodname, exception, suiteClass, message):
def testFailure(self):
raise exception
attrs = {methodname: testFailure}
TestClass = type(classname, (case.TestCase,), attrs)
- return suiteClass((TestClass(methodname),))
+ return suiteClass((TestClass(methodname),)), message
def _make_skipped_test(methodname, exception, suiteClass):
@case.skip(str(exception))
@@ -58,6 +62,10 @@ class TestLoader(object):
suiteClass = suite.TestSuite
_top_level_dir = None
+ def __init__(self):
+ super(TestLoader, self).__init__()
+ self.errors = []
+
def loadTestsFromTestCase(self, testCaseClass):
"""Return a suite of all tests cases contained in testCaseClass"""
if issubclass(testCaseClass, suite.TestSuite):
@@ -70,8 +78,30 @@ class TestLoader(object):
loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames))
return loaded_suite
- def loadTestsFromModule(self, module, use_load_tests=True):
+ # XXX After Python 3.5, remove backward compatibility hacks for
+ # use_load_tests deprecation via *args and **kws. See issue 16662.
+ def loadTestsFromModule(self, module, *args, pattern=None, **kws):
"""Return a suite of all tests cases contained in the given module"""
+ # This method used to take an undocumented and unofficial
+ # use_load_tests argument. For backward compatibility, we still
+ # accept the argument (which can also be the first position) but we
+ # ignore it and issue a deprecation warning if it's present.
+ if len(args) > 0 or 'use_load_tests' in kws:
+ warnings.warn('use_load_tests is deprecated and ignored',
+ DeprecationWarning)
+ kws.pop('use_load_tests', None)
+ if len(args) > 1:
+ # Complain about the number of arguments, but don't forget the
+ # required `module` argument.
+ complaint = len(args) + 1
+ raise TypeError('loadTestsFromModule() takes 1 positional argument but {} were given'.format(complaint))
+ if len(kws) != 0:
+ # Since the keyword arguments are unsorted (see PEP 468), just
+ # pick the alphabetically sorted first argument to complain about,
+ # if multiple were given. At least the error message will be
+ # predictable.
+ complaint = sorted(kws)[0]
+ raise TypeError("loadTestsFromModule() got an unexpected keyword argument '{}'".format(complaint))
tests = []
for name in dir(module):
obj = getattr(module, name)
@@ -80,12 +110,14 @@ class TestLoader(object):
load_tests = getattr(module, 'load_tests', None)
tests = self.suiteClass(tests)
- if use_load_tests and load_tests is not None:
+ if load_tests is not None:
try:
- return load_tests(self, tests, None)
+ return load_tests(self, tests, pattern)
except Exception as e:
- return _make_failed_load_tests(module.__name__, e,
- self.suiteClass)
+ error_case, error_message = _make_failed_load_tests(
+ module.__name__, e, self.suiteClass)
+ self.errors.append(error_message)
+ return error_case
return tests
def loadTestsFromName(self, name, module=None):
@@ -98,20 +130,47 @@ class TestLoader(object):
The method optionally resolves the names relative to a given module.
"""
parts = name.split('.')
+ error_case, error_message = None, None
if module is None:
parts_copy = parts[:]
while parts_copy:
try:
- module = __import__('.'.join(parts_copy))
+ module_name = '.'.join(parts_copy)
+ module = __import__(module_name)
break
except ImportError:
- del parts_copy[-1]
+ next_attribute = parts_copy.pop()
+ # Last error so we can give it to the user if needed.
+ error_case, error_message = _make_failed_import_test(
+ next_attribute, self.suiteClass)
if not parts_copy:
- raise
+ # Even the top level import failed: report that error.
+ self.errors.append(error_message)
+ return error_case
parts = parts[1:]
obj = module
for part in parts:
- parent, obj = obj, getattr(obj, part)
+ try:
+ parent, obj = obj, getattr(obj, part)
+ except AttributeError as e:
+ # We can't traverse some part of the name.
+ if (getattr(obj, '__path__', None) is not None
+ and error_case is not None):
+ # This is a package (no __path__ per importlib docs), and we
+ # encountered an error importing something. We cannot tell
+ # the difference between package.WrongNameTestClass and
+ # package.wrong_module_name so we just report the
+ # ImportError - it is more informative.
+ self.errors.append(error_message)
+ return error_case
+ else:
+ # Otherwise, we signal that an AttributeError has occurred.
+ error_case, error_message = _make_failed_test(
+ 'AttributeError', part, e, self.suiteClass,
+ 'Failed to access attribute:\n%s' % (
+ traceback.format_exc(),))
+ self.errors.append(error_message)
+ return error_case
if isinstance(obj, types.ModuleType):
return self.loadTestsFromModule(obj)
@@ -313,7 +372,10 @@ class TestLoader(object):
except case.SkipTest as e:
yield _make_skipped_test(name, e, self.suiteClass)
except:
- yield _make_failed_import_test(name, self.suiteClass)
+ error_case, error_message = \
+ _make_failed_import_test(name, self.suiteClass)
+ self.errors.append(error_message)
+ yield error_case
else:
mod_file = os.path.abspath(getattr(module, '__file__', full_path))
realpath = _jython_aware_splitext(os.path.realpath(mod_file))
@@ -325,7 +387,7 @@ class TestLoader(object):
msg = ("%r module incorrectly imported from %r. Expected %r. "
"Is this module globally installed?")
raise ImportError(msg % (mod_name, module_dir, expected_dir))
- yield self.loadTestsFromModule(module)
+ yield self.loadTestsFromModule(module, pattern=pattern)
elif os.path.isdir(full_path):
if (not namespace and
not os.path.isfile(os.path.join(full_path, '__init__.py'))):
@@ -333,26 +395,30 @@ class TestLoader(object):
load_tests = None
tests = None
- if fnmatch(path, pattern):
- # only check load_tests if the package directory itself matches the filter
- name = self._get_name_from_path(full_path)
+ name = self._get_name_from_path(full_path)
+ try:
package = self._get_module_from_name(name)
+ except case.SkipTest as e:
+ yield _make_skipped_test(name, e, self.suiteClass)
+ except:
+ error_case, error_message = \
+ _make_failed_import_test(name, self.suiteClass)
+ self.errors.append(error_message)
+ yield error_case
+ else:
load_tests = getattr(package, 'load_tests', None)
- tests = self.loadTestsFromModule(package, use_load_tests=False)
-
- if load_tests is None:
+ tests = self.loadTestsFromModule(package, pattern=pattern)
if tests is not None:
# tests loaded from package file
yield tests
+
+ if load_tests is not None:
+ # loadTestsFromModule(package) has load_tests for us.
+ continue
# recurse into the package
yield from self._find_tests(full_path, pattern,
namespace=namespace)
- else:
- try:
- yield load_tests(self, tests, pattern)
- except Exception as e:
- yield _make_failed_load_tests(package.__name__, e,
- self.suiteClass)
+
defaultTestLoader = TestLoader()