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