@@ -61,8 +61,9 @@ class TestLoader(object):
6161 def loadTestsFromTestCase (self , testCaseClass ):
6262 """Return a suite of all tests cases contained in testCaseClass"""
6363 if issubclass (testCaseClass , suite .TestSuite ):
64- raise TypeError ("Test cases should not be derived from TestSuite." \
65- " Maybe you meant to derive from TestCase?" )
64+ raise TypeError ("Test cases should not be derived from "
65+ "TestSuite. Maybe you meant to derive from "
66+ "TestCase?" )
6667 testCaseNames = self .getTestCaseNames (testCaseClass )
6768 if not testCaseNames and hasattr (testCaseClass , 'runTest' ):
6869 testCaseNames = ['runTest' ]
@@ -200,6 +201,8 @@ def discover(self, start_dir, pattern='test*.py', top_level_dir=None):
200201 self ._top_level_dir = top_level_dir
201202
202203 is_not_importable = False
204+ is_namespace = False
205+ tests = []
203206 if os .path .isdir (os .path .abspath (start_dir )):
204207 start_dir = os .path .abspath (start_dir )
205208 if start_dir != top_level_dir :
@@ -213,15 +216,52 @@ def discover(self, start_dir, pattern='test*.py', top_level_dir=None):
213216 else :
214217 the_module = sys .modules [start_dir ]
215218 top_part = start_dir .split ('.' )[0 ]
216- start_dir = os .path .abspath (os .path .dirname ((the_module .__file__ )))
219+ try :
220+ start_dir = os .path .abspath (
221+ os .path .dirname ((the_module .__file__ )))
222+ except AttributeError :
223+ # look for namespace packages
224+ try :
225+ spec = the_module .__spec__
226+ except AttributeError :
227+ spec = None
228+
229+ if spec and spec .loader is None :
230+ if spec .submodule_search_locations is not None :
231+ is_namespace = True
232+
233+ for path in the_module .__path__ :
234+ if (not set_implicit_top and
235+ not path .startswith (top_level_dir )):
236+ continue
237+ self ._top_level_dir = \
238+ (path .split (the_module .__name__
239+ .replace ("." , os .path .sep ))[0 ])
240+ tests .extend (self ._find_tests (path ,
241+ pattern ,
242+ namespace = True ))
243+ elif the_module .__name__ in sys .builtin_module_names :
244+ # builtin module
245+ raise TypeError ('Can not use builtin modules '
246+ 'as dotted module names' ) from None
247+ else :
248+ raise TypeError (
249+ 'don\' t know how to discover from {!r}'
250+ .format (the_module )) from None
251+
217252 if set_implicit_top :
218- self ._top_level_dir = self ._get_directory_containing_module (top_part )
219- sys .path .remove (top_level_dir )
253+ if not is_namespace :
254+ self ._top_level_dir = \
255+ self ._get_directory_containing_module (top_part )
256+ sys .path .remove (top_level_dir )
257+ else :
258+ sys .path .remove (top_level_dir )
220259
221260 if is_not_importable :
222261 raise ImportError ('Start directory is not importable: %r' % start_dir )
223262
224- tests = list (self ._find_tests (start_dir , pattern ))
263+ if not is_namespace :
264+ tests = list (self ._find_tests (start_dir , pattern ))
225265 return self .suiteClass (tests )
226266
227267 def _get_directory_containing_module (self , module_name ):
@@ -254,7 +294,7 @@ def _match_path(self, path, full_path, pattern):
254294 # override this method to use alternative matching strategy
255295 return fnmatch (path , pattern )
256296
257- def _find_tests (self , start_dir , pattern ):
297+ def _find_tests (self , start_dir , pattern , namespace = False ):
258298 """Used by discovery. Yields test suites it loads."""
259299 paths = sorted (os .listdir (start_dir ))
260300
@@ -287,7 +327,8 @@ def _find_tests(self, start_dir, pattern):
287327 raise ImportError (msg % (mod_name , module_dir , expected_dir ))
288328 yield self .loadTestsFromModule (module )
289329 elif os .path .isdir (full_path ):
290- if not os .path .isfile (os .path .join (full_path , '__init__.py' )):
330+ if (not namespace and
331+ not os .path .isfile (os .path .join (full_path , '__init__.py' ))):
291332 continue
292333
293334 load_tests = None
@@ -304,7 +345,8 @@ def _find_tests(self, start_dir, pattern):
304345 # tests loaded from package file
305346 yield tests
306347 # recurse into the package
307- yield from self ._find_tests (full_path , pattern )
348+ yield from self ._find_tests (full_path , pattern ,
349+ namespace = namespace )
308350 else :
309351 try :
310352 yield load_tests (self , tests , pattern )
0 commit comments