Source code for xdoctest.doctest_part

"""
Simple storage container used to store a single executable part of a doctest
example. Multiple parts are typically stored in a
:class:`xdoctest.doctest_example.Doctest`, which manages execution of each
part.
"""
import math
from xdoctest import utils
from xdoctest import checker
from xdoctest import directive
from xdoctest import constants


__devnotes__ = """
TODO:
    perhaps rename doctest part to DoctestCell, because there is a striking
    similarity between Jupyter notebook cells and doctest parts.
"""


[docs] class DoctestPart(object): """ The result of parsing that represents a "logical block" of code. If a want statement is defined, it is stored here. Attributes: exec_lines (List[str]): executable lines in this part want_lines (List[str] | None): lines that the result of the execution should match line_offset (int): line number relative to the start of the doctest orig_lines (List[str] | None): the original text parsed into exec and want directives (list | None): directives that this part will apply before being run partno (int | None): identifies the part number in the larger example compile_mode (str): mode passed to compile. """ def __init__(self, exec_lines, want_lines=None, line_offset=0, orig_lines=None, directives=None, partno=None): """ Args: exec_lines (List[str]): executable lines in this part want_lines (List[str] | None): lines that the result of the execution should match line_offset (int): line number relative to the start of the doctest orig_lines (List[str] | None): The original text parsed into exec and want. This is only used in formatting and may be removed in the future. directives (list | None): directives that this part will apply before being run. If unspecified, these will be extracted. partno (int | None): identifies the part number in the larger example """ self.exec_lines = exec_lines self.want_lines = want_lines self.line_offset = line_offset self.orig_lines = orig_lines self._directives = directives self.partno = partno self.compile_mode = 'exec' @property def n_lines(self): """ Returns: int: number of lines in the entire source (i.e. exec + want) """ return self.n_exec_lines + self.n_want_lines @property def n_exec_lines(self): """ Returns: int: number of executable lines (excluding want) """ return len(self.exec_lines) @property def n_want_lines(self): """ Returns: int: number of lines in the "want" statement. """ if self.want_lines: return len(self.want_lines) else: return 0 @property def source(self): """ Returns: str: A single block of text representing the source code. """ return '\n'.join(self.exec_lines)
[docs] def compilable_source(self): """ Use this to build the string for compile. Takes care of a corner case. Returns: str """ if self.compile_mode == 'single': return '\n'.join(self.exec_lines + ['']) else: return '\n'.join(self.exec_lines)
[docs] def has_any_code(self): """ Heuristic to check if there is any runnable code in this doctest. We currently just check that not every line is a comment, which helps the runner count a test as skipped if only lines with comments "ran". Returns: bool """ slines = [line.strip() for line in self.exec_lines] return not all( not line or line.startswith('#') for line in slines )
@property def directives(self): """ Returns: List[directive.Directive]: The extracted or provided directives to be applied. Example: >>> self = DoctestPart(['# doctest: +SKIP'], None, 0) >>> print(', '.join(list(map(str, self.directives)))) <Directive(+SKIP)> """ if self._directives is None: self._directives = list(directive.Directive.extract(self.source)) return self._directives @property def want(self): """ Returns: str | None: what the test is expected to produce """ if self.want_lines: return '\n'.join(self.want_lines) else: return None def __nice__(self): """ Returns: str: a pretty and concise "nice" representation """ parts = [] if self.line_offset is not None: parts.append('ln %s' % (self.line_offset)) if self.source: head_src = self.source.splitlines()[0][0:8] parts.append('src="%s..."' % (head_src,)) else: parts.append('src=""') if self.want is None: parts.append('want=None') else: head_wnt = self.want.splitlines()[0][0:8] parts.append('want="%s..."' % (head_wnt,)) return ', '.join(parts) def __repr__(self): classname = self.__class__.__name__ devnice = self.__nice__() return '<%s(%s) at %s>' % (classname, devnice, hex(id(self))) def __str__(self): classname = self.__class__.__name__ devnice = self.__nice__() return '<%s(%s)>' % (classname, devnice)
[docs] def check(part, got_stdout, got_eval=constants.NOT_EVALED, runstate=None, unmatched=None): r""" Check if the "got" output obtained by running this test matches the "want" target. Note there are two types of "got" output: (1) output from stdout and (2) evaled output. If both are specified, then want may match either value. Args: got_stdout (str): output from stdout got_eval (str): output from an eval statement runstate (directive.RuntimeState): runner options unmatched (list): if specified, the want statement is allowed to match any trailing sequence of unmatched output and got_stdout from this doctest part. Raises: xdoctest.checker.GotWantException - If the "got" differs from this parts want. Example: >>> # xdoctest: +REQUIRES(module:pytest) >>> import pytest >>> got_stdout = 'more text\n' >>> unmatched = ['some text\n'] >>> self = DoctestPart(None, want_lines=['some text', 'more text']) >>> self.check(got_stdout, unmatched=unmatched) >>> # Leading junk doesnt matter if we match a trailing sequence >>> self.check(got_stdout, unmatched=['junk\n'] + unmatched) >>> # fail when want doesnt match any trailing sequence >>> with pytest.raises(checker.GotWantException): >>> self.check(got_stdout) >>> with pytest.raises(checker.GotWantException): >>> self.check(got_stdout, ['some text\n', 'junk\n']) """ if unmatched is None: unmatched = [] trailing_gots = unmatched + [got_stdout] success = False exceptions = [] for i in range(1, len(trailing_gots) + 1): # Try the i-th trailing sequence got_ = ''.join(trailing_gots[-i:]) try: checker.check_got_vs_want(part.want, got_, got_eval, runstate) except checker.GotWantException as ex: exceptions.append(ex) else: success = True break if not success: # for ex in exceptions: # print(ex.output_difference()) # print(ex.output_repr_difference()) # If none of the checks pass, return the error message with the # largest got message. (perhaps the output with the closest edit # distance might be better to report?) raise exceptions[-1]
[docs] def format_part(self, linenos=True, want=True, startline=1, n_digits=None, colored=False, partnos=False, prefix=True): """ Customizable formatting of the source and want for this doctest. Args: linenos (bool): show line numbers want (bool): include the want value if it exists startline (int): offsets the line numbering n_digits (int): number of digits to use for line numbers colored (bool): pygmentize the code partnos (bool): if True, shows the part number in the string prefix (bool): if False, exclude the doctest ``>>> `` prefix Returns: str: pretty text suitable for printing CommandLine: python -m xdoctest.doctest_part DoctestPart.format_part Example: >>> from xdoctest.parser import * >>> self = DoctestPart(exec_lines=['print(123)'], >>> want_lines=['123'], line_offset=0, partno=1) >>> # xdoctest: -NORMALIZE_WHITESPACE >>> print(self.format_part(partnos=True)) (p1) 1 >>> print(123) 123 Example: >>> from xdoctest.parser import * >>> self = DoctestPart(exec_lines=['print(123)'], >>> want_lines=['123'], line_offset=0, partno=1) >>> # xdoctest: -NORMALIZE_WHITESPACE >>> print(self.format_part(partnos=False, prefix=False, >>> linenos=False, want=False)) print(123) """ if prefix: # Show the original line prefix when possible if self.orig_lines is None: src_text = utils.indent(self.source, '>>> ') else: src_text = '\n'.join(self.orig_lines) else: src_text = self.source want_text = self.want if self.want else '' if n_digits is None: endline = startline + self.n_lines n_digits = math.log(max(1, endline), 10) n_digits = int(math.ceil(n_digits)) part_lines = src_text.splitlines() n_spaces = 0 if linenos: n_spaces += n_digits + 1 start = startline + self.line_offset part_lines = utils.add_line_numbers(part_lines, n_digits=n_digits, start=start) if partnos: part_lines = [ '(p{}) {}'.format(self.partno, line) for line in part_lines ] n_spaces += 4 + 1 # FIXME could be more robust if more than 9 parts want_lines = [] if want_text: want_fmt = ' ' * n_spaces + '{line}' for line in want_text.splitlines(): if want: want_lines.append(want_fmt.format(line=line)) part_text = '\n'.join(part_lines) want_text = '\n'.join(want_lines) if colored: part_text = utils.highlight_code(part_text, 'python') want_text = utils.color_text(want_text, 'green') if want_lines: part_text += '\n' + want_text return part_text
if __name__ == '__main__': """ CommandLine: python -m xdoctest.doctest_part """ import xdoctest xdoctest.doctest_module(__file__)