1#!/usr/bin/env python 2""" 3refguide_check.py [OPTIONS] [-- ARGS] 4 5Check for a PyWavelets submodule whether the objects in its __all__ dict 6correspond to the objects included in the reference guide. 7 8Example of usage:: 9 10 $ python refguide_check.py optimize 11 12Note that this is a helper script to be able to check if things are missing; 13the output of this script does need to be checked manually. In some cases 14objects are left out of the refguide for a good reason (it's an alias of 15another function, or deprecated, or ...) 16 17Another use of this helper script is to check validity of code samples 18in docstrings. This is different from doctesting [we do not aim to have 19scipy docstrings doctestable!], this is just to make sure that code in 20docstrings is valid python:: 21 22 $ python refguide_check.py --check_docs optimize 23 24""" 25from __future__ import print_function 26 27import sys 28import os 29import re 30import copy 31import inspect 32import warnings 33import doctest 34import tempfile 35import io 36import docutils.core 37from docutils.parsers.rst import directives 38import shutil 39import glob 40from doctest import NORMALIZE_WHITESPACE, ELLIPSIS, IGNORE_EXCEPTION_DETAIL 41from argparse import ArgumentParser 42import numpy as np 43 44# FIXME: doctests need the str/repr formatting used in Numpy < 1.14. 45try: 46 np.set_printoptions(legacy='1.13') 47except TypeError: 48 pass 49 50# sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'doc', 51# 'sphinxext')) 52from numpydoc.docscrape_sphinx import get_doc_object 53# Remove sphinx directives that don't run without Sphinx environment 54directives._directives.pop('versionadded', None) 55directives._directives.pop('versionchanged', None) 56directives._directives.pop('moduleauthor', None) 57directives._directives.pop('sectionauthor', None) 58directives._directives.pop('codeauthor', None) 59directives._directives.pop('toctree', None) 60 61 62BASE_MODULE = "pywt" 63 64PUBLIC_SUBMODULES = [] 65 66# Docs for these modules are included in the parent module 67OTHER_MODULE_DOCS = {} 68 69# these names are known to fail doctesting and we like to keep it that way 70# e.g. sometimes pseudocode is acceptable etc 71DOCTEST_SKIPLIST = set([]) 72 73# these names are not required to be present in ALL despite being in 74# autosummary:: listing 75REFGUIDE_ALL_SKIPLIST = [] 76 77HAVE_MATPLOTLIB = False 78 79 80def short_path(path, cwd=None): 81 """ 82 Return relative or absolute path name, whichever is shortest. 83 """ 84 if not isinstance(path, str): 85 return path 86 if cwd is None: 87 cwd = os.getcwd() 88 abspath = os.path.abspath(path) 89 relpath = os.path.relpath(path, cwd) 90 if len(abspath) <= len(relpath): 91 return abspath 92 return relpath 93 94 95def find_names(module, names_dict): 96 # Refguide entries: 97 # 98 # - 3 spaces followed by function name, and maybe some spaces, some 99 # dashes, and an explanation; only function names listed in 100 # refguide are formatted like this (mostly, there may be some false 101 # positives) 102 # 103 # - special directives, such as data and function 104 # 105 # - (scipy.constants only): quoted list 106 # 107 patterns = [ 108 r"^\s\s\s([a-z_0-9A-Z]+)(\s+-+.*)?$", 109 r"^\.\. (?:data|function)::\s*([a-z_0-9A-Z]+)\s*$" 110 ] 111 112 if module.__name__ == 'scipy.constants': 113 patterns += ["^``([a-z_0-9A-Z]+)``"] 114 115 patterns = [re.compile(pattern) for pattern in patterns] 116 module_name = module.__name__ 117 118 for line in module.__doc__.splitlines(): 119 res = re.search(r"^\s*\.\. (?:currentmodule|module):: ([a-z0-9A-Z_.]+)\s*$", line) 120 if res: 121 module_name = res.group(1) 122 continue 123 124 for pattern in patterns: 125 res = re.match(pattern, line) 126 if res is not None: 127 name = res.group(1) 128 entry = '.'.join([module_name, name]) 129 names_dict.setdefault(module_name, set()).add(name) 130 break 131 132 133def get_all_dict(module): 134 """Return a copy of the __all__ dict with irrelevant items removed.""" 135 if hasattr(module, "__all__"): 136 all_dict = copy.deepcopy(module.__all__) 137 else: 138 all_dict = copy.deepcopy(dir(module)) 139 all_dict = [name for name in all_dict 140 if not name.startswith("_")] 141 for name in ['absolute_import', 'division', 'print_function']: 142 try: 143 all_dict.remove(name) 144 except ValueError: 145 pass 146 147 # Modules are almost always private; real submodules need a separate 148 # run of refguide_check. 149 all_dict = [name for name in all_dict 150 if not inspect.ismodule(getattr(module, name, None))] 151 152 deprecated = [] 153 not_deprecated = [] 154 for name in all_dict: 155 f = getattr(module, name, None) 156 if callable(f) and is_deprecated(f): 157 deprecated.append(name) 158 else: 159 not_deprecated.append(name) 160 161 others = set(dir(module)).difference(set(deprecated)).difference(set(not_deprecated)) 162 163 return not_deprecated, deprecated, others 164 165 166def compare(all_dict, others, names, module_name): 167 """Return sets of objects only in __all__, refguide, or completely missing.""" 168 only_all = set() 169 for name in all_dict: 170 if name not in names: 171 only_all.add(name) 172 173 only_ref = set() 174 missing = set() 175 for name in names: 176 if name not in all_dict: 177 for pat in REFGUIDE_ALL_SKIPLIST: 178 if re.match(pat, module_name + '.' + name): 179 if name not in others: 180 missing.add(name) 181 break 182 else: 183 only_ref.add(name) 184 185 return only_all, only_ref, missing 186 187 188def is_deprecated(f): 189 with warnings.catch_warnings(record=True) as w: 190 warnings.simplefilter("error") 191 try: 192 f(**{"not a kwarg": None}) 193 except DeprecationWarning: 194 return True 195 except: 196 pass 197 return False 198 199 200def check_items(all_dict, names, deprecated, others, module_name, dots=True): 201 num_all = len(all_dict) 202 num_ref = len(names) 203 204 output = "" 205 206 output += "Non-deprecated objects in __all__: %i\n" % num_all 207 output += "Objects in refguide: %i\n\n" % num_ref 208 209 only_all, only_ref, missing = compare(all_dict, others, names, module_name) 210 dep_in_ref = set(only_ref).intersection(deprecated) 211 only_ref = set(only_ref).difference(deprecated) 212 213 if len(dep_in_ref) > 0: 214 output += "Deprecated objects in refguide::\n\n" 215 for name in sorted(deprecated): 216 output += " " + name + "\n" 217 218 if len(only_all) == len(only_ref) == len(missing) == 0: 219 if dots: 220 output_dot('.') 221 return [(None, True, output)] 222 else: 223 if len(only_all) > 0: 224 output += "ERROR: objects in %s.__all__ but not in refguide::\n\n" % module_name 225 for name in sorted(only_all): 226 output += " " + name + "\n" 227 228 if len(only_ref) > 0: 229 output += "ERROR: objects in refguide but not in %s.__all__::\n\n" % module_name 230 for name in sorted(only_ref): 231 output += " " + name + "\n" 232 233 if len(missing) > 0: 234 output += "ERROR: missing objects::\n\n" 235 for name in sorted(missing): 236 output += " " + name + "\n" 237 238 if dots: 239 output_dot('F') 240 return [(None, False, output)] 241 242 243def validate_rst_syntax(text, name, dots=True): 244 if text is None: 245 if dots: 246 output_dot('E') 247 return False, "ERROR: %s: no documentation" % (name,) 248 249 ok_unknown_items = set([ 250 'mod', 'currentmodule', 'autosummary', 'data', 251 'obj', 'versionadded', 'versionchanged', 'module', 'class', 252 'ref', 'func', 'toctree', 'moduleauthor', 253 'sectionauthor', 'codeauthor', 'eq', 254 ]) 255 256 # Run through docutils 257 error_stream = io.StringIO() 258 259 def resolve(name, is_label=False): 260 return ("http://foo", name) 261 262 token = '<RST-VALIDATE-SYNTAX-CHECK>' 263 264 docutils.core.publish_doctree( 265 text, token, 266 settings_overrides = dict(halt_level=5, 267 traceback=True, 268 default_reference_context='title-reference', 269 default_role='emphasis', 270 link_base='', 271 resolve_name=resolve, 272 stylesheet_path='', 273 raw_enabled=0, 274 file_insertion_enabled=0, 275 warning_stream=error_stream)) 276 277 # Print errors, disregarding unimportant ones 278 error_msg = error_stream.getvalue() 279 errors = error_msg.split(token) 280 success = True 281 output = "" 282 283 for error in errors: 284 lines = error.splitlines() 285 if not lines: 286 continue 287 288 m = re.match(r'.*Unknown (?:interpreted text role|directive type) "(.*)".*$', lines[0]) 289 if m: 290 if m.group(1) in ok_unknown_items: 291 continue 292 293 m = re.match(r'.*Error in "math" directive:.*unknown option: "label"', " ".join(lines), re.S) 294 if m: 295 continue 296 297 output += name + lines[0] + "::\n " + "\n ".join(lines[1:]).rstrip() + "\n" 298 success = False 299 300 if not success: 301 output += " " + "-"*72 + "\n" 302 for lineno, line in enumerate(text.splitlines()): 303 output += " %-4d %s\n" % (lineno+1, line) 304 output += " " + "-"*72 + "\n\n" 305 306 if dots: 307 output_dot('.' if success else 'F') 308 return success, output 309 310 311def output_dot(msg='.', stream=sys.stderr): 312 stream.write(msg) 313 stream.flush() 314 315 316def check_rest(module, names, dots=True): 317 """ 318 Check reStructuredText formatting of docstrings 319 320 Returns: [(name, success_flag, output), ...] 321 """ 322 323 try: 324 skip_types = (dict, str, unicode, float, int) 325 except NameError: 326 # python 3 327 skip_types = (dict, str, float, int) 328 329 results = [] 330 331 if module.__name__[6:] not in OTHER_MODULE_DOCS: 332 results += [(module.__name__,) + 333 validate_rst_syntax(inspect.getdoc(module), 334 module.__name__, dots=dots)] 335 336 for name in names: 337 full_name = module.__name__ + '.' + name 338 obj = getattr(module, name, None) 339 340 if obj is None: 341 results.append((full_name, False, "%s has no docstring" % (full_name,))) 342 continue 343 elif isinstance(obj, skip_types): 344 continue 345 346 if inspect.ismodule(obj): 347 text = inspect.getdoc(obj) 348 else: 349 try: 350 text = str(get_doc_object(obj)) 351 except: 352 import traceback 353 results.append((full_name, False, 354 "Error in docstring format!\n" + 355 traceback.format_exc())) 356 continue 357 358 m = re.search("([\x00-\x09\x0b-\x1f])", text) 359 if m: 360 msg = ("Docstring contains a non-printable character %r! " 361 "Maybe forgot r\"\"\"?" % (m.group(1),)) 362 results.append((full_name, False, msg)) 363 continue 364 365 try: 366 src_file = short_path(inspect.getsourcefile(obj)) 367 except TypeError: 368 src_file = None 369 370 if src_file: 371 file_full_name = src_file + ':' + full_name 372 else: 373 file_full_name = full_name 374 375 results.append((full_name,) + 376 validate_rst_syntax(text, file_full_name, dots=dots)) 377 378 return results 379 380 381### Doctest helpers #### 382 383# the namespace to run examples in 384DEFAULT_NAMESPACE = {'np': np} 385 386# the namespace to do checks in 387CHECK_NAMESPACE = { 388 'np': np, 389 'assert_allclose': np.testing.assert_allclose, 390 'assert_equal': np.testing.assert_equal, 391 # recognize numpy repr's 392 'array': np.array, 393 'matrix': np.matrix, 394 'int64': np.int64, 395 'uint64': np.uint64, 396 'int8': np.int8, 397 'int32': np.int32, 398 'float64': np.float64, 399 'dtype': np.dtype, 400 'nan': np.nan, 401 'NaN': np.nan, 402 'inf': np.inf, 403 'Inf': np.inf, } 404 405 406class DTRunner(doctest.DocTestRunner): 407 DIVIDER = "\n" 408 409 def __init__(self, item_name, checker=None, verbose=None, optionflags=0): 410 self._item_name = item_name 411 doctest.DocTestRunner.__init__(self, checker=checker, verbose=verbose, 412 optionflags=optionflags) 413 414 def _report_item_name(self, out, new_line=False): 415 if self._item_name is not None: 416 if new_line: 417 out("\n") 418 self._item_name = None 419 420 def report_start(self, out, test, example): 421 self._checker._source = example.source 422 return doctest.DocTestRunner.report_start(self, out, test, example) 423 424 def report_success(self, out, test, example, got): 425 if self._verbose: 426 self._report_item_name(out, new_line=True) 427 return doctest.DocTestRunner.report_success( 428 self, out, test, example, got) 429 430 def report_unexpected_exception(self, out, test, example, exc_info): 431 self._report_item_name(out) 432 return doctest.DocTestRunner.report_unexpected_exception( 433 self, out, test, example, exc_info) 434 435 def report_failure(self, out, test, example, got): 436 self._report_item_name(out) 437 return doctest.DocTestRunner.report_failure(self, out, test, 438 example, got) 439 440class Checker(doctest.OutputChecker): 441 obj_pattern = re.compile('at 0x[0-9a-fA-F]+>') 442 vanilla = doctest.OutputChecker() 443 rndm_markers = {'# random', '# Random', '#random', '#Random', "# may vary"} 444 stopwords = {'plt.', '.hist', '.show', '.ylim', '.subplot(', 445 'set_title', 'imshow', 'plt.show', 'ax.axis', 'plt.plot(', 446 '.bar(', '.title', '.ylabel', '.xlabel', 'set_ylim', 447 'set_xlim', '# reformatted'} 448 449 def __init__(self, parse_namedtuples=True, ns=None, atol=1e-8, rtol=1e-2): 450 self.parse_namedtuples = parse_namedtuples 451 self.atol, self.rtol = atol, rtol 452 if ns is None: 453 self.ns = dict(CHECK_NAMESPACE) 454 else: 455 self.ns = ns 456 457 def check_output(self, want, got, optionflags): 458 # cut it short if they are equal 459 if want == got: 460 return True 461 462 # skip stopwords in source 463 if any(word in self._source for word in self.stopwords): 464 return True 465 466 # skip random stuff 467 if any(word in want for word in self.rndm_markers): 468 return True 469 470 # skip function/object addresses 471 if self.obj_pattern.search(got): 472 return True 473 474 # ignore comments (e.g. signal.freqresp) 475 if want.lstrip().startswith("#"): 476 return True 477 478 # try the standard doctest 479 try: 480 if self.vanilla.check_output(want, got, optionflags): 481 return True 482 except Exception: 483 pass 484 485 # OK then, convert strings to objects 486 try: 487 a_want = eval(want, dict(self.ns)) 488 a_got = eval(got, dict(self.ns)) 489 except: 490 if not self.parse_namedtuples: 491 return False 492 # suppose that "want" is a tuple, and "got" is smth like 493 # MoodResult(statistic=10, pvalue=0.1). 494 # Then convert the latter to the tuple (10, 0.1), 495 # and then compare the tuples. 496 try: 497 num = len(a_want) 498 regex = ('[\w\d_]+\(' + 499 ', '.join(['[\w\d_]+=(.+)']*num) + 500 '\)') 501 grp = re.findall(regex, got.replace('\n', ' ')) 502 if len(grp) > 1: # no more than one for now 503 return False 504 # fold it back to a tuple 505 got_again = '(' + ', '.join(grp[0]) + ')' 506 return self.check_output(want, got_again, optionflags) 507 except Exception: 508 return False 509 510 # ... and defer to numpy 511 try: 512 return self._do_check(a_want, a_got) 513 except Exception: 514 # heterog tuple, eg (1, np.array([1., 2.])) 515 try: 516 return all(self._do_check(w, g) for w, g in zip(a_want, a_got)) 517 except (TypeError, ValueError): 518 return False 519 520 def _do_check(self, want, got): 521 # This should be done exactly as written to correctly handle all of 522 # numpy-comparable objects, strings, and heterogenous tuples 523 try: 524 if want == got: 525 return True 526 except Exception: 527 pass 528 return np.allclose(want, got, atol=self.atol, rtol=self.rtol) 529 530 531def _run_doctests(tests, full_name, verbose, doctest_warnings): 532 """Run modified doctests for the set of `tests`. 533 534 Returns: list of [(success_flag, output), ...] 535 """ 536 flags = NORMALIZE_WHITESPACE | ELLIPSIS | IGNORE_EXCEPTION_DETAIL 537 runner = DTRunner(full_name, checker=Checker(), optionflags=flags, 538 verbose=verbose) 539 540 output = [] 541 success = True 542 def out(msg): 543 output.append(msg) 544 545 class MyStderr(object): 546 """Redirect stderr to the current stdout""" 547 def write(self, msg): 548 if doctest_warnings: 549 sys.stdout.write(msg) 550 else: 551 out(msg) 552 553 # Run tests, trying to restore global state afterward 554 old_printoptions = np.get_printoptions() 555 old_errstate = np.seterr() 556 old_stderr = sys.stderr 557 cwd = os.getcwd() 558 tmpdir = tempfile.mkdtemp() 559 sys.stderr = MyStderr() 560 try: 561 os.chdir(tmpdir) 562 563 # try to ensure random seed is NOT reproducible 564 np.random.seed(None) 565 566 for t in tests: 567 t.filename = short_path(t.filename, cwd) 568 fails, successes = runner.run(t, out=out) 569 if fails > 0: 570 success = False 571 finally: 572 sys.stderr = old_stderr 573 os.chdir(cwd) 574 shutil.rmtree(tmpdir) 575 np.set_printoptions(**old_printoptions) 576 np.seterr(**old_errstate) 577 578 return success, output 579 580 581def check_doctests(module, verbose, ns=None, 582 dots=True, doctest_warnings=False): 583 """Check code in docstrings of the module's public symbols. 584 585 Returns: list of [(item_name, success_flag, output), ...] 586 """ 587 if ns is None: 588 ns = dict(DEFAULT_NAMESPACE) 589 590 # Loop over non-deprecated items 591 results = [] 592 593 for name in get_all_dict(module)[0]: 594 full_name = module.__name__ + '.' + name 595 596 if full_name in DOCTEST_SKIPLIST: 597 continue 598 599 try: 600 obj = getattr(module, name) 601 except AttributeError: 602 import traceback 603 results.append((full_name, False, 604 "Missing item!\n" + 605 traceback.format_exc())) 606 continue 607 608 finder = doctest.DocTestFinder() 609 try: 610 tests = finder.find(obj, name, globs=dict(ns)) 611 except: 612 import traceback 613 results.append((full_name, False, 614 "Failed to get doctests!\n" + 615 traceback.format_exc())) 616 continue 617 618 success, output = _run_doctests(tests, full_name, verbose, 619 doctest_warnings) 620 621 if dots: 622 output_dot('.' if success else 'F') 623 624 results.append((full_name, success, "".join(output))) 625 626 if HAVE_MATPLOTLIB: 627 import matplotlib.pyplot as plt 628 plt.close('all') 629 630 return results 631 632 633def check_doctests_testfile(fname, verbose, ns=None, 634 dots=True, doctest_warnings=False): 635 """Check code in a text file. 636 637 Mimic `check_doctests` above, differing mostly in test discovery. 638 (which is borrowed from stdlib's doctest.testfile here, 639 https://github.com/python-git/python/blob/master/Lib/doctest.py) 640 641 Returns: list of [(item_name, success_flag, output), ...] 642 643 Notes 644 ----- 645 646 We also try to weed out pseudocode: 647 * We maintain a list of exceptions which signal pseudocode, 648 * We split the text file into "blocks" of code separated by empty lines 649 and/or intervening text. 650 * If a block contains a marker, the whole block is then assumed to be 651 pseudocode. It is then not being doctested. 652 653 The rationale is that typically, the text looks like this: 654 655 blah 656 <BLANKLINE> 657 >>> from numpy import some_module # pseudocode! 658 >>> func = some_module.some_function 659 >>> func(42) # still pseudocode 660 146 661 <BLANKLINE> 662 blah 663 <BLANKLINE> 664 >>> 2 + 3 # real code, doctest it 665 5 666 667 """ 668 results = [] 669 670 if ns is None: 671 ns = dict(DEFAULT_NAMESPACE) 672 673 _, short_name = os.path.split(fname) 674 if short_name in DOCTEST_SKIPLIST: 675 return results 676 677 full_name = fname 678 text = open(fname).read() 679 680 PSEUDOCODE = set(['some_function', 'some_module', 'import example', 681 'ctypes.CDLL', # likely need compiling, skip it 682 'integrate.nquad(func,' # ctypes integrate tutotial 683 ]) 684 685 # split the text into "blocks" and try to detect and omit pseudocode blocks. 686 parser = doctest.DocTestParser() 687 good_parts = [] 688 for part in text.split('\n\n'): 689 tests = parser.get_doctest(part, ns, fname, fname, 0) 690 if any(word in ex.source for word in PSEUDOCODE 691 for ex in tests.examples): 692 # omit it 693 pass 694 else: 695 # `part` looks like a good code, let's doctest it 696 good_parts += [part] 697 698 # Reassemble the good bits and doctest them: 699 good_text = '\n\n'.join(good_parts) 700 tests = parser.get_doctest(good_text, ns, fname, fname, 0) 701 success, output = _run_doctests([tests], full_name, verbose, 702 doctest_warnings) 703 704 if dots: 705 output_dot('.' if success else 'F') 706 707 results.append((full_name, success, "".join(output))) 708 709 if HAVE_MATPLOTLIB: 710 import matplotlib.pyplot as plt 711 plt.close('all') 712 713 return results 714 715 716def init_matplotlib(): 717 global HAVE_MATPLOTLIB 718 719 try: 720 import matplotlib 721 matplotlib.use('Agg') 722 HAVE_MATPLOTLIB = True 723 except ImportError: 724 HAVE_MATPLOTLIB = False 725 726 727def main(argv): 728 parser = ArgumentParser(usage=__doc__.lstrip()) 729 parser.add_argument("module_names", metavar="SUBMODULES", default=[], 730 nargs='*', 731 help="Submodules to check (default: all public)") 732 parser.add_argument("--doctests", action="store_true", 733 help="Run also doctests") 734 parser.add_argument("-v", "--verbose", action="count", default=0) 735 parser.add_argument("--doctest-warnings", action="store_true", 736 help="Enforce warning checking for doctests") 737 parser.add_argument("--skip-examples", action="store_true", 738 help="Skip running doctests in the examples.") 739 args = parser.parse_args(argv) 740 741 modules = [] 742 names_dict = {} 743 744 if args.module_names: 745 args.skip_examples = True 746 else: 747 args.module_names = list(PUBLIC_SUBMODULES) 748 749 os.environ['SCIPY_PIL_IMAGE_VIEWER'] = 'true' 750 751 module_names = list(args.module_names) 752 for name in list(module_names): 753 if name in OTHER_MODULE_DOCS: 754 name = OTHER_MODULE_DOCS[name] 755 if name not in module_names: 756 module_names.append(name) 757 758 for submodule_name in module_names: 759 module_name = BASE_MODULE + '.' + submodule_name 760 __import__(module_name) 761 module = sys.modules[module_name] 762 763 if submodule_name not in OTHER_MODULE_DOCS: 764 find_names(module, names_dict) 765 766 if submodule_name in args.module_names: 767 modules.append(module) 768 769 dots = True 770 success = True 771 results = [] 772 773 print("Running checks for %d modules:" % (len(modules),)) 774 775 if args.doctests or not args.skip_examples: 776 init_matplotlib() 777 778 for module in modules: 779 if dots: 780 if module is not modules[0]: 781 sys.stderr.write(' ') 782 sys.stderr.write(module.__name__ + ' ') 783 sys.stderr.flush() 784 785 all_dict, deprecated, others = get_all_dict(module) 786 names = names_dict.get(module.__name__, set()) 787 788 mod_results = [] 789 mod_results += check_items(all_dict, names, deprecated, others, module.__name__) 790 mod_results += check_rest(module, set(names).difference(deprecated), 791 dots=dots) 792 if args.doctests: 793 mod_results += check_doctests(module, (args.verbose >= 2), dots=dots, 794 doctest_warnings=args.doctest_warnings) 795 796 for v in mod_results: 797 assert isinstance(v, tuple), v 798 799 results.append((module, mod_results)) 800 801 if dots: 802 sys.stderr.write("\n") 803 sys.stderr.flush() 804 805 if not args.skip_examples: 806 examples_path = os.path.join( 807 os.getcwd(), 'doc', 'source', 'regression', '*.rst') 808 print('\nChecking examples files at %s:' % examples_path) 809 for filename in sorted(glob.glob(examples_path)): 810 if dots: 811 sys.stderr.write('\n') 812 sys.stderr.write(os.path.split(filename)[1] + ' ') 813 sys.stderr.flush() 814 815 examples_results = check_doctests_testfile( 816 filename, (args.verbose >= 2), dots=dots, 817 doctest_warnings=args.doctest_warnings) 818 819 def scratch(): pass # stub out a "module", see below 820 scratch.__name__ = filename 821 results.append((scratch, examples_results)) 822 823 if dots: 824 sys.stderr.write("\n") 825 sys.stderr.flush() 826 827 # Report results 828 all_success = True 829 830 for module, mod_results in results: 831 success = all(x[1] for x in mod_results) 832 all_success = all_success and success 833 834 if success and args.verbose == 0: 835 continue 836 837 print("") 838 print("=" * len(module.__name__)) 839 print(module.__name__) 840 print("=" * len(module.__name__)) 841 print("") 842 843 for name, success, output in mod_results: 844 if name is None: 845 if not success or args.verbose >= 1: 846 print(output.strip()) 847 print("") 848 elif not success or (args.verbose >= 2 and output.strip()): 849 print(name) 850 print("-"*len(name)) 851 print("") 852 print(output.strip()) 853 print("") 854 855 if all_success: 856 print("\nOK: refguide and doctests checks passed!") 857 sys.exit(0) 858 else: 859 print("\nERROR: refguide or doctests have errors") 860 sys.exit(1) 861 862 863if __name__ == '__main__': 864 main(argv=sys.argv[1:]) 865