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