Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions bpython/importcompletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,24 @@ def complete(self, cursor_offset: int, line: str) -> set[str] | None:
else:
return None

def _is_skipped(self, path: Path) -> bool:
for pattern in self.skiplist:
if Path(pattern).is_absolute():
if any(
fnmatch.fnmatch(str(parent), pattern)
for parent in (path, *path.parents)
):
return True
elif fnmatch.fnmatch(path.name, pattern):
return True
return False

def find_modules(self, path: Path) -> Generator[str | None, None, None]:
"""Find all modules (and packages) for a given directory."""
if not path.is_dir():
# Perhaps a zip file
return
if any(fnmatch.fnmatch(path.name, entry) for entry in self.skiplist):
if self._is_skipped(path):
# Path is on skiplist
return

Expand All @@ -177,9 +189,7 @@ def find_modules(self, path: Path) -> Generator[str | None, None, None]:
if p.name.startswith(".") or p.name == "__pycache__":
# Impossible to import from names starting with . and we can skip __pycache__
continue
elif any(
fnmatch.fnmatch(p.name, entry) for entry in self.skiplist
):
elif self._is_skipped(p):
# Path is on skiplist
continue
elif not any(p.name.endswith(suffix) for suffix in SUFFIXES):
Expand Down
34 changes: 34 additions & 0 deletions bpython/test/test_importcompletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,40 @@ def test_simple_symbolic_link_loop(self):
self.assertFalse(filepaths)


class TestSkiplist(unittest.TestCase):
def test_absolute_paths(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory).resolve()
for name in ("first", "second"):
(root / name).mkdir()
(root / name / "__init__.py").touch()
(root / name / "inside.py").touch()

cases = (
([root], str(root), set()),
([root], str(root / "first"), {"second", "second.inside"}),
([root / "first"], str(root), set()),
)
for paths, skip, expected in cases:
with self.subTest(paths=paths, skip=skip):
gatherer = ModuleGatherer(paths, [skip])
while gatherer.find_coroutine():
pass
self.assertEqual(gatherer.modules, expected)

def test_relative_name_pattern_remains_supported(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory).resolve()
for name in ("first", "second"):
(root / name).mkdir()
(root / name / "__init__.py").touch()
(root / name / "inside.py").touch()
gatherer = ModuleGatherer([root], ["f*"])
while gatherer.find_coroutine():
pass
self.assertEqual(gatherer.modules, {"second", "second.inside"})


class TestBugReports(unittest.TestCase):
def test_issue_847(self):
with tempfile.TemporaryDirectory() as import_test_folder:
Expand Down
8 changes: 7 additions & 1 deletion doc/sphinx/source/configuration-options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,13 @@ Whether to use Unicode characters to draw boxes (default: True).

import_completion_skiplist
^^^^^^^^^^^^^^^^^^^^^^^^^^
A `:`-seperated list of patterns to skip when processing modules for import completion.
A ``:``-separated list of patterns to skip when processing modules for import
completion. A name pattern such as ``node_modules`` or ``build*`` matches names
at any level. On POSIX systems, an absolute path such as ``/home/user/projects``
skips that directory and its descendants, including when a descendant is itself
on the module search path. Paths are matched against resolved filesystem paths.
The colon separator means that Windows drive-letter paths cannot be specified
in this option.

.. versionadded:: 0.21

Expand Down
Loading