From af46067d60e5f7d2ae5d6257585866bc20570c30 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Sun, 19 Jun 2022 11:04:59 -0400 Subject: [PATCH 1/2] ci: have pytest capture log output at debug level --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 25e150a..b1a4977 100644 --- a/tox.ini +++ b/tox.ini @@ -6,9 +6,10 @@ envlist=py,linter,docs extras = test commands= - pytest \ + python -m pytest \ --cov=sphinxcontrib.spelling \ - --cov-report term-missing + --cov-report term-missing \ + --log-level DEBUG passenv= # Let the caller point to where libenchant is on their system, in # case it's in an unusual place. For example, on macOS with From 36e365848a3aec6d90fb5fa158d9fa4da969e778 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Sun, 19 Jun 2022 11:02:31 -0400 Subject: [PATCH 2/2] filters: trap exits from imported modules Fix the ImportableModuleFilter so that when importing a module results in a SystemExit being raised we trap the exit and treat it as an issue finding the module. Fixes #180 --- docs/source/history.rst | 8 ++++ sphinxcontrib/spelling/filters.py | 4 +- tests/test_filter.py | 62 ++++++++++++++++++++++++++++--- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/docs/source/history.rst b/docs/source/history.rst index d6c0dac..a921040 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -13,6 +13,14 @@ Next ==== +Bug Fixes +--------- + +- `#180 `__ + Suppress `SystemExit` errors in `ImportableModuleFilter` caused by + importing modules that run code on import and exit when that code + sees an error. Bug report and reproducer provided by Trevor Gross. + Features -------- diff --git a/sphinxcontrib/spelling/filters.py b/sphinxcontrib/spelling/filters.py index 3546776..e9265df 100644 --- a/sphinxcontrib/spelling/filters.py +++ b/sphinxcontrib/spelling/filters.py @@ -209,8 +209,8 @@ def _skip(self, word): self.sought_modules.add(word) try: mod = importlib.util.find_spec(word) - except Exception as err: - # This could be an ImportError, some more detailed + except BaseException as err: + # This could be an ImportError, SystemExit, some more detailed # error out of distutils, or something else triggered # by failing to be able to import a parent package to # use the metadata to search for a subpackage. diff --git a/tests/test_filter.py b/tests/test_filter.py index 71d54fb..bf76e64 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -4,13 +4,20 @@ """Tests for filters. """ +import contextlib +import logging import os +import sys from pathlib import Path import pytest from enchant.tokenize import get_tokenizer -from sphinxcontrib.spelling import filters +from sphinxcontrib.spelling import filters # isort:skip + +# Replace the sphinx logger with a normal one so pytest can collect +# the output. +filters.logger = logging.getLogger('test.filters') def test_builtin_unicode(): @@ -91,9 +98,54 @@ def test_importable_module_skip(word, expected): assert f._skip(word) is expected +@contextlib.contextmanager +def import_path(new_path): + "Temporarily change sys.path for imports." + before = sys.path + try: + sys.path = new_path + yield + finally: + sys.path = before + + def test_importable_module_with_side_effets(tmpdir): - with tmpdir.as_cwd(): - path = tmpdir.join('setup.py') - path.write('raise SystemExit\n') + logging.debug('tmpdir %r', tmpdir) + logging.debug('cwd %r', os.getcwd()) + + parentdir = tmpdir.join('parent') + parentdir.mkdir() + + parentdir.join('__init__.py').write( + 'raise SystemExit("exit as side-effect")\n' + ) + parentdir.join('child.py').write('') + + with import_path([str(tmpdir)] + sys.path): f = filters.ImportableModuleFilter(None) - assert f._skip('setup.cfg') is False + skip_parent = f._skip('parent') + skip_both = f._skip('parent.child') + + # The parent module name is valid because it is not imported, only + # discovered. + assert skip_parent is True + assert 'parent' in f.found_modules + + # The child module name is not valid because the parent is + # imported to find the child and that triggers the side-effect. + assert skip_both is False + assert 'parent.child' not in f.found_modules + + +def test_importable_module_with_system_exit(tmpdir): + path = tmpdir.join('mytestmodule.py') + path.write('raise SystemExit("exit as side-effect")\n') + + with import_path([str(tmpdir)] + sys.path): + f = filters.ImportableModuleFilter(None) + skip = f._skip('mytestmodule') + + # The filter does not actually import the module in this case, so + # it shows up as a valid word. + assert skip is True + assert 'mytestmodule' in f.found_modules