From d243b7fa019d27db694998c7c94d58023abde6fb Mon Sep 17 00:00:00 2001 From: joncrall Date: Fri, 19 Aug 2022 11:04:25 -0400 Subject: [PATCH] Defer pre-imports until a non-skipped line is about to --- CHANGELOG.md | 5 ++- src/xdoctest/doctest_example.py | 55 +++++++++++++++++++-------------- tests/test_preimport.py | 43 ++++++++++++++++++++++++++ tests/test_runner.py | 9 ++++-- 4 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 tests/test_preimport.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e3f94ef..09cfb61f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm skip. * Disabled traceback suppression on module import errors (this is is configurable via the "supress_import_errors" option). - +* Xdoctest will no longer try to pre-import the module if none of its doctests + have any enabled lines. This also means global-exec statements will NOT run + for those tests, which means you can no longer use global-exec to + force enabling tests. ## Version 1.0.1 - Released 2022-07-10 diff --git a/src/xdoctest/doctest_example.py b/src/xdoctest/doctest_example.py index 0fcbb055..f946c6dd 100644 --- a/src/xdoctest/doctest_example.py +++ b/src/xdoctest/doctest_example.py @@ -677,29 +677,9 @@ 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 - global_source = utils.codeblock(global_exec.replace('\\n', '\n')) - global_code = compile( - global_source, mode='exec', - filename='', - flags=compileflags, dont_inherit=True - ) - exec(global_code, test_globals) + # Defer the execution of the pre-import until we know at least one part + # in the doctest will run. + did_pre_import = False # Can't do this because we can't force execution of SCRIPTS # if self.is_disabled(): @@ -744,6 +724,35 @@ def run(self, verbose=None, on_error=None): self._skipped_parts.append(part) continue + if not did_pre_import: + # Execute the pre-import before the first run of + # non-skipped code. + 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 + global_source = utils.codeblock(global_exec.replace('\\n', '\n')) + global_code = compile( + global_source, mode='exec', + filename='', + flags=compileflags, dont_inherit=True + ) + exec(global_code, test_globals) + + did_pre_import = True + try: # Compile code, handle syntax errors # part.compile_mode can be single, exec, or eval. diff --git a/tests/test_preimport.py b/tests/test_preimport.py new file mode 100644 index 00000000..aacc9969 --- /dev/null +++ b/tests/test_preimport.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from os.path import join +from xdoctest import utils + + +def test_preimport_skiped_on_disabled_module(): + """ + If our module has no enabled tests, pre-import should never run. + """ + + from xdoctest import runner + import os + + source = utils.codeblock( + ''' + raise Exception("DONT IMPORT ME!") + + + def ima_function(): + """ + Example: + >>> # xdoctest: +REQUIRES(env:XDOCTEST_TEST_DOITANYWAY) + >>> print('hello') + """ + ''') + + with utils.TempDir() as temp: + dpath = temp.dpath + modpath = join(dpath, 'test_bad_preimport.py') + with open(modpath, 'w') as file: + file.write(source) + os.environ['XDOCTEST_TEST_DOITANYWAY'] = '' + with utils.CaptureStdout() as cap: + runner.doctest_module(modpath, 'all', argv=['']) + assert 'Failed to import modname' not in cap.text + assert '1 skipped' in cap.text + + os.environ['XDOCTEST_TEST_DOITANYWAY'] = '1' + with utils.CaptureStdout() as cap: + runner.doctest_module(modpath, 'all', argv=[]) + assert 'Failed to import modname' in cap.text + + del os.environ['XDOCTEST_TEST_DOITANYWAY'] diff --git a/tests/test_runner.py b/tests/test_runner.py index 70c754d7..b5fce96d 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -427,7 +427,9 @@ def test_hack_the_sys_argv(): """ Tests hacky solution to issue #76 - pytest tests/test_runner.py::test_global_exec -s + NOTE: in version 1.0.2 this hack no longer works! + + pytest tests/test_runner.py::test_hack_the_sys_argv -s References: https://github.com/Erotemic/xdoctest/issues/76 @@ -461,12 +463,13 @@ def foo(): with utils.CaptureStdout() as cap: runner.doctest_module(modpath, 'foo', argv=[''], config=config) - if NEEDS_FIX: + if 0 and NEEDS_FIX: # Fix the global state sys.argv.remove('--hackedflag') # print(cap.text) - assert '1 passed' in cap.text + assert '1 skipped' in cap.text + # assert '1 passed' in cap.text if __name__ == '__main__':