"""
Core methods used by xdoctest runner and plugin code to statically extract
doctests from a module or package.
The following is a glossary of terms and jargon used in this repo.
* callname - the name of a callable function, method, class etc... e.g.
``myfunc``, ``MyClass``, or ``MyClass.some_method``.
* got / want - a test that produces stdout or a value to check. Whatever is
produced is what you "got" and whatever is expected is what you "want".
See :mod:`xdoctest.checker` for more details.
* directives - special in-doctest comments that change the behavior of the
doctests at runtime. See :mod:`xdoctest.directive` for more details.
* chevrons - the three cheverons (``>>> ``) or right angle brakets are the
standard prefix for a doctest, also referred to as a PS1 line in the
parser.
* zero-args - a function that can be called without any arguments.
* freeform style - This is the term used to refer to a doctest that could be
anywhere in the docstring. The alternative are structured doctests where
they are only expected in known positions like in "Example blocks" for
google and numpy style docstrings.
* TODO - complete this list (Make an issue or PR if there is any term you don't
immediately understand!).
"""
import sys
import textwrap
import warnings
import itertools as it
import types
from os.path import exists
from fnmatch import fnmatch
from xdoctest import dynamic_analysis
from xdoctest import static_analysis
from xdoctest import parser
from xdoctest import exceptions
from xdoctest import doctest_example
from xdoctest import utils
from xdoctest.docstr import docscrape_google
from xdoctest.utils import util_import
from xdoctest import global_state
DOCTEST_STYLES = [
'freeform',
'google',
'auto',
# 'numpy', # TODO
]
__docstubs__ = """
import xdoctest.doctest_example
"""
[docs]
def parse_google_docstr_examples(docstr, callname=None, modpath=None, lineno=1,
fpath=None, eager_parse=True):
"""
Parses Google-style doctests from a docstr and generates example objects
Args:
docstr (str): an extracted docstring
callname (str | None):
the name of the callable (e.g. function, class, or method)
that this docstring belongs to.
modpath (str | PathLike | None):
original module the docstring is from
lineno (int):
the line number (starting from 1) of the docstring. i.e. if you
were to go to this line number in the source file the starting
quotes of the docstr would be on this line. Defaults to 1.
fpath (str | PathLike | None):
the file that the docstring is from (if the file was not a module,
needed for backwards compatibility)
eager_parse (bool):
if True eagerly evaluate the parser inside the google example
blocks. Defaults to True.
Yields:
xdoctest.doctest_example.DocTest : doctest object
Raises:
xdoctest.exceptions.MalformedDocstr: if an error occurs in finding google blocks
xdoctest.exceptions.DoctestParseError: if an error occurs in parsing
"""
try:
blocks = docscrape_google.split_google_docblocks(docstr)
except exceptions.MalformedDocstr:
print('ERROR PARSING {} GOOGLE BLOCKS IN {} ON line {}'.format(
callname, modpath, lineno))
print('Did you forget to make a docstr with newlines raw?')
raise
example_blocks = []
example_tags = ('Example', 'Doctest', 'Script', 'Benchmark')
for type, block in blocks:
if type.startswith(example_tags):
example_blocks.append((type, block))
for num, (type, (docsrc, offset)) in enumerate(example_blocks):
# Add one because offset indicates the position of the block-label
# and the body of the block always starts on the next line.
label_lineno = lineno + offset
body_lineno = label_lineno + 1
example = doctest_example.DocTest(docsrc, modpath, callname, num,
lineno=body_lineno, fpath=fpath,
block_type=type)
if eager_parse:
# parse on the fly to be consistent with freeform?
example._parse()
yield example
[docs]
def parse_auto_docstr_examples(docstr, *args, **kwargs):
"""
First try to parse google style, but if no tests are found use freeform
style.
"""
if global_state.DEBUG_CORE: # nocover
print('Automatic style is trying google parsing')
n_found = 0
try:
for example in parse_google_docstr_examples(docstr, *args, **kwargs):
n_found += 1
yield example
except Exception:
if n_found > 0:
raise
# no google style tests were found, parse in freeform
if n_found == 0:
if global_state.DEBUG_CORE: # nocover
print('Automatic style is trying freeform parsing')
for example in parse_freeform_docstr_examples(docstr, *args, **kwargs):
yield example
[docs]
def parse_docstr_examples(docstr, callname=None, modpath=None, lineno=1,
style='auto', fpath=None, parser_kw=None):
"""
Parses doctests from a docstr and generates example objects.
The style influences which tests are found.
Args:
docstr (str): a previously extracted docstring
callname (str | None):
the name of the callable (e.g. function, class, or method)
that this docstring belongs to.
modpath (str | PathLike | None):
original module the docstring is from
lineno (int):
the line number (starting from 1) of the docstring. i.e. if you
were to go to this line number in the source file the starting
quotes of the docstr would be on this line. Defaults to 1.
style (str): expected doctest style, which can
be "google", "freeform", or "auto". Defaults to 'auto'.
fpath (str | PathLike | None):
the file that the docstring is from (if the file was not a module,
needed for backwards compatibility)
parser_kw (dict | None): passed to the parser as keyword args
Yields:
xdoctest.doctest_example.DocTest : parsed example
CommandLine:
python -m xdoctest.core parse_docstr_examples
Example:
>>> from xdoctest.core import *
>>> from xdoctest import utils
>>> docstr = utils.codeblock(
... '''
... >>> 1 + 1 # xdoctest: +SKIP
... 2
... >>> 2 + 2
... 4
... ''')
>>> examples = list(parse_docstr_examples(docstr, 'name', fpath='foo.txt', style='freeform'))
>>> print(len(examples))
1
>>> examples = list(parse_docstr_examples(docstr, fpath='foo.txt'))
"""
if global_state.DEBUG_CORE: # nocover
print('Parsing docstring examples for '
'callname={} in modpath={}'.format(callname, modpath))
if style == 'freeform':
parser = parse_freeform_docstr_examples
elif style == 'google':
parser = parse_google_docstr_examples
elif style == 'auto':
parser = parse_auto_docstr_examples
# TODO: epdoc
# TODO:
# elif style == 'numpy':
# parser = parse_numpy_docstr_examples
else:
raise KeyError('Unknown style={}. Valid styles are {}'.format(
style, DOCTEST_STYLES))
if global_state.DEBUG_CORE: # nocover
print('parser = {!r}'.format(parser))
n_parsed = 0
try:
if parser_kw is None:
parser_kw = {}
for example in parser(docstr, callname=callname, modpath=modpath,
fpath=fpath, lineno=lineno, **parser_kw):
n_parsed += 1
yield example
except Exception as ex:
if global_state.DEBUG_CORE: # nocover
print('Caught an error when parsing')
msg = ('Cannot scrape callname={} in modpath={} line={}.\n'
'Caused by: {}\n')
# raise
msg = msg.format(callname, modpath, lineno, repr(ex))
if isinstance(ex, exceptions.DoctestParseError):
# TODO: Can we print a nicer syntax error here?
msg += '{}\n'.format(ex.string)
msg += 'Original Error: {}\n'.format(repr(ex.orig_ex))
if isinstance(ex.orig_ex, SyntaxError):
extra_help = ''
if ex.orig_ex.text:
extra_help += utils.ensure_unicode(ex.orig_ex.text)
if ex.orig_ex.offset is not None:
extra_help += ' ' * (ex.orig_ex.offset - 1) + '^'
if extra_help:
msg += '\n' + extra_help
# Always warn when something bad is happening.
# However, dont error if the docstr simply has bad syntax
print('msg = {}'.format(msg))
warnings.warn(msg)
if isinstance(ex, exceptions.MalformedDocstr):
pass
elif isinstance(ex, exceptions.DoctestParseError):
pass
else:
raise
if global_state.DEBUG_CORE: # nocover
print('Finished parsing {} examples'.format(n_parsed))
def _rectify_to_modpath(modpath_or_name):
""" if modpath_or_name is a name, statically converts it to a path """
if isinstance(modpath_or_name, types.ModuleType):
raise TypeError('Expected a static module but got a dynamic one')
modpath = util_import.modname_to_modpath(modpath_or_name)
if modpath is None:
if exists(modpath_or_name):
modpath = modpath_or_name
else:
raise ValueError('Cannot find module={}'.format(modpath_or_name))
return modpath
[docs]
def package_calldefs(pkg_identifier, exclude=[], ignore_syntax_errors=True,
analysis='auto'):
"""
Statically generates all callable definitions in a module or package
Args:
pkg_identifier (str | ModuleType): path to or name of the module to be
tested (or the live module itself, which is not recommended)
exclude (List[str]): glob-patterns of file names to exclude
ignore_syntax_errors (bool):
if False raise an error when syntax errors occur in a doctest
Defaults to True.
analysis (str):
if 'static', only static analysis is used to parse call
definitions. If 'auto', uses dynamic analysis for compiled python
extensions, but static analysis elsewhere, if 'dynamic', then
dynamic analysis is used to parse all calldefs. Defaults to 'auto'.
Yields:
Tuple[Dict[str, xdoctest.static_analysis.CallDefNode], str | ModuleType] -
* item[0]: the mapping of callnames-to-calldefs
* item[1]: the path to the file containing the doctest
(usually a module) or the module itself
Example:
>>> pkg_identifier = 'xdoctest.core'
>>> testables = list(package_calldefs(pkg_identifier))
>>> assert len(testables) == 1
>>> calldefs, modpath = testables[0]
>>> assert util_import.modpath_to_modname(modpath) == pkg_identifier
>>> assert 'package_calldefs' in calldefs
"""
if global_state.DEBUG_CORE: # nocover
print('Find package calldefs: pkg_identifier = {!r}'.format(pkg_identifier))
if isinstance(pkg_identifier, types.ModuleType):
# Case where we are forced to use a live module
identifiers = [pkg_identifier]
else:
pkgpath = _rectify_to_modpath(pkg_identifier)
identifiers = list(static_analysis.package_modpaths(
pkgpath, with_pkg=True, with_libs=True))
for module_identifier in identifiers:
if isinstance(module_identifier, str):
modpath = module_identifier
modname = util_import.modpath_to_modname(modpath)
if any(fnmatch(modname, pat) for pat in exclude):
continue
if not exists(modpath):
warnings.warn(
'Module {} does not exist. '
'Is it an old pyc file?'.format(modname))
continue
try:
calldefs = parse_calldefs(module_identifier, analysis=analysis)
if calldefs is not None:
yield calldefs, module_identifier
except SyntaxError as ex:
# Handle error due to the actual code containing errors
msg = 'Cannot parse module={}.\nCaused by: {}'
msg = msg.format(module_identifier, ex)
if ignore_syntax_errors:
warnings.warn(msg) # real code or docstr contained errors
else:
raise SyntaxError(msg)
[docs]
def parse_calldefs(module_identifier, analysis='auto'):
"""
Parse calldefs from a single module using either static or dynamic
analysis.
Args:
module_identifier (str | ModuleType): path to or name of the module to be
tested (or the live module itself, which is not recommended)
analysis (str, default='auto'):
if 'static', only static analysis is used to parse call
definitions. If 'auto', uses dynamic analysis for compiled python
extensions, but static analysis elsewhere, if 'dynamic', then
dynamic analysis is used to parse all calldefs.
Returns:
Dict[str, xdoctest.static_analysis.CallDefNode]:
the mapping of callnames-to-calldefs within the module.
"""
# backwards compatibility hacks
if '--allow-xdoc-dynamic' in sys.argv:
from xdoctest.utils import util_deprecation
util_deprecation.schedule_deprecation(
modname='xdoctest',
name='--allow-xdoc-dynamic', type='CLI flag',
migration='use --analysis=auto instead',
deprecate='1.0.0', error='1.1.0', remove='1.2.0'
)
analysis = 'auto'
if '--xdoc-force-dynamic' in sys.argv:
from xdoctest.utils import util_deprecation
util_deprecation.schedule_deprecation(
modname='xdoctest',
name='--xdoc-force-dynamic', type='CLI flag',
migration='use --analysis=dynamic instead',
deprecate='1.0.0', error='1.1.0', remove='1.2.0'
)
analysis = 'dynamic'
if isinstance(module_identifier, types.ModuleType):
# identifier is a live module
need_dynamic = True
else:
# identifier is a path to a module
modpath = module_identifier
# Certain files (notebooks and c-extensions) require dynamic analysis
need_dynamic = modpath.endswith(
static_analysis._platform_pylib_exts())
if modpath.endswith('.ipynb'):
need_dynamic = True
if analysis == 'static':
if need_dynamic:
# Some modules can only be parsed dynamically
raise Exception((
'Static analysis required, but {} requires '
'dynamic analysis').format(module_identifier))
do_dynamic = False
elif analysis == 'dynamic':
do_dynamic = True
elif analysis == 'auto':
do_dynamic = need_dynamic
else:
raise KeyError(analysis)
if global_state.DEBUG_CORE: # nocover
print('About to parse calldefs with do_dynamic={}'.format(do_dynamic))
calldefs = None
if do_dynamic:
try:
calldefs = dynamic_analysis.parse_dynamic_calldefs(module_identifier)
except (ImportError, RuntimeError) as ex:
# Some modules are just c modules
msg = 'Cannot dynamically parse module={}.\nCaused by: {!r} {}'
msg = msg.format(module_identifier, type(ex), ex)
warnings.warn(msg)
except Exception as ex:
msg = 'Cannot dynamically parse module={}.\nCaused by: {!r} {}'
msg = msg.format(module_identifier, type(ex), ex)
warnings.warn(msg)
raise
else:
calldefs = static_analysis.parse_static_calldefs(fpath=module_identifier)
if global_state.DEBUG_CORE: # nocover
print('Found {} calldefs'.format(len(calldefs)))
return calldefs
[docs]
def parse_doctestables(module_identifier, exclude=[], style='auto',
ignore_syntax_errors=True, parser_kw={},
analysis='auto'):
"""
Parses all doctests within top-level callables of a module and generates
example objects. The style influences which tests are found.
Args:
module_identifier (str | PathLike | ModuleType):
path or name of a module or a module itself (we prefer a path)
exclude (List[str]): glob-patterns of file names to exclude
style (str): expected doctest style (e.g. google, freeform, auto)
ignore_syntax_errors (bool, default=True):
if False raise an error when syntax errors
parser_kw: extra args passed to the parser
analysis (str, default='auto'):
if 'static', only static analysis is used to parse call
definitions. If 'auto', uses dynamic analysis for compiled python
extensions, but static analysis elsewhere, if 'dynamic', then
dynamic analysis is used to parse all calldefs.
Yields:
xdoctest.doctest_example.DocTest : parsed doctest example objects
CommandLine:
python -m xdoctest.core parse_doctestables
Example:
>>> module_identifier = 'xdoctest.core'
>>> testables = list(parse_doctestables(module_identifier))
>>> this_example = None
>>> for example in testables:
>>> # print(example)
>>> if example.callname == 'parse_doctestables':
>>> this_example = example
>>> assert this_example is not None
>>> assert this_example.callname == 'parse_doctestables'
Example:
>>> from xdoctest import utils
>>> docstr = utils.codeblock(
... '''
... >>> 1 + 1 # xdoctest: +SKIP
... 2
... >>> 2 + 2
... 4
... ''')
>>> temp = utils.TempDoctest(docstr, 'test_modfile')
>>> modpath = temp.modpath
>>> examples = list(parse_doctestables(modpath, style='freeform'))
>>> print(len(examples))
1
"""
if style not in DOCTEST_STYLES:
raise KeyError('Unknown style={}. Valid styles are {}'.format(
style, DOCTEST_STYLES))
# Statically parse modules and their doctestable callables in a package
for calldefs, modpath in package_calldefs(module_identifier, exclude,
ignore_syntax_errors,
analysis=analysis):
for callname, calldef in calldefs.items():
docstr = calldef.docstr
if calldef.docstr is not None:
lineno = calldef.doclineno
example_gen = parse_docstr_examples(
docstr, callname=callname, modpath=modpath, lineno=lineno,
style=style, parser_kw=parser_kw)
if global_state.DEBUG_CORE: # nocover
for example in example_gen:
print(' * Yield example={}'.format(example))
yield example
else:
for example in example_gen:
yield example
if __name__ == '__main__':
"""
CommandLine:
python -m xdoctest.core all
"""
import xdoctest as xdoc
xdoc.doctest_module()