Skip to content

Commit

Permalink
Fix discovery of modules in namespace packages
Browse files Browse the repository at this point in the history
Instead of relying on `__init__.py` files, stop at the first parent
directory that is in `sys.path`. This gives the shortest module name
under which the file can really be imported. (Unless there are name
conflicts in `sys.path`, which is arguably a misconfiguration; this
is caught by the location check in `module_tree`.)
  • Loading branch information
eltoder committed Mar 21, 2024
1 parent 8f0d153 commit 8a58ef1
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 16 deletions.
15 changes: 10 additions & 5 deletions src/slotscheck/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import importlib
import pkgutil
import sys
from dataclasses import dataclass, field, replace
from functools import partial, reduce
from importlib.util import find_spec
Expand Down Expand Up @@ -299,15 +300,19 @@ def _is_package(p: AbsPath) -> bool:
return p.is_dir() and (p / _INIT_PY).is_file()


def find_modules(p: AbsPath) -> Iterable[ModuleLocated]:
def find_modules(
p: AbsPath, python_path: Optional[FrozenSet[AbsPath]] = None
) -> Iterable[ModuleLocated]:
"Recursively find modules at given path. Nonexistent Path is ignored"
if python_path is None:
python_path = frozenset(map(Path, sys.path))
if p.name == _INIT_PY:
yield from find_modules(p.parent)
yield from find_modules(p.parent, python_path)
elif _is_module(p):
parents = [p] + list(takewhile(_is_package, p.parents))
parents = [p, *takewhile(lambda p: p not in python_path, p.parents)]
yield ModuleLocated(
".".join(p.stem for p in reversed(parents)),
(p / "__init__.py" if _is_package(p) else p),
(p / _INIT_PY if _is_package(p) else p),
)
elif p.is_dir():
yield from flatten(map(find_modules, p.iterdir()))
yield from flatten(find_modules(sp, python_path) for sp in p.iterdir())
4 changes: 2 additions & 2 deletions tests/src/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
@pytest.fixture(scope="session", autouse=True)
def add_pypath() -> Iterator[None]:
"Add example modules to the python path"
sys.path.insert(0, str(EXAMPLES_DIR))
sys.path[:0] = [str(EXAMPLES_DIR), str(EXAMPLES_DIR / "other")]
yield
sys.path.remove(str(EXAMPLES_DIR))
del sys.path[:2]


@pytest.fixture(autouse=True)
Expand Down
24 changes: 15 additions & 9 deletions tests/src/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,32 +370,34 @@ def test_given_nonpython_file(self):
def test_given_python_file(self):
location = EXAMPLES_DIR / "files/subdir/myfile.py"
result = list(find_modules(location))
assert result == [ModuleLocated("myfile", location)]
assert result == [ModuleLocated("files.subdir.myfile", location)]

def test_given_python_root_module(self):
location = EXAMPLES_DIR / "files/subdir/some_module/"
result = list(find_modules(location))
assert result == [
ModuleLocated("some_module", location / "__init__.py")
ModuleLocated("files.subdir.some_module", location / "__init__.py")
]

def test_given_dir_containing_python_files(self):
location = EXAMPLES_DIR / "files/my_scripts/"
result = list(find_modules(location))
assert len(result) == 4
assert set(result) == {
ModuleLocated("bla", location / "bla.py"),
ModuleLocated("foo", location / "foo.py"),
ModuleLocated("foo", location / "sub/foo.py"),
ModuleLocated("mymodule", location / "mymodule/__init__.py"),
ModuleLocated("files.my_scripts.bla", location / "bla.py"),
ModuleLocated("files.my_scripts.foo", location / "foo.py"),
ModuleLocated("files.my_scripts.sub.foo", location / "sub/foo.py"),
ModuleLocated(
"files.my_scripts.mymodule", location / "mymodule/__init__.py"
),
}

def test_given_file_within_module(self):
location = EXAMPLES_DIR / "files/subdir/some_module/sub/foo.py"
result = list(find_modules(location))
assert result == [
ModuleLocated(
"some_module.sub.foo",
"files.subdir.some_module.sub.foo",
EXAMPLES_DIR / "files/subdir/some_module/sub/foo.py",
)
]
Expand All @@ -404,13 +406,17 @@ def test_given_submodule(self):
location = EXAMPLES_DIR / "files/subdir/some_module/sub"
result = list(find_modules(location))
assert result == [
ModuleLocated("some_module.sub", location / "__init__.py")
ModuleLocated(
"files.subdir.some_module.sub", location / "__init__.py"
)
]

def test_given_init_py(self):
location = EXAMPLES_DIR / "files/subdir/some_module/sub/__init__.py"
result = list(find_modules(location))
assert result == [ModuleLocated("some_module.sub", location)]
assert result == [
ModuleLocated("files.subdir.some_module.sub", location)
]


class TestConsolidate:
Expand Down

0 comments on commit 8a58ef1

Please sign in to comment.