classification
Title: pattern=None when following documentation for load_tests and unittest.main()
Type: behavior Stage: test needed
Components: Library (Lib) Versions: Python 3.2, Python 3.3, Python 2.7
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: michael.foord Nosy List: Stefan Sullivan, Zbynek.Winkler, eric.araujo, ezio.melotti, gagern, mark.dickinson, michael.foord, rik.poggi, vila
Priority: normal Keywords: patch

Created on 2011-02-15 08:33 by gagern, last changed 2020-03-24 09:43 by Zbynek.Winkler.

Files
File name Uploaded Description Edit
tester.py gagern, 2011-02-15 08:33 demonstrating issue
issue11218_patternNone.patch gagern, 2012-04-13 20:56 Treat pattern=None like ommitted pattern review
Messages (10)
msg128579 - (view) Author: Martin von Gagern (gagern) Date: 2011-02-15 08:33
If I follow the documentation at http://docs.python.org/library/unittest.html#unittest.main by putting the following two snippets of code in my module file:

def load_tests(loader, standard_tests, pattern='test*.py'):
    # top level directory cached on loader instance
    this_dir = os.path.dirname(__file__)
    package_tests = loader.discover(start_dir=this_dir, pattern=pattern)
    standard_tests.addTests(package_tests)
    return standard_tests

if __name__ == "__main__":
    unittest.main()

then the application will fail with an obscure error message:

======================================================================
ERROR: __main__ (unittest.loader.LoadTestsFailure)
----------------------------------------------------------------------
TypeError: object of type 'NoneType' has no len()

----------------------------------------------------------------------
Ran 1 test in 0.000s

Monkeypatching unittest.loader._make_failed_load_tests to display a stack trace, I got this:

Traceback (most recent call last):
  File "/usr/lib64/python2.7/unittest/loader.py", line 71, in loadTestsFromModule
    return load_tests(self, tests, None)
  File "tester.py", line 15, in load_tests
    package_tests = loader.discover(start_dir=this_dir, pattern=pattern)
  File "/usr/lib64/python2.7/unittest/loader.py", line 204, in discover
    tests = list(self._find_tests(start_dir, pattern))
  File "/usr/lib64/python2.7/unittest/loader.py", line 247, in _find_tests
    if not self._match_path(path, full_path, pattern):
  File "/usr/lib64/python2.7/unittest/loader.py", line 235, in _match_path
    return fnmatch(path, pattern)
  File "/usr/lib64/python2.7/fnmatch.py", line 43, in fnmatch
    return fnmatchcase(name, pat)
  File "/usr/lib64/python2.7/fnmatch.py", line 75, in fnmatchcase
    res = translate(pat)
  File "/usr/lib64/python2.7/fnmatch.py", line 87, in translate
    i, n = 0, len(pat)
TypeError: object of type 'NoneType' has no len()

The error is due to the fact that pattern is passed as None to load_tests, but apparently loader.discover doesn't loke a None pattern.

I would suggest that
a) discover internally translates None to the default of 'test*.py' or
b) the documentation is changed to cater for this common use case, i.e. by including a "pattern is None" case distinction in its load_tests snippet.

In case b) is implemented but not a), it would be nice to have a more expressive error message by catching the error somewhat sooner.
msg157891 - (view) Author: Rik Poggi (rik.poggi) Date: 2012-04-09 20:17
I think the doc should be improved (http://docs.python.org/library/unittest.html#load-tests-protocol), it's not clear how pattern in the example (last one) could not be None.

Changing the discover signature doesn't seem to be an option since the TestLoader.discover doc http://docs.python.org/library/unittest.html#unittest.TestLoader.discover says:

  The pattern is deliberately not stored as a loader attribute so that
  packages can continue discovery themselves.
msg157935 - (view) Author: Martin von Gagern (gagern) Date: 2012-04-10 05:42
Rik, I don't follow your argument on not changing discover. Currently, if code calls discover with pattern=None, there will be an exception. So there cannot be any working code out there which passes pattern=None. Therefore, it should be all right for the implementation to detect the None and replace it by a default. No currently working code should be affected, and new code could be shorter that way. The pattern still wouldn't be stored inside the loader, so the doc there still holds. Only the fallback for None would be stored.

By the way, what's the rationale behind passing the pattern to the load_tests function? Shouldn't each package decide which of its files constitute the test suite, independent of what the parent module used as a filter?
msg158219 - (view) Author: Rik Poggi (rik.poggi) Date: 2012-04-13 17:48
I wasn't trying to make any argument, just thinking that such particular signature was intentional.

Also notice that there might be code that doesn't pass the pattern argument, and fall back on the default value. So a signature change will break their code. I think that the best solution would be to provide a better documentation. 

I don't know what's the rational behind all that.
msg158232 - (view) Author: Martin von Gagern (gagern) Date: 2012-04-13 20:56
I'm attaching a patch to better explain what I'm suggesting. As you can see, this patch doesn't change the signature of discover, nor does it change the semantics for any code that doesn't pass pattern, or that passes some pattern other than None.

The only change is that now, passing pattern=None is the same as not passing pattern at all. As a result, load_tests might now pass pattern=pattern as the documentation suggests, and still be called with pattern=None without raising an exception.
msg158234 - (view) Author: Michael Foord (michael.foord) * (Python committer) Date: 2012-04-13 21:05
So the logic of the "pattern" argument to "load_tests" is that it should not be None when test discovery is loading the __init__.py module of a test package. However, because most patterns will actually *prevent* __init__.py from being loaded by test discovery - this turns out to not be very useful and in all practical cases this argument will be None.

I don't think there are any backward compatibility issues with the real pattern being passed in.
msg158235 - (view) Author: Michael Foord (michael.foord) * (Python committer) Date: 2012-04-13 21:06
Also the patch to allow the pattern to be None (and revert to the default pattern in this case) looks good.
msg158236 - (view) Author: Martin von Gagern (gagern) Date: 2012-04-13 21:23
Michael wrote: "[…] the real pattern being passed in".
I wonder, what would be "the real pattern"? In the code I originally pasted, the load_tests function would be invoked by loadTestsFromModule (for module __main__). There is nothing file-based about this, so although you could pass a default pattern, it wouldn't be any more or less "real" than passing None. It might be more useful, though.

"most patterns will actually *prevent* __init__.py from being loaded by test discovery - this turns out to not be very useful and in all practical cases this argument will be None."

Not sure I follow there. For the root of the test suite, yes, it will always be None. But for child packages it will be the pattern that led to the discovery of the __init__.py of that package. In all practical cases this will be a string different from both None and the default of 'test*.py', as it has to match the directory name. Most likely it will be what the load_tests function of the parent package passed to its invocation of discover.
msg172463 - (view) Author: Michael Foord (michael.foord) * (Python committer) Date: 2012-10-09 09:20
Changing the docs to the following fixes the original reported issue:

def load_tests(loader, standard_tests, pattern):
    # top level directory cached on loader instance
    this_dir = os.path.dirname(__file__)
    pattern = pattern or "test_*.py"
    package_tests = loader.discover(start_dir=this_dir, pattern=pattern)
    standard_tests.addTests(package_tests)
    return standard_tests

(Suggested by Antoine in issue 16171.)

I think that load_tests not working as documented is a bug and calling discover with pattern=None should work.
msg348881 - (view) Author: Stefan Sullivan (Stefan Sullivan) Date: 2019-08-02 01:25
This seems like it directly contradicts the documentation for the discover method:

"If a package (a directory containing a file named __init__.py) is found, the package will be checked for a load_tests function. If this exists then it will be called package.load_tests(loader, tests, pattern)."

I believe this _is_ a functional bug. I have a use case where I have provided a root script with contents along the lines of 

    for d in includes:
        test_dir = os.path.join(search_dir, d)
        if test_dir not in excludes:
            test_suite.addTests(test_loader.discover(test_dir, '*.py', search_dir))

and a load_tests function in one of those directory's __init__.py. However, since test discovery does not pass on the pattern, I cannot possibly discover what the pattern should have been.

Furthermore, if I instead invoke test discovery on the command line using:

    python -m unittest discover -s /path/to/code -t /path/to/code -p *.py

I again will see a load_tests invoked with pattern=None.

What is the situation where load_tests will be invoked with a non-None pattern?

Is there a workaround for how I'm supposed to get this pattern? Is there a convention or design pattern where I'm supposed to return everything and let the caller filter out the extra tests?

As far as I can tell, the load_tests protocol is only relevant to packages (not modules) by virtue of its placement in the __init__.py file. However, the only way to load tests from a package is via discover, because there is no loadTestsFromPackage method (like there is for loadTestsFromModule). Have I misunderstood the load_tests protocol? Is there some special way to get a pattern?

I feel that if the user really wanted to use their own pattern, being passed a string won't hurt anything, but converse is not true (at least for my use case)
History
Date User Action Args
2020-03-24 09:43:01Zbynek.Winklersetnosy: + Zbynek.Winkler
2019-08-02 01:25:31Stefan Sullivansetnosy: + Stefan Sullivan
messages: + msg348881
2013-04-26 14:46:58vilasetnosy: + vila
2012-11-26 10:17:26mark.dickinsonsetnosy: + mark.dickinson
2012-10-09 09:20:54michael.foordsetmessages: + msg172463
2012-10-09 09:18:46michael.foordlinkissue16171 superseder
2012-04-13 21:23:39gagernsetmessages: + msg158236
2012-04-13 21:06:12michael.foordsetmessages: + msg158235
2012-04-13 21:05:13michael.foordsetmessages: + msg158234
2012-04-13 20:56:27gagernsetfiles: + issue11218_patternNone.patch
keywords: + patch
messages: + msg158232
2012-04-13 17:48:32rik.poggisetmessages: + msg158219
2012-04-10 05:42:25gagernsetmessages: + msg157935
2012-04-09 20:17:02rik.poggisetnosy: + rik.poggi
messages: + msg157891
2011-06-14 16:03:41michael.foordsetassignee: michael.foord
2011-06-14 15:41:18eric.araujosetnosy: + ezio.melotti, eric.araujo, michael.foord, - docs@python
versions: + Python 3.2, Python 3.3
assignee: docs@python -> (no value)
components: - Documentation
type: enhancement -> behavior
stage: test needed
2011-02-15 08:33:26gagerncreate