From a3fe9f074f3348f81fc7210896a2f7989156c637 Mon Sep 17 00:00:00 2001 From: joncrall Date: Sun, 8 Aug 2021 16:05:34 -0400 Subject: [PATCH 1/7] Start branch for 0.15.7 --- CHANGELOG.md | 5 ++++- xdoctest/__init__.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89e1871e..6835f5ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,10 @@ We are currently working on porting this changelog to the specifications in This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Version 0.15.6 - Unreleased +## Version 0.15.7 - Unreleased + + +## Version 0.15.6 - Released 2021-08-08 ### Changed diff --git a/xdoctest/__init__.py b/xdoctest/__init__.py index ad232a1a..75f34f2c 100644 --- a/xdoctest/__init__.py +++ b/xdoctest/__init__.py @@ -280,7 +280,7 @@ def fib(n): mkinit xdoctest --nomods ''' -__version__ = '0.15.6' +__version__ = '0.15.7' # Expose only select submodules From a5b264f3661d2011398b118e21413c0a9cc120a0 Mon Sep 17 00:00:00 2001 From: joncrall Date: Sun, 8 Aug 2021 16:38:11 -0400 Subject: [PATCH 2/7] Add in hotfix notes --- .circleci/config.yml | 16 ++++++++-------- CHANGELOG.md | 2 ++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 332ea428..75be81c1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,7 +66,7 @@ __doc__: &__doc__ This is only needed if you want to automatically sign published wheels with a gpg key. - * GITHUB_PUSH_TOKEN - + * PERSONAL_GITHUB_PUSH_TOKEN - This is only needed if you want to automatically git-tag release branches. To make a API token go to: @@ -80,7 +80,7 @@ __doc__: &__doc__ Do whatever you need to locally access the values of these variables echo $TWINE_USERNAME - echo $GITHUB_PUSH_TOKEN + echo $PERSONAL_GITHUB_PUSH_TOKEN echo $CIRCLE_CI_SECRET echo $TWINE_PASSWORD @@ -122,7 +122,7 @@ __doc__: &__doc__ export TWINE_USERNAME= export TWINE_PASSWORD= export CIRCLE_CI_SECRET="" - export GITHUB_PUSH_TOKEN='git-push-token:' + export PERSONAL_GITHUB_PUSH_TOKEN='git-push-token:' ``` You should also make a secret_unloader.sh that points to a script that @@ -180,10 +180,10 @@ __doc__: &__doc__ ``` - TEST GITHUB_PUSH_TOKEN + TEST PERSONAL_GITHUB_PUSH_TOKEN ------------------- - The following script tests if your GITHUB_PUSH_TOKEN environment variable is correctly setup. + The following script tests if your PERSONAL_GITHUB_PUSH_TOKEN environment variable is correctly setup. ```bash docker run -it ubuntu @@ -196,7 +196,7 @@ __doc__: &__doc__ URL_HOST=$(git remote get-url origin | sed -e 's|https\?://.*@||g' | sed -e 's|https\?://||g') echo "URL_HOST = $URL_HOST" git tag "test-tag4" - git push --tags "https://${GITHUB_PUSH_TOKEN}@${URL_HOST}" + git push --tags "https://${PERSONAL_GITHUB_PUSH_TOKEN}@${URL_HOST}" # Cleanup after you verify the tags shows up on the remote git push --delete origin test-tag4 @@ -445,7 +445,7 @@ jobs: # Have the server git-tag the release and push the tags VERSION=$($PYTHON_EXE -c "import setup; print(setup.VERSION)") # do sed twice to handle the case of https clone with and without a read token - URL_HOST=$(git remote get-url origin | sed -e 's|https\?://.*@||g' | sed -e 's|https\?://||g') + URL_HOST=$(git remote get-url origin | sed -e 's|https\?://.*@||g' | sed -e 's|https\?://||g' | sed -e 's|git@||g' | sed -e 's|:|/|g') echo "URL_HOST = $URL_HOST" # A git config user name and email is required. Set if needed. if [[ "$(git config user.email)" == "" ]]; then @@ -456,7 +456,7 @@ jobs: echo "Tag already exists" else git tag $VERSION -m "tarball tag $VERSION" - git push --tags "https://${GITHUB_PUSH_TOKEN}@${URL_HOST}" + git push --tags "https://${PERSONAL_GITHUB_PUSH_TOKEN}@${URL_HOST}" fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 6835f5ce..a0594d04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm * Directive syntax errors are now handled as doctest runtime errors and return better debugging information. +* README and docs were improved + ## Version 0.15.5 - Released 2021-06-27 From 0e94adfb979f59672a24955689ee32cfb7554be5 Mon Sep 17 00:00:00 2001 From: "jon.crall" Date: Mon, 9 Aug 2021 17:57:20 -0400 Subject: [PATCH 3/7] Fix warning in test --- testing/test_traceback.py | 121 ++++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 56 deletions(-) diff --git a/testing/test_traceback.py b/testing/test_traceback.py index 1b91f943..95959d59 100644 --- a/testing/test_traceback.py +++ b/testing/test_traceback.py @@ -7,19 +7,22 @@ def test_fail_call_onefunc(): - text = _run_case(utils.codeblock( - ''' - def func(a): - """ - Example: - >>> a = 1 - >>> func(a) - """ - a = []() - return a - ''')) - assert '>>> func(a)' in text - assert 'rel: 2, abs: 5' in text + import warnings + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + text = _run_case(utils.codeblock( + ''' + def func(a): + """ + Example: + >>> a = 1 + >>> func(a) + """ + a = []() + return a + ''')) + assert '>>> func(a)' in text + assert 'rel: 2, abs: 5' in text def test_fail_call_twofunc(): @@ -27,27 +30,30 @@ def test_fail_call_twofunc(): python ~/code/xdoctest/testing/test_traceback.py test_fail_call_twofunc """ - text = _run_case(utils.codeblock( - ''' - def func(a): - """ - Example: - >>> a = 1 - >>> func(a) - """ - a = []() - return a + import warnings + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + text = _run_case(utils.codeblock( + ''' + def func(a): + """ + Example: + >>> a = 1 + >>> func(a) + """ + a = []() + return a - def func2(a): - """ - Example: - >>> pass - """ - pass - ''')) - assert text - assert '>>> func(a)' in text - assert 'rel: 2, abs: 5,' in text + def func2(a): + """ + Example: + >>> pass + """ + pass + ''')) + assert text + assert '>>> func(a)' in text + assert 'rel: 2, abs: 5,' in text def test_fail_inside_twofunc(): @@ -55,30 +61,33 @@ def test_fail_inside_twofunc(): python ~/code/xdoctest/testing/test_traceback.py test_fail_inside_twofunc """ - text = _run_case(utils.codeblock( - ''' - def func(a): - """ - Example: - >>> print('not failed') - >>> # just a comment - >>> print(("foo" - ... "bar")) - >>> a = []() - >>> func(a) - """ - return a + import warnings + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + text = _run_case(utils.codeblock( + ''' + def func(a): + """ + Example: + >>> print('not failed') + >>> # just a comment + >>> print(("foo" + ... "bar")) + >>> a = []() + >>> func(a) + """ + return a - def func2(a): - """ - Example: - >>> pass - """ - pass - ''')) - assert text - assert '>>> a = []()' in text - assert 'rel: 5, abs: 8' in text + def func2(a): + """ + Example: + >>> pass + """ + pass + ''')) + assert text + assert '>>> a = []()' in text + assert 'rel: 5, abs: 8' in text def test_fail_inside_onefunc(): From 5f2786b5fac5b02790cc42ce90d47f47bb5e8324 Mon Sep 17 00:00:00 2001 From: joncrall Date: Sun, 22 Aug 2021 14:47:16 -0400 Subject: [PATCH 4/7] Fix bug in REQUIRES for platform_implementation --- CHANGELOG.md | 2 ++ xdoctest/directive.py | 15 +++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0594d04..f9a072c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## Version 0.15.7 - Unreleased +### Fixed +* Bug in REQUIRES state did not respect `python_implementation` arguments ## Version 0.15.6 - Released 2021-08-08 diff --git a/xdoctest/directive.py b/xdoctest/directive.py index 0538b8a5..9b7df2e4 100644 --- a/xdoctest/directive.py +++ b/xdoctest/directive.py @@ -343,6 +343,12 @@ def extract(cls, text): directive pattern. Because ``xdoctest`` is parsing the text, this issue does not occur. + Example: + >>> from xdoctest.directive import Directive, RuntimeState + >>> state = RuntimeState() + >>> state.update(Directive.extract('# xdoctest: +REQUIRES(CPYTHON)')) + >>> list([0].effects()[0] + Example: >>> from xdoctest.directive import Directive >>> text = '# xdoc: + SKIP' @@ -409,7 +415,7 @@ def __nice__(self): return '{}{}'.format(prefix, self.name) def _unpack_args(self, num): - warnings.warning('Deprecated and will be removed', DeprecationWarning) + warnings.warn('Deprecated and will be removed', DeprecationWarning) nargs = self.args if len(nargs) != 1: raise TypeError( @@ -418,7 +424,7 @@ def _unpack_args(self, num): return self.args def effect(self, argv=None, environ=None): - warnings.warning('Deprecated use effects', DeprecationWarning) + warnings.warn('Deprecated use effects', DeprecationWarning) effects = self.effects(argv=argv, environ=environ) if len(effects) > 1: raise Exception('Old method cannot hanldle multiple effects') @@ -572,6 +578,7 @@ def _is_requires_satisfied(arg, argv=None, environ=None): bool: flag - True if the requirement is met Example: + >>> from xdoctest.directive import * # NOQA >>> _is_requires_satisfied('PY2', argv=[]) >>> _is_requires_satisfied('PY3', argv=[]) >>> _is_requires_satisfied('cpython', argv=[]) @@ -651,12 +658,12 @@ def _is_requires_satisfied(arg, argv=None, environ=None): else: raise ValueError('Too many expr_parts={}'.format(expr_parts)) elif arg_lower in SYS_PLATFORM_TAGS: - flag = sys.platform.startswith(arg_lower) + flag = sys.platform.lower().startswith(arg_lower) elif arg_lower in OS_NAME_TAGS: flag = os.name.startswith(arg_lower) elif arg_lower in PY_IMPL_TAGS: import platform - flag = platform.python_implementation().startswith(arg_lower) + flag = platform.python_implementation().lower().startswith(arg_lower) elif arg_lower in PY_VER_TAGS: if sys.version_info[0] == 2: # nocover flag = arg_lower == 'py2' From 26214d220e515883a573713c82c57ce13e69baa8 Mon Sep 17 00:00:00 2001 From: joncrall Date: Thu, 2 Sep 2021 13:51:27 -0400 Subject: [PATCH 5/7] Attempt at reducing clutter seen in import errors --- testing/test_pytest_errors.py | 138 ++++++++++++++++++++++++++++++++++ xdoctest/directive.py | 33 +++++--- xdoctest/doctest_example.py | 57 +++++++++++--- xdoctest/utils/util_misc.py | 24 ++++++ 4 files changed, 233 insertions(+), 19 deletions(-) create mode 100644 testing/test_pytest_errors.py diff --git a/testing/test_pytest_errors.py b/testing/test_pytest_errors.py new file mode 100644 index 00000000..85cf4bee --- /dev/null +++ b/testing/test_pytest_errors.py @@ -0,0 +1,138 @@ +from xdoctest.utils import util_misc +from xdoctest import utils + + +def cmd(command): + # simplified version of ub.cmd no fancy tee behavior + import subprocess + proc = subprocess.Popen( + command, shell=True, universal_newlines=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + out, err = proc.communicate() + ret = proc.wait() + info = { + 'proc': proc, + 'out': out, + 'test_doctest_in_notebook.ipynberr': err, + 'ret': ret, + } + return info + + +def test_simple_pytest_cli(): + module_text = utils.codeblock( + ''' + def module_func1(): + """ + This module has a doctest + + Example: + >>> print('hello world') + """ + ''') + temp_module = util_misc.TempModule(module_text) + modpath = temp_module.modpath + + info = cmd('pytest --xdoctest ' + modpath) + print(info['out']) + assert info['ret'] == 0 + + +def test_simple_pytest_import_error_cli(): + """ + This test case triggers an excessively long callback in xdoctest < + dev/0.15.7 + + xdoctest ~/code/xdoctest/testing/test_pytest_errors.py test_simple_pytest_import_error_cli + + import sys, ubelt + sys.path.append(ubelt.expandpath('~/code/xdoctest/testing')) + from test_pytest_errors import * # NOQA + """ + module_text = utils.codeblock( + ''' + # There are lines before the bad line + import os + import sys + import does_not_exist + + def module_func1(): + """ + This module has a doctest + + Example: + >>> print('hello world') + """ + ''') + temp_module = util_misc.TempModule(module_text, modname='imperr_test_mod') + command = 'pytest -v -s --xdoctest-verbose=3 --xdoctest ' + temp_module.dpath + print(command) + info = cmd(command) + print(info['out']) + + # info = cmd('pytest --xdoctest ' + temp_module.modpath) + # print(info['out']) + + assert info['ret'] == 1 + + +def test_simple_pytest_syntax_error_cli(): + """ + """ + module_text = utils.codeblock( + ''' + &&does_not_exist + + def module_func1(): + """ + This module has a doctest + + Example: + >>> print('hello world') + """ + ''') + temp_module = util_misc.TempModule(module_text) + info = cmd('pytest --xdoctest ' + temp_module.dpath) + print(info['out']) + + info = cmd('pytest --xdoctest ' + temp_module.modpath) + print(info['out']) + + +def test_simple_pytest_import_error_no_xdoctest(): + """ + """ + module_text = utils.codeblock( + ''' + import does_not_exist + + def test_this(): + print('hello world') + ''') + temp_module = util_misc.TempModule(module_text) + info = cmd('pytest ' + temp_module.modpath) + print(info['out']) + + info = cmd('pytest ' + temp_module.dpath) + print(info['out']) + # assert info['ret'] == 0 + + +def test_simple_pytest_syntax_error_no_xdoctest(): + """ + """ + module_text = utils.codeblock( + ''' + &&does_not_exist + + def test_this(): + print('hello world') + ''') + temp_module = util_misc.TempModule(module_text) + info = cmd('pytest ' + temp_module.modpath) + print(info['out']) + + info = cmd('pytest ' + temp_module.dpath) + print(info['out']) + # assert info['ret'] == 0 diff --git a/xdoctest/directive.py b/xdoctest/directive.py index 9b7df2e4..66ff52d1 100644 --- a/xdoctest/directive.py +++ b/xdoctest/directive.py @@ -70,11 +70,10 @@ CommandLine: python -m xdoctest.directive __doc__ +The following example shows how the ``+SKIP`` directives may be used to +bypass certain places in the code. Example: - The following example shows how the ``+SKIP`` directives may be used to - bypass certain places in the code. - >>> # An inline directive appears on the same line as a command and >>> # only applies to the current line. >>> raise AssertionError('this will not be run (a)') # xdoctest: +SKIP @@ -98,10 +97,10 @@ >>> # xdoctest: -SKIP >>> print('This line will print: (F)') -Example: - This next examples illustrates how to use the advanced ``+REQURIES()`` - directive. Note, the REQUIRES and SKIP states are independent. +This next examples illustrates how to use the advanced ``+REQURIES()`` +directive. Note, the REQUIRES and SKIP states are independent. +Example: >>> import sys >>> count = 0 >>> # xdoctest: +REQUIRES(WIN32) @@ -338,7 +337,7 @@ def extract(cls, text): Yeilds: Directive: directive: the parsed directives - Notes: + Note: The original ``doctest`` module sometimes yeilded false positives for a directive pattern. Because ``xdoctest`` is parsing the text, this issue does not occur. @@ -346,8 +345,23 @@ def extract(cls, text): Example: >>> from xdoctest.directive import Directive, RuntimeState >>> state = RuntimeState() - >>> state.update(Directive.extract('# xdoctest: +REQUIRES(CPYTHON)')) - >>> list([0].effects()[0] + >>> assert len(state['REQUIRES']) == 0 + >>> extracted1 = list(Directive.extract('# xdoctest: +REQUIRES(CPYTHON)')) + >>> extracted2 = list(Directive.extract('# xdoctest: +REQUIRES(PYPY)')) + >>> print('extracted1 = {!r}'.format(extracted1)) + >>> print('extracted2 = {!r}'.format(extracted2)) + >>> effect1 = extracted1[0].effects()[0] + >>> effect2 = extracted2[0].effects()[0] + >>> print('effect1 = {!r}'.format(effect1)) + >>> print('effect2 = {!r}'.format(effect2)) + >>> assert effect1.value == 'CPYTHON' + >>> assert effect2.value == 'PYPY' + >>> # At least one of these will not be satisfied + >>> assert effect1.action == 'set.add' or effect2.action == 'set.add' + >>> state.update(extracted1) + >>> state.update(extracted2) + >>> print('state = {!r}'.format(state)) + >>> assert len(state['REQUIRES']) > 0 Example: >>> from xdoctest.directive import Directive @@ -379,6 +393,7 @@ def extract(cls, text): Example: + >>> from xdoctest.directive import Directive, RuntimeState >>> any(Directive.extract(' # xdoctest: skip')) True >>> any(Directive.extract(' # badprefix: not-a-directive')) diff --git a/xdoctest/doctest_example.py b/xdoctest/doctest_example.py index 07fa13a8..015523ae 100644 --- a/xdoctest/doctest_example.py +++ b/xdoctest/doctest_example.py @@ -444,7 +444,22 @@ def _import_module(self): if self.module is None: if not self.modname.startswith('<'): # self.module = utils.import_module_from_path(self.modpath, index=0) - self.module = utils.import_module_from_path(self.modpath, index=-1) + try: + self.module = utils.import_module_from_path(self.modpath, index=-1) + except RuntimeError as ex: + msg_parts = [ + ('XDoctest failed to pre-import the module ' + 'containing the doctest.') + ] + msg_parts.append(str(ex)) + new_exc = RuntimeError('\n'.join(msg_parts)) + # new_exc = ex + # Remove traceback before this line + new_exc.__traceback__ = None + # Backwards syntax compatible raise exc from None + # https://www.python.org/dev/peps/pep-3134/#explicit-exception-chaining + new_exc.__cause__ = None + raise new_exc @staticmethod def _extract_future_flags(namespace): @@ -493,11 +508,8 @@ def run(self, verbose=None, on_error=None): self._parse() # parse out parts if we have not already done so self._pre_run(verbose) - self._import_module() # Prepare for actual test run - test_globals, compileflags = self._test_globals() - self.logged_evals.clear() self.logged_stdout.clear() self._unmatched_stdout = [] @@ -512,6 +524,19 @@ def run(self, verbose=None, on_error=None): # setup reporting choice runstate.set_report_style(self.config['reportchoice'].lower()) + try: + self._import_module() + except Exception: + self.failed_part = '' + self._partfilename = '' + self.exc_info = sys.exc_info() + if on_error == 'raise': + raise + else: + summary = self._post_run(verbose) + return summary + + test_globals, compileflags = self._test_globals() global_exec = self.config.getvalue('global_exec') if global_exec: # Hack to make it easier to specify multi-line input on the CLI @@ -785,6 +810,8 @@ def failed_line_offset(self): if self.exc_info is None: return None else: + if self.failed_part == '': + return 0 ex_type, ex_value, tb = self.exc_info offset = self.failed_part.line_offset if isinstance(ex_value, checker.ExtractGotReprException): @@ -1021,12 +1048,21 @@ def overwrite_lineno(linepart): new_tblines = [] for i, line in enumerate(tblines): - if 'xdoctest/xdoctest/doctest_example' in line: - # hack, remove ourselves from the tracback - continue - # new_tblines.append('!!!!!') - # raise Exception('foo') - # continue + # if '>> from xdoctest import core + >>> self = TempDoctest('>>> a = 1') + >>> doctests = list(core.parse_doctestables(self.modpath)) + >>> assert len(doctests) == 1 + """ + def __init__(self, module_text, modname=None): + if modname is None: + # make a random temporary module name + alphabet = list(map(chr, range(97, 97 + 26))) + modname = ''.join([random.choice(alphabet) for _ in range(8)]) + self.modname = modname + self.module_text = module_text + self.temp = TempDir() + self.dpath = self.temp.ensure() + self.modpath = join(self.dpath, self.modname + '.py') + with open(self.modpath, 'w') as file: + file.write(module_text) + + def _run_case(source, style='auto'): """ Runs all doctests in a source block From d85d12b657e568508b65f3f2e6449f22d5686e0a Mon Sep 17 00:00:00 2001 From: joncrall Date: Thu, 2 Sep 2021 14:14:53 -0400 Subject: [PATCH 6/7] Update docs. Add CHANGELOG entry --- CHANGELOG.md | 7 +++ docs/requirements.txt | 5 +- docs/source/conf.py | 102 +++++++++++++++++++++++++++++++++- xdoctest/checker.py | 2 +- xdoctest/doctest_example.py | 28 +++++++--- xdoctest/parser.py | 66 ++++++++++++---------- xdoctest/utils/util_import.py | 9 ++- xdoctest/utils/util_stream.py | 2 +- 8 files changed, 174 insertions(+), 47 deletions(-) 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. """ From 02e99a1f9b2a2ef2bd27a62b313613a41d58012d Mon Sep 17 00:00:00 2001 From: joncrall Date: Thu, 2 Sep 2021 14:59:43 -0400 Subject: [PATCH 7/7] Attempt to fix win32 test errors --- CHANGELOG.md | 2 ++ ...st_pytest_errors.py => test_pytest_cli.py} | 19 +++++++++-------- xdoctest/__main__.py | 2 +- xdoctest/doctest_example.py | 21 ++++++++++++------- xdoctest/plugin.py | 12 ++++++----- xdoctest/runner.py | 8 +++---- 6 files changed, 37 insertions(+), 27 deletions(-) rename testing/{test_pytest_errors.py => test_pytest_cli.py} (80%) diff --git a/CHANGELOG.md b/CHANGELOG.md index d44cad24..21a2d89d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm * 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. +* Pytest now defaults to `--xdoctest-verbose=2` by default (note this does + nothing unless `-s` is also given so pytest does not supress output) ### Fixed diff --git a/testing/test_pytest_errors.py b/testing/test_pytest_cli.py similarity index 80% rename from testing/test_pytest_errors.py rename to testing/test_pytest_cli.py index 85cf4bee..25c83636 100644 --- a/testing/test_pytest_errors.py +++ b/testing/test_pytest_cli.py @@ -1,4 +1,5 @@ from xdoctest.utils import util_misc +import sys from xdoctest import utils @@ -34,7 +35,7 @@ def module_func1(): temp_module = util_misc.TempModule(module_text) modpath = temp_module.modpath - info = cmd('pytest --xdoctest ' + modpath) + info = cmd(sys.executable + ' -m pytest --xdoctest ' + modpath) print(info['out']) assert info['ret'] == 0 @@ -44,11 +45,11 @@ def test_simple_pytest_import_error_cli(): This test case triggers an excessively long callback in xdoctest < dev/0.15.7 - xdoctest ~/code/xdoctest/testing/test_pytest_errors.py test_simple_pytest_import_error_cli + xdoctest ~/code/xdoctest/testing/test_pytest_cli.py test_simple_pytest_import_error_cli import sys, ubelt sys.path.append(ubelt.expandpath('~/code/xdoctest/testing')) - from test_pytest_errors import * # NOQA + from test_pytest_cli import * # NOQA """ module_text = utils.codeblock( ''' @@ -66,7 +67,7 @@ def module_func1(): """ ''') temp_module = util_misc.TempModule(module_text, modname='imperr_test_mod') - command = 'pytest -v -s --xdoctest-verbose=3 --xdoctest ' + temp_module.dpath + command = sys.executable + ' -m pytest -v -s --xdoctest-verbose=3 --xdoctest ' + temp_module.dpath print(command) info = cmd(command) print(info['out']) @@ -93,10 +94,10 @@ def module_func1(): """ ''') temp_module = util_misc.TempModule(module_text) - info = cmd('pytest --xdoctest ' + temp_module.dpath) + info = cmd(sys.executable + ' -m pytest --xdoctest ' + temp_module.dpath) print(info['out']) - info = cmd('pytest --xdoctest ' + temp_module.modpath) + info = cmd(sys.executable + ' -m pytest --xdoctest ' + temp_module.modpath) print(info['out']) @@ -111,7 +112,7 @@ def test_this(): print('hello world') ''') temp_module = util_misc.TempModule(module_text) - info = cmd('pytest ' + temp_module.modpath) + info = cmd(sys.executable + ' -m pytest ' + temp_module.modpath) print(info['out']) info = cmd('pytest ' + temp_module.dpath) @@ -130,9 +131,9 @@ def test_this(): print('hello world') ''') temp_module = util_misc.TempModule(module_text) - info = cmd('pytest ' + temp_module.modpath) + info = cmd(sys.executable + ' -m pytest ' + temp_module.modpath) print(info['out']) - info = cmd('pytest ' + temp_module.dpath) + info = cmd(sys.executable + ' -m pytest ' + temp_module.dpath) print(info['out']) # assert info['ret'] == 0 diff --git a/xdoctest/__main__.py b/xdoctest/__main__.py index 1a047414..dd3223f2 100644 --- a/xdoctest/__main__.py +++ b/xdoctest/__main__.py @@ -66,7 +66,7 @@ class RawDescriptionDefaultsHelpFormatter( Defaults --modname to arg.pop(0). Defaults --command to arg.pop(0). ''')) - parser.add_argument('--version', action='store_true', help='display version info and quit') + parser.add_argument('--version', action='store_true', help='Display version info and quit') # The bulk of the argparse CLI is defined in the doctest example from xdoctest import doctest_example diff --git a/xdoctest/doctest_example.py b/xdoctest/doctest_example.py index c2709458..66194b31 100644 --- a/xdoctest/doctest_example.py +++ b/xdoctest/doctest_example.py @@ -91,22 +91,27 @@ def str_lower(x): help=('Disable ANSI coloration in stdout'))), (['--offset'], dict(dest='offset_linenos', action='store_true', default=self['offset_linenos'], - help=('if True formatted source linenumbers will agree with ' + help=('If True formatted source linenumbers will agree with ' 'their location in the source file. Otherwise they ' 'will be relative to the doctest itself.'))), (['--report'], dict(dest='reportchoice', type=str_lower, choices=('none', 'cdiff', 'ndiff', 'udiff', 'only_first_failure',), default=self['reportchoice'], - help=('choose another output format for diffs on xdoctest failure'))), + help=('Choose another output format for diffs on xdoctest failure'))), # used to build default_runtime_state (['--options'], dict(type=str_lower, default=None, dest='options', - help='default directive flags for doctests')), + help='Default directive flags for doctests')), (['--global-exec'], dict(type=str, default=None, dest='global_exec', - help='exec these lines before every test')), - (['--verbose'], dict(type=int, default=defaults.get('verbose', 3), dest='verbose', - help='verbosity level')), - # (['--verbose'], dict(action='store_true', dest='verbose')), + help='Custom Python code to execute before every test')), + (['--verbose'], dict( + type=int, default=defaults.get('verbose', 3), dest='verbose', + help=( + 'Verbosity level. ' + '0 is silent, ' + '1 prints out test names, ' + '2 additionally prints test stdout, ' + '3 additionally prints test source'))), (['--quiet'], dict(action='store_true', dest='verbose', default=argparse.SUPPRESS, help='sets verbosity to 1')), @@ -303,7 +308,7 @@ def is_disabled(self, pytest=False): @property def unique_callname(self): """ - A key that references this doctest within xdoctest given its module + A key that references this doctest given its module """ return self.callname + ':' + str(self.num) diff --git a/xdoctest/plugin.py b/xdoctest/plugin.py index d637ed50..a9bc8118 100644 --- a/xdoctest/plugin.py +++ b/xdoctest/plugin.py @@ -75,12 +75,12 @@ def str_lower(x): # type="args", default=["+ELLIPSIS"]) group.addoption('--xdoctest-modules', '--xdoctest', '--xdoc', action='store_true', default=False, - help='run doctests in all .py modules using new style parsing', + help='Run doctests in all .py modules using new style parsing', dest='xdoctestmodules') group.addoption('--xdoctest-glob', '--xdoc-glob', action='append', default=[], metavar='pat', help=( - 'text files matching this pattern will be checked ' + 'Text files matching this pattern will be checked ' 'for doctests. This option may be specified multiple ' 'times. XDoctest does not check any text files by ' 'default. For compatibility with doctest set this to ' @@ -88,12 +88,12 @@ def str_lower(x): dest='xdoctestglob') group.addoption('--xdoctest-ignore-syntax-errors', action='store_true', default=False, - help='ignore xdoctest SyntaxErrors', + help='Ignore xdoctest SyntaxErrors', dest='xdoctest_ignore_syntax_errors') group.addoption('--xdoctest-style', '--xdoc-style', type=str_lower, default='freeform', - help='basic style used to write doctests', + help='Basic style used to write doctests', choices=core.DOCTEST_STYLES, dest='xdoctest_style') @@ -107,7 +107,7 @@ def str_lower(x): from xdoctest import doctest_example doctest_example.DoctestConfig()._update_argparse_cli( group.addoption, prefix=['xdoctest', 'xdoc'], - defaults=dict(verbose=0) + defaults=dict(verbose=2) ) @@ -207,7 +207,9 @@ def __getattr__(self, attr): ns = NamespaceLike(self.config) from xdoctest import doctest_example + print('ns = {!r}'.format(ns.__dict__['config'].__dict__)) self._examp_conf = doctest_example.DoctestConfig()._populate_from_cli(ns) + print('self._examp_conf = {!r}'.format(self._examp_conf)) class XDoctestTextfile(_XDoctestBase): diff --git a/xdoctest/runner.py b/xdoctest/runner.py index 8f02f794..bb03e62d 100644 --- a/xdoctest/runner.py +++ b/xdoctest/runner.py @@ -571,16 +571,16 @@ def str_lower(x): return str.lower(str(x)) add_argument(*('-m', '--modname'), type=str, - help='module name or path. If specified positional modules are ignored', + help='Module name or path. If specified positional modules are ignored', default=None) add_argument(*('-c', '--command'), type=str, - help='a doctest name or a command (list|all|). ' + help='A doctest name or a command (list|all|). ' 'Defaults to all', default=None) add_argument(*('--style',), type=str, - help='choose the style of doctests that will be parsed', + help='Choose the style of doctests that will be parsed', choices=['auto', 'google', 'freeform'], default='auto') add_argument(*('--analysis',), type=str, @@ -588,7 +588,7 @@ def str_lower(x): choices=['auto', 'static', 'dynamic'], default='static') add_argument(*('--durations',), type=int, - help=('specify execution times for slowest N tests.' + help=('Specify execution times for slowest N tests.' 'N=0 will show times for all tests'), default=None)