22import argparse
33import doctest
44import importlib
5+ import traceback
56from pathlib import Path
67import pkgutil
78import sys
9+ import warnings
810
911
1012def list_modules_recursive (
11- module_importname : str , include_private : bool = True
13+ module_importname : str , include_private : bool = True ,
14+ exclude_matches : list [str ] = []
1215):
1316 module_names = [module_importname ]
1417 # Identify module from its import path (no import -> fail back to caller)
15- module = importlib .import_module (module_importname )
16- # Get the filepath of the module base directory
17- module_filepath = Path (module .__file__ )
18- if module_filepath .name == "__init__.py" :
19- search_filepath = str (module_filepath .parent )
20- for _ , name , ispkg in pkgutil .iter_modules ([search_filepath ]):
21- if not name .startswith ("_" ) or include_private :
18+ try :
19+ error = None
20+ module = importlib .import_module (module_importname )
21+ except Exception as exc :
22+ print (f"\n \n IMPORT FAILED: { module_importname } \n " )
23+ error = exc
24+
25+ if error is None :
26+ # Add sub-modules to the list
27+ # Get the filepath of the module base directory
28+ module_filepath = Path (module .__file__ )
29+ if module_filepath .name == "__init__.py" :
30+ search_filepath = str (module_filepath .parent )
31+ for _ , name , ispkg in pkgutil .iter_modules ([search_filepath ]):
32+ if name .startswith ("_" ) and not include_private :
33+ continue
34+
2235 submodule_name = module_importname + "." + name
36+ if any (match in submodule_name for match in exclude_matches ):
37+ continue
38+
2339 module_names .append (submodule_name )
2440 if ispkg :
2541 module_names .extend (
2642 list_modules_recursive (
2743 submodule_name , include_private = include_private
2844 )
2945 )
46+
3047 # I don't know why there are duplicates, but there can be.
3148 result = []
3249 for name in module_names :
@@ -69,6 +86,7 @@ def run_doctest_paths(
6986 paths_are_modules :bool = False ,
7087 recurse_modules : bool = False ,
7188 include_private_modules : bool = False ,
89+ exclude_matches : list [str ] = [],
7290 option_kwargs : dict = {},
7391 verbose : bool = False ,
7492 dry_run : bool = False ,
@@ -80,23 +98,28 @@ def run_doctest_paths(
8098 "RUNNING run_doctest("
8199 f"paths={ paths !r} "
82100 f", paths_are_modules={ paths_are_modules !r} "
101+ f", recurse_modules={ recurse_modules !r} "
102+ f", include_private_modules={ include_private_modules !r} "
103+ f", exclude_matches={ exclude_matches !r} "
83104 f", option_kwargs={ option_kwargs !r} "
84105 f", verbose={ verbose !r} "
85106 f", dry_run={ dry_run !r} "
86107 f", stop_on_failure={ stop_on_failure !r} "
87- f", include_private={ include_private_modules !r} "
88108 ")"
89109 )
90110 if dry_run :
91111 verbose = True
92112
113+ warnings .simplefilter ("ignore" )
114+
93115 if paths_are_modules :
94116 doctest_function = doctest .testmod
95117 if recurse_modules :
96118 module_paths = []
97119 for path in paths :
98120 module_paths += list_modules_recursive (
99- path , include_private = include_private_modules
121+ path , include_private = include_private_modules ,
122+ exclude_matches = exclude_matches
100123 )
101124 paths = module_paths
102125
@@ -113,14 +136,33 @@ def run_doctest_paths(
113136 if verbose :
114137 print (f"\n -----\n doctest.{ doctest_function .__name__ } : { path !r} " )
115138 if not dry_run :
139+ op_fail = None
116140 if paths_are_modules :
117- arg = importlib .import_module (path )
141+ try :
142+ arg = importlib .import_module (path )
143+ except Exception as exc :
144+ op_fail = exc
118145 else :
119146 arg = path
120- n_fails , n_tests = doctest_function (arg , ** option_kwargs )
121- n_total_fails += n_fails
122- n_total_tests += n_tests
123- n_paths_tested += 1
147+
148+ if op_fail is None :
149+ try :
150+ n_fails , n_tests = doctest_function (arg , ** option_kwargs )
151+ n_total_fails += n_fails
152+ n_total_tests += n_tests
153+ n_paths_tested += 1
154+ except Exception as exc :
155+ op_fail = exc
156+
157+ if op_fail is not None :
158+ n_total_fails += 1
159+ print (f"\n \n ERROR occurred at { path !r} : { op_fail } \n " )
160+ if isinstance (op_fail , doctest .UnexpectedException ):
161+ # This is what happens with "-o raise_on_error=True", which is the
162+ # Python call equivalent of "-o FAIL_FAST" in the doctest CLI.
163+ print (f"Doctest caught exception: { op_fail } " )
164+ traceback .print_exception (* op_fail .exc_info )
165+
124166 if n_total_fails > 0 and stop_on_failure :
125167 break
126168
@@ -168,6 +210,12 @@ def run_doctest_paths(
168210 action = "store_true" ,
169211 help = "If set, exclude private modules (only applies with -m and -r)" ,
170212)
213+ _parser .add_argument (
214+ "-e" ,
215+ "--exclude" ,
216+ action = "append" ,
217+ help = "Match fragments of paths to exclude." ,
218+ )
171219_parser .add_argument (
172220 "-o" ,
173221 "--options" ,
@@ -209,6 +257,7 @@ def parserargs_as_kwargs(args):
209257 paths_are_modules = args .module ,
210258 recurse_modules = args .recursive ,
211259 include_private_modules = not args .publiconly ,
260+ exclude_matches = args .exclude or [],
212261 option_kwargs = process_options (args .options ),
213262 verbose = args .verbose ,
214263 dry_run = args .dryrun ,
0 commit comments