diff --git a/CHANGELOG.md b/CHANGELOG.md index f9a072c1..d44cad24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,15 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## Version 0.15.7 - Unreleased +### Changed +* Removed the distracting and very long internal traceback that occurred in + pytest when a module errors while it is being imported before the doctest is + run. + + ### Fixed * Bug in REQUIRES state did not respect `python_implementation` arguments +* Ported sphinx fixes from ubelt ## Version 0.15.6 - Released 2021-08-08 diff --git a/docs/requirements.txt b/docs/requirements.txt index 4c28754d..5602a7eb 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,12 +2,9 @@ sphinx sphinx-autobuild sphinx_rtd_theme sphinxcontrib-napoleon - sphinx-autoapi - six Pygments - ubelt - sphinx-reredirects +myst_parser diff --git a/docs/source/conf.py b/docs/source/conf.py index 0dbbcfde..a2a23784 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -282,7 +282,6 @@ def visit_Assign(self, node): sphobjinv suggest -t 90 -u https://readthedocs.org/projects/pytest/reference/objects.inv "signal.convolve2d" - python -m sphinx.ext.intersphinx https://pygments-doc.readthedocs.io/en/latest/objects.inv """ @@ -294,8 +293,107 @@ def visit_Assign(self, node): # 'pygments': ('https://pygments-doc.readthedocs.io/en/latest/', None), # 'colorama': ('https://pypi.org/project/colorama/', None), 'python': ('https://docs.python.org/3', None), - # 'ubelt': ('https://readthedocs.org/projects/ubelt/', None), + 'ubelt': ('https://ubelt.readthedocs.io/en/latest/', None), # 'numpy': ('http://docs.scipy.org/doc/numpy/', None), # 'cv2' : ('http://docs.opencv.org/2.4/', None), # 'h5py' : ('http://docs.h5py.org/en/latest/', None) } +__dev_note__ = """ +python -m sphinx.ext.intersphinx https://docs.python.org/3/objects.inv +python -m sphinx.ext.intersphinx https://ubelt.readthedocs.io/en/latest/objects.inv +python -m sphinx.ext.intersphinx https://networkx.org/documentation/stable/objects.inv +""" + + +# -- Extension configuration ------------------------------------------------- + + +from sphinx.domains.python import PythonDomain # NOQA + + +class PatchedPythonDomain(PythonDomain): + """ + References: + https://github.com/sphinx-doc/sphinx/issues/3866 + """ + def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): + # TODO: can use this to resolve references nicely + if target.startswith('xdoc.'): + target = 'xdoctest.' + target[3] + return_value = super(PatchedPythonDomain, self).resolve_xref( + env, fromdocname, builder, typ, target, node, contnode) + return return_value + + +def setup(app): + # app.add_domain(PatchedPythonDomain, override=True) + + if 1: + # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html + from sphinx.application import Sphinx + from typing import Any, List + + what = None + # Custom process to transform docstring lines + # Remove "Ignore" blocks + def process(app: Sphinx, what_: str, name: str, obj: Any, options: Any, lines: List[str] + ) -> None: + if what and what_ not in what: + return + orig_lines = lines[:] + + # text = '\n'.join(lines) + # if 'Example' in text and 'CommandLine' in text: + # import xdev + # xdev.embed() + + ignore_tags = tuple(['Ignore']) + + mode = None + # buffer = None + new_lines = [] + for i, line in enumerate(orig_lines): + + # See if the line triggers a mode change + if line.startswith(ignore_tags): + mode = 'ignore' + elif line.startswith('CommandLine'): + mode = 'cmdline' + elif line and not line.startswith(' '): + # if the line startswith anything but a space, we are no + # longer in the previous nested scope + mode = None + + if mode is None: + new_lines.append(line) + elif mode == 'ignore': + # print('IGNORE line = {!r}'.format(line)) + pass + elif mode == 'cmdline': + if line.startswith('CommandLine'): + new_lines.append('.. rubric:: CommandLine') + new_lines.append('') + new_lines.append('.. code-block:: bash') + new_lines.append('') + # new_lines.append(' # CommandLine') + else: + # new_lines.append(line.strip()) + new_lines.append(line) + else: + raise KeyError(mode) + + lines[:] = new_lines + # make sure there is a blank line at the end + if lines and lines[-1]: + lines.append('') + + app.connect('autodoc-process-docstring', process) + else: + # https://stackoverflow.com/questions/26534184/can-sphinx-ignore-certain-tags-in-python-docstrings + # Register a sphinx.ext.autodoc.between listener to ignore everything + # between lines that contain the word IGNORE + # from sphinx.ext.autodoc import between + # app.connect('autodoc-process-docstring', between('^ *Ignore:$', exclude=True)) + pass + + return app diff --git a/xdoctest/checker.py b/xdoctest/checker.py index 46dd15d6..05ca8698 100644 --- a/xdoctest/checker.py +++ b/xdoctest/checker.py @@ -412,7 +412,7 @@ def output_difference(self, runstate=None, colored=True): for a given example (`example`) and the actual output (`got`). The `runstate` contains option flags used to compare `want` and `got`. - Notes: + Note: This does not check if got matches want, it only outputs the raw differences. Got/Want normalization may make the differences appear more exagerated than they are. diff --git a/xdoctest/doctest_example.py b/xdoctest/doctest_example.py index 015523ae..c2709458 100644 --- a/xdoctest/doctest_example.py +++ b/xdoctest/doctest_example.py @@ -272,14 +272,16 @@ def is_disabled(self, pytest=False): """ Checks for comment directives on the first line of the doctest - A doctest is disabled if it starts with any of the following patterns: - # DISABLE_DOCTEST - # SCRIPT - # UNSTABLE - # FAILING + A doctest is disabled if it starts with any of the following patterns + + * ``>>> # DISABLE_DOCTEST`` + * ``>>> # SCRIPT`` + * ``>>> # UNSTABLE`` + * ``>>> # FAILING`` And if running in pytest, you can also use - # pytest.skip + + * ``>>> import pytest; pytest.skip()`` """ disable_patterns = [ r'>>>\s*#\s*DISABLE', @@ -300,15 +302,23 @@ def is_disabled(self, pytest=False): @property def unique_callname(self): + """ + A key that references this doctest within xdoctest given its module + """ return self.callname + ':' + str(self.num) @property def node(self): - """ this pytest node """ + """ + A key that references this doctest within pytest + """ return self.modpath + '::' + self.callname + ':' + str(self.num) @property def valid_testnames(self): + """ + A set of callname and unique_callname + """ return { self.callname, self.unique_callname, @@ -325,7 +335,9 @@ def wants(self): def format_parts(self, linenos=True, colored=None, want=True, offset_linenos=None, prefix=True): - """ used by format_src """ + """ + Used by :func:`format_src` + """ self._parse() colored = self.config.getvalue('colored', colored) partnos = self.config.getvalue('partnos') diff --git a/xdoctest/parser.py b/xdoctest/parser.py index 7f98f525..9eab6ab0 100644 --- a/xdoctest/parser.py +++ b/xdoctest/parser.py @@ -7,24 +7,28 @@ Terms and definitions: - logical block: a snippet of code that can be executed by itself if given - the correct global / local variable context. - - PS1 : The original meaning is "Prompt String 1". In the context of - xdoctest, instead of referring to the prompt prefix, we use PS1 to refer - to a line that starts a "logical block" of code. In the original - doctest module these all had to be prefixed with ">>>". In xdoctest the - prefix is used to simply denote the code is part of a doctest. It does - not necessarilly mean a new "logical block" is starting. - - PS2 : The original meaning is "Prompt String 2". In the context of - xdoctest, instead of referring to the prompt prefix, we use PS2 to refer - to a line that continues a "logical block" of code. In the original - doctest module these all had to be prefixed with "...". However, - xdoctest uses parsing to automatically determine this. - - want statement: Lines directly after a logical block of code in a doctest - indicating the desired result of executing the previous block. + logical block: + a snippet of code that can be executed by itself if given the correct + global / local variable context. + + PS1: + The original meaning is "Prompt String 1". In the context of xdoctest, + instead of referring to the prompt prefix, we use PS1 to refer to a + line that starts a "logical block" of code. In the original doctest + module these all had to be prefixed with ">>>". In xdoctest the prefix + is used to simply denote the code is part of a doctest. It does not + necessarilly mean a new "logical block" is starting. + + PS2: + The original meaning is "Prompt String 2". In the context of xdoctest, + instead of referring to the prompt prefix, we use PS2 to refer to a + line that continues a "logical block" of code. In the original doctest + module these all had to be prefixed with "...". However, xdoctest uses + parsing to automatically determine this. + + want statement: + Lines directly after a logical block of code in a doctest indicating + the desired result of executing the previous block. While I do believe this AST-based code is a significant improvement over the RE-based builtin doctest parser, I acknowledge that I'm not an AST expert and @@ -216,7 +220,8 @@ def _package_chunk(self, raw_source_lines, raw_want_lines, lineno=0): own part. Otherwise, statements are grouped by the closest `want` statement. - TODO: EXCEPT IN CASES OF EXPLICIT CONTINUATION + TODO: + - [ ] EXCEPT IN CASES OF EXPLICIT CONTINUATION Example: >>> from xdoctest.parser import * @@ -429,9 +434,10 @@ def _locate_ps1_linenos(self, source_lines): these will be unindented, prefixed, and without any want. Returns: - Tuple[List[int], bool]: a list of indices indicating which lines - are considered "PS1" and a flag indicating if the final line - should be considered for a got/want assertion. + Tuple[List[int], bool]: + a list of indices indicating which lines are considered "PS1" + and a flag indicating if the final line should be considered + for a got/want assertion. Example: >>> self = DoctestParser() @@ -557,14 +563,15 @@ def _workaround_16806(ps1_linenos, exec_source_lines): exec_source_lines (List[str]): code referenced by ps1_linenos Returns: - List[int]: new_ps1_lines: Fixed `ps1_linenos` where multiline - strings now point to the line where they begin. + List[int]: new_ps1_lines + Fixed `ps1_linenos` where multiline strings now point to the + line where they begin. - Notes: + Note: A patch for this issue exists - `https://github.com/python/cpython/pull/1800`. This workaround is a - idempotent (i.e. a no-op) when line numbers are correct, so nothing - should break when this bug is fixed. + ``_. This workaround + is a idempotent (i.e. a no-op) when line numbers are correct, so + nothing should break when this bug is fixed. Starting from the end look at consecutive pairs of indices to inspect the statement it corresponds to. (the first statement goes @@ -605,6 +612,9 @@ def _label_docsrc_lines(self, string): up by lines, each with a label indicating its type for later use in parsing. + TODO: + - [ ] Sphinx does not parse this doctest properly + Example: >>> from xdoctest.parser import * >>> # Having multiline strings in doctests can be nice diff --git a/xdoctest/utils/util_import.py b/xdoctest/utils/util_import.py index 61a7e077..df974247 100644 --- a/xdoctest/utils/util_import.py +++ b/xdoctest/utils/util_import.py @@ -212,7 +212,7 @@ def import_module_from_path(modpath, index=-1): References: https://stackoverflow.com/questions/67631/import-module-given-path - Notes: + Note: If the module is part of a package, the package will be imported first. These modules may cause problems when reloading via IPython magic @@ -231,6 +231,9 @@ def import_module_from_path(modpath, index=-1): For example if you try to import '/foo/bar/pkg/mod.py' from the folder structure: + + .. code:: + - foo/ +- bar/ +- pkg/ @@ -408,7 +411,7 @@ def _syspath_modname_to_modpath(modname, sys_path=None, exclude=None): list of directory paths. if specified prevents these directories from being searched. - Notes: + Note: This is much slower than the pkgutil mechanisms. Example: @@ -565,7 +568,7 @@ def normalize_modpath(modpath, hide_init=True, hide_main=False): Returns: PathLike: a normalized path to the module - Notes: + Note: Adds __init__ if reasonable, but only removes __main__ by default Example: diff --git a/xdoctest/utils/util_stream.py b/xdoctest/utils/util_stream.py index 0bc14971..87a413ec 100644 --- a/xdoctest/utils/util_stream.py +++ b/xdoctest/utils/util_stream.py @@ -44,7 +44,7 @@ def isatty(self): # nocover """ Returns true of the redirect is a terminal. - Notes: + Note: Needed for IPython.embed to work properly when this class is used to override stdout / stderr. """