Skip to content
Open
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: 17 additions & 1 deletion blacksheep/utils/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ def get_parent_file():
return ""


def _get_dotted_path(root_path: Path) -> str:
"""
Builds the dotted module path for a package folder by walking up its
parent directories while they contain an `__init__.py` file, so the
package can be imported by its fully qualified name regardless of the
current working directory of the running process.
"""
root_path = root_path.resolve()
parts = [root_path.name]
current = root_path.parent
while (current / "__init__.py").is_file():
parts.append(current.name)
current = current.parent
return ".".join(reversed(parts))


def import_child_modules(root_path: Path):
"""
Import automatically all modules defined
Expand All @@ -26,7 +42,7 @@ def import_child_modules(root_path: Path):
for f in glob.glob(path + "/*.py")
if not os.path.basename(f).startswith("_")
]
stripped_path = os.path.relpath(path).replace("/", ".").replace("\\", ".")
stripped_path = _get_dotted_path(root_path)
for module in modules:
__import__(stripped_path + "." + module)

Expand Down
37 changes: 37 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import sys
from typing import AnyStr, Sequence

import pytest

from blacksheep.utils import ensure_bytes, ensure_str, join_fragments
from blacksheep.utils.meta import import_child_modules


@pytest.mark.parametrize(
Expand Down Expand Up @@ -45,3 +47,38 @@ def test_ensure_bytes_throws_for_invalid_value():
def test_ensure_str_throws_for_invalid_value():
with pytest.raises(ValueError):
ensure_str(True) # type: ignore


def test_import_child_modules_independent_of_cwd(tmp_path, monkeypatch):
"""
import_child_modules must resolve the dotted module path of a package
folder from its own location on disk, not from the current working
directory of the process. This reproduces the crash reported when a
blacksheep app is installed as a package (e.g. with `uv tool install`)
and started from an unrelated working directory: os.getcwd()-based
relative paths produced bogus dotted paths such as ".local.share...",
raising ModuleNotFoundError.
"""
package_root = tmp_path / "myapp_pkg"
package = package_root / "myapp"
routes_folder = package / "routes"
routes_folder.mkdir(parents=True)

(package / "__init__.py").write_text("")
(routes_folder / "__init__.py").write_text("")
(routes_folder / "home.py").write_text("imported = True\n")

unrelated_cwd = tmp_path / "somewhere_else"
unrelated_cwd.mkdir()

monkeypatch.chdir(unrelated_cwd)
monkeypatch.syspath_prepend(str(package_root))

try:
import_child_modules(routes_folder)
assert "myapp.routes.home" in sys.modules
assert sys.modules["myapp.routes.home"].imported is True
finally:
for name in list(sys.modules):
if name == "myapp" or name.startswith("myapp."):
del sys.modules[name]