Commit d78742a2 authored by Barry Warsaw's avatar Barry Warsaw

- Issue #16662: load_tests() is now unconditionally run when it is present in

  a package's __init__.py.  TestLoader.loadTestsFromModule() still accepts
  use_load_tests, but it is deprecated and ignored.  A new keyword-only
  attribute `pattern` is added and documented.  Patch given by Robert Collins,
  tweaked by Barry Warsaw.
parent 238f5aa6
...@@ -1561,7 +1561,7 @@ Loading and running tests ...@@ -1561,7 +1561,7 @@ Loading and running tests
:class:`testCaseClass`. :class:`testCaseClass`.
.. method:: loadTestsFromModule(module) .. method:: loadTestsFromModule(module, pattern=None)
Return a suite of all tests cases contained in the given module. This Return a suite of all tests cases contained in the given module. This
method searches *module* for classes derived from :class:`TestCase` and method searches *module* for classes derived from :class:`TestCase` and
...@@ -1578,11 +1578,18 @@ Loading and running tests ...@@ -1578,11 +1578,18 @@ Loading and running tests
If a module provides a ``load_tests`` function it will be called to If a module provides a ``load_tests`` function it will be called to
load the tests. This allows modules to customize test loading. load the tests. This allows modules to customize test loading.
This is the `load_tests protocol`_. This is the `load_tests protocol`_. The *pattern* argument is passed as
the third argument to ``load_tests``.
.. versionchanged:: 3.2 .. versionchanged:: 3.2
Support for ``load_tests`` added. Support for ``load_tests`` added.
.. versionchanged:: 3.5
The undocumented and unofficial *use_load_tests* default argument is
deprecated and ignored, although it is still accepted for backward
compatibility. The method also now accepts a keyword-only argument
*pattern* which is passed to ``load_tests`` as the third argument.
.. method:: loadTestsFromName(name, module=None) .. method:: loadTestsFromName(name, module=None)
...@@ -1634,18 +1641,18 @@ Loading and running tests ...@@ -1634,18 +1641,18 @@ Loading and running tests
the start directory is not the top level directory then the top level the start directory is not the top level directory then the top level
directory must be specified separately. directory must be specified separately.
If importing a module fails, for example due to a syntax error, then this If importing a module fails, for example due to a syntax error, then
will be recorded as a single error and discovery will continue. If the this will be recorded as a single error and discovery will continue. If
import failure is due to :exc:`SkipTest` being raised, it will be recorded the import failure is due to :exc:`SkipTest` being raised, it will be
as a skip instead of an error. recorded as a skip instead of an error.
If a test package name (directory with :file:`__init__.py`) matches the If a package (a directory containing a file named :file:`__init__.py`) is
pattern then the package will be checked for a ``load_tests`` found, the package will be checked for a ``load_tests`` function. If this
function. If this exists then it will be called with *loader*, *tests*, exists then it will be called with *loader*, *tests*, *pattern*.
*pattern*.
If load_tests exists then discovery does *not* recurse into the package, If ``load_tests`` exists then discovery does *not* recurse into the
``load_tests`` is responsible for loading all tests in the package. package, ``load_tests`` is responsible for loading all tests in the
package.
The pattern is deliberately not stored as a loader attribute so that The pattern is deliberately not stored as a loader attribute so that
packages can continue discovery themselves. *top_level_dir* is stored so packages can continue discovery themselves. *top_level_dir* is stored so
...@@ -1664,6 +1671,11 @@ Loading and running tests ...@@ -1664,6 +1671,11 @@ Loading and running tests
the same even if the underlying file system's ordering is not the same even if the underlying file system's ordering is not
dependent on file name. dependent on file name.
.. versionchanged:: 3.5
Found packages are now checked for ``load_tests`` regardless of
whether their path matches *pattern*, because it is impossible for
a package name to match the default pattern.
The following attributes of a :class:`TestLoader` can be configured either by The following attributes of a :class:`TestLoader` can be configured either by
subclassing or assignment on an instance: subclassing or assignment on an instance:
...@@ -2032,7 +2044,10 @@ test runs or test discovery by implementing a function called ``load_tests``. ...@@ -2032,7 +2044,10 @@ test runs or test discovery by implementing a function called ``load_tests``.
If a test module defines ``load_tests`` it will be called by If a test module defines ``load_tests`` it will be called by
:meth:`TestLoader.loadTestsFromModule` with the following arguments:: :meth:`TestLoader.loadTestsFromModule` with the following arguments::
load_tests(loader, standard_tests, None) load_tests(loader, standard_tests, pattern)
where *pattern* is passed straight through from ``loadTestsFromModule``. It
defaults to ``None``.
It should return a :class:`TestSuite`. It should return a :class:`TestSuite`.
...@@ -2054,21 +2069,12 @@ A typical ``load_tests`` function that loads tests from a specific set of ...@@ -2054,21 +2069,12 @@ A typical ``load_tests`` function that loads tests from a specific set of
suite.addTests(tests) suite.addTests(tests)
return suite return suite
If discovery is started, either from the command line or by calling If discovery is started in a directory containing a package, either from the
:meth:`TestLoader.discover`, with a pattern that matches a package command line or by calling :meth:`TestLoader.discover`, then the package
name then the package :file:`__init__.py` will be checked for ``load_tests``. :file:`__init__.py` will be checked for ``load_tests``. If that function does
not exist, discovery will recurse into the package as though it were just
.. note:: another directory. Otherwise, discovery of the package's tests will be left up
to ``load_tests`` which is called with the following arguments::
The default pattern is ``'test*.py'``. This matches all Python files
that start with ``'test'`` but *won't* match any test directories.
A pattern like ``'test*'`` will match test packages as well as
modules.
If the package :file:`__init__.py` defines ``load_tests`` then it will be
called and discovery not continued into the package. ``load_tests``
is called with the following arguments::
load_tests(loader, standard_tests, pattern) load_tests(loader, standard_tests, pattern)
...@@ -2087,6 +2093,11 @@ continue (and potentially modify) test discovery. A 'do nothing' ...@@ -2087,6 +2093,11 @@ continue (and potentially modify) test discovery. A 'do nothing'
standard_tests.addTests(package_tests) standard_tests.addTests(package_tests)
return standard_tests return standard_tests
.. versionchanged:: 3.5
Discovery no longer checks package names for matching *pattern* due to the
impossibility of package names matching the default pattern.
Class and Module Fixtures Class and Module Fixtures
------------------------- -------------------------
......
...@@ -6,6 +6,7 @@ import sys ...@@ -6,6 +6,7 @@ import sys
import traceback import traceback
import types import types
import functools import functools
import warnings
from fnmatch import fnmatch from fnmatch import fnmatch
...@@ -70,8 +71,27 @@ class TestLoader(object): ...@@ -70,8 +71,27 @@ class TestLoader(object):
loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames)) loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames))
return loaded_suite 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""" """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) == 1 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:
raise TypeError('loadTestsFromModule() takes 1 positional argument but {} were given'.format(len(args)))
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 = [] tests = []
for name in dir(module): for name in dir(module):
obj = getattr(module, name) obj = getattr(module, name)
...@@ -80,9 +100,9 @@ class TestLoader(object): ...@@ -80,9 +100,9 @@ class TestLoader(object):
load_tests = getattr(module, 'load_tests', None) load_tests = getattr(module, 'load_tests', None)
tests = self.suiteClass(tests) tests = self.suiteClass(tests)
if use_load_tests and load_tests is not None: if load_tests is not None:
try: try:
return load_tests(self, tests, None) return load_tests(self, tests, pattern)
except Exception as e: except Exception as e:
return _make_failed_load_tests(module.__name__, e, return _make_failed_load_tests(module.__name__, e,
self.suiteClass) self.suiteClass)
...@@ -325,7 +345,7 @@ class TestLoader(object): ...@@ -325,7 +345,7 @@ class TestLoader(object):
msg = ("%r module incorrectly imported from %r. Expected %r. " msg = ("%r module incorrectly imported from %r. Expected %r. "
"Is this module globally installed?") "Is this module globally installed?")
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, pattern=pattern)
elif os.path.isdir(full_path): elif os.path.isdir(full_path):
if (not namespace and if (not namespace and
not os.path.isfile(os.path.join(full_path, '__init__.py'))): not os.path.isfile(os.path.join(full_path, '__init__.py'))):
...@@ -333,26 +353,27 @@ class TestLoader(object): ...@@ -333,26 +353,27 @@ class TestLoader(object):
load_tests = None load_tests = None
tests = None tests = None
if fnmatch(path, pattern): name = self._get_name_from_path(full_path)
# only check load_tests if the package directory itself matches the filter try:
name = self._get_name_from_path(full_path)
package = self._get_module_from_name(name) package = self._get_module_from_name(name)
except case.SkipTest as e:
yield _make_skipped_test(name, e, self.suiteClass)
except:
yield _make_failed_import_test(name, self.suiteClass)
else:
load_tests = getattr(package, 'load_tests', None) load_tests = getattr(package, 'load_tests', None)
tests = self.loadTestsFromModule(package, use_load_tests=False) tests = self.loadTestsFromModule(package, pattern=pattern)
if load_tests is None:
if tests is not None: if tests is not None:
# tests loaded from package file # tests loaded from package file
yield tests yield tests
if load_tests is not None:
# loadTestsFromModule(package) has load_tests for us.
continue
# 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) 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() defaultTestLoader = TestLoader()
......
This diff is collapsed.
import sys import sys
import types import types
import warnings
import unittest import unittest
# Decorator used in the deprecation tests to reset the warning registry for
# test isolation and reproducibility.
def warningregistry(func):
def wrapper(*args, **kws):
missing = object()
saved = getattr(warnings, '__warningregistry__', missing).copy()
try:
return func(*args, **kws)
finally:
if saved is missing:
try:
del warnings.__warningregistry__
except AttributeError:
pass
else:
warnings.__warningregistry__ = saved
class Test_TestLoader(unittest.TestCase): class Test_TestLoader(unittest.TestCase):
...@@ -150,6 +167,7 @@ class Test_TestLoader(unittest.TestCase): ...@@ -150,6 +167,7 @@ class Test_TestLoader(unittest.TestCase):
# Check that loadTestsFromModule honors (or not) a module # Check that loadTestsFromModule honors (or not) a module
# with a load_tests function. # with a load_tests function.
@warningregistry
def test_loadTestsFromModule__load_tests(self): def test_loadTestsFromModule__load_tests(self):
m = types.ModuleType('m') m = types.ModuleType('m')
class MyTestCase(unittest.TestCase): class MyTestCase(unittest.TestCase):
...@@ -168,10 +186,139 @@ class Test_TestLoader(unittest.TestCase): ...@@ -168,10 +186,139 @@ class Test_TestLoader(unittest.TestCase):
suite = loader.loadTestsFromModule(m) suite = loader.loadTestsFromModule(m)
self.assertIsInstance(suite, unittest.TestSuite) self.assertIsInstance(suite, unittest.TestSuite)
self.assertEqual(load_tests_args, [loader, suite, None]) self.assertEqual(load_tests_args, [loader, suite, None])
# With Python 3.5, the undocumented and unofficial use_load_tests is
# ignored (and deprecated).
load_tests_args = []
with warnings.catch_warnings(record=False):
warnings.simplefilter('never')
suite = loader.loadTestsFromModule(m, use_load_tests=False)
self.assertEqual(load_tests_args, [loader, suite, None])
@warningregistry
def test_loadTestsFromModule__use_load_tests_deprecated_positional(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always')
m = types.ModuleType('m')
class MyTestCase(unittest.TestCase):
def test(self):
pass
m.testcase_1 = MyTestCase
load_tests_args = []
def load_tests(loader, tests, pattern):
self.assertIsInstance(tests, unittest.TestSuite)
load_tests_args.extend((loader, tests, pattern))
return tests
m.load_tests = load_tests
# The method still works.
loader = unittest.TestLoader()
# use_load_tests=True as a positional argument.
suite = loader.loadTestsFromModule(m, False)
self.assertIsInstance(suite, unittest.TestSuite)
# load_tests was still called because use_load_tests is deprecated
# and ignored.
self.assertEqual(load_tests_args, [loader, suite, None])
# We got a warning.
self.assertIs(w[-1].category, DeprecationWarning)
self.assertEqual(str(w[-1].message),
'use_load_tests is deprecated and ignored')
@warningregistry
def test_loadTestsFromModule__use_load_tests_deprecated_keyword(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always')
m = types.ModuleType('m')
class MyTestCase(unittest.TestCase):
def test(self):
pass
m.testcase_1 = MyTestCase
load_tests_args = []
def load_tests(loader, tests, pattern):
self.assertIsInstance(tests, unittest.TestSuite)
load_tests_args.extend((loader, tests, pattern))
return tests
m.load_tests = load_tests
# The method still works.
loader = unittest.TestLoader()
suite = loader.loadTestsFromModule(m, use_load_tests=False)
self.assertIsInstance(suite, unittest.TestSuite)
# load_tests was still called because use_load_tests is deprecated
# and ignored.
self.assertEqual(load_tests_args, [loader, suite, None])
# We got a warning.
self.assertIs(w[-1].category, DeprecationWarning)
self.assertEqual(str(w[-1].message),
'use_load_tests is deprecated and ignored')
def test_loadTestsFromModule__too_many_positional_args(self):
m = types.ModuleType('m')
class MyTestCase(unittest.TestCase):
def test(self):
pass
m.testcase_1 = MyTestCase
load_tests_args = []
def load_tests(loader, tests, pattern):
self.assertIsInstance(tests, unittest.TestSuite)
load_tests_args.extend((loader, tests, pattern))
return tests
m.load_tests = load_tests
loader = unittest.TestLoader()
with self.assertRaises(TypeError) as cm:
loader.loadTestsFromModule(m, False, 'testme.*')
self.assertEqual(type(cm.exception), TypeError)
# The error message names the first bad argument alphabetically,
# however use_load_tests (which sorts first) is ignored.
self.assertEqual(
str(cm.exception),
'loadTestsFromModule() takes 1 positional argument but 2 were given')
@warningregistry
def test_loadTestsFromModule__use_load_tests_other_bad_keyword(self):
m = types.ModuleType('m')
class MyTestCase(unittest.TestCase):
def test(self):
pass
m.testcase_1 = MyTestCase
load_tests_args = []
def load_tests(loader, tests, pattern):
self.assertIsInstance(tests, unittest.TestSuite)
load_tests_args.extend((loader, tests, pattern))
return tests
m.load_tests = load_tests
loader = unittest.TestLoader()
with warnings.catch_warnings():
warnings.simplefilter('never')
with self.assertRaises(TypeError) as cm:
loader.loadTestsFromModule(
m, use_load_tests=False, very_bad=True, worse=False)
self.assertEqual(type(cm.exception), TypeError)
# The error message names the first bad argument alphabetically,
# however use_load_tests (which sorts first) is ignored.
self.assertEqual(
str(cm.exception),
"loadTestsFromModule() got an unexpected keyword argument 'very_bad'")
def test_loadTestsFromModule__pattern(self):
m = types.ModuleType('m')
class MyTestCase(unittest.TestCase):
def test(self):
pass
m.testcase_1 = MyTestCase
load_tests_args = [] load_tests_args = []
suite = loader.loadTestsFromModule(m, use_load_tests=False) def load_tests(loader, tests, pattern):
self.assertEqual(load_tests_args, []) self.assertIsInstance(tests, unittest.TestSuite)
load_tests_args.extend((loader, tests, pattern))
return tests
m.load_tests = load_tests
loader = unittest.TestLoader()
suite = loader.loadTestsFromModule(m, pattern='testme.*')
self.assertIsInstance(suite, unittest.TestSuite)
self.assertEqual(load_tests_args, [loader, suite, 'testme.*'])
def test_loadTestsFromModule__faulty_load_tests(self): def test_loadTestsFromModule__faulty_load_tests(self):
m = types.ModuleType('m') m = types.ModuleType('m')
......
...@@ -132,6 +132,12 @@ Core and Builtins ...@@ -132,6 +132,12 @@ Core and Builtins
Library Library
------- -------
- Issue #16662: load_tests() is now unconditionally run when it is present in
a package's __init__.py. TestLoader.loadTestsFromModule() still accepts
use_load_tests, but it is deprecated and ignored. A new keyword-only
attribute `pattern` is added and documented. Patch given by Robert Collins,
tweaked by Barry Warsaw.
- Issue #22226: First letter no longer is stripped from the "status" key in - Issue #22226: First letter no longer is stripped from the "status" key in
the result of Treeview.heading(). the result of Treeview.heading().
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment