diff --git a/backend/fluksio/sdk/cli.py b/backend/fluksio/sdk/cli.py
index 66b22b1..4b25d85 100644
--- a/backend/fluksio/sdk/cli.py
+++ b/backend/fluksio/sdk/cli.py
@@ -136,8 +136,8 @@ def _import(root: str, dotted: str, expect: Path | None = None) -> None:
f" {actual}\n {expect}\n"
"Python keeps one module per name, and a node's generated body "
"imports by that name, so the second would run the first's code. "
- "Put an `__init__.py` in each directory — they become "
- f"'
.{dotted}' and stop colliding — or rename one of the files."
+ "Sync the directory they are both under — each then imports as "
+ "'.' — or rename one of the files."
)
@@ -211,7 +211,14 @@ def discover(targets: list[str], keep_going: bool = False) -> list[Flow]:
if entry.is_dir():
_import_package(entry)
else:
- _import(*_module_of(entry), entry)
+ # Named for where it sits under the directory being synced,
+ # so `dev/s1/study.py` imports as `s1.study` and one
+ # `study.py` per study collides with nothing. No
+ # `__init__.py` needed: the directories in between are
+ # namespace packages. A file at the root keeps its bare
+ # name, which is what it has always had.
+ dotted = ".".join(entry.relative_to(path).with_suffix("").parts)
+ _import(str(path), dotted, entry)
except SyncError:
# A name collision is never somebody else's problem: it would
# put the wrong file behind a node.
diff --git a/backend/tests/test_cli.py b/backend/tests/test_cli.py
index ce9e923..8ef1eaa 100644
--- a/backend/tests/test_cli.py
+++ b/backend/tests/test_cli.py
@@ -472,8 +472,38 @@ def test_a_study_in_a_subfolder_is_found(tmp_path) -> None:
}
+def test_a_study_per_directory_imports_under_its_own_name(tmp_path) -> None:
+ """One `study.py` per folder is a layout people have, and it works.
+
+ Named for where each sits under the directory being synced, so nothing
+ collides and no `__init__.py` has to be added — which would break the
+ bare `from study import ...` a test beside it does.
+ """
+ import sys
+
+ from fluksio.sdk.cli import discover
+
+ for study in ("s1", "s2"):
+ (tmp_path / "dev" / study).mkdir(parents=True)
+ (tmp_path / "dev" / study / "study.py").write_text(f"VALUE = {study!r}\n")
+
+ try:
+ discover([str(tmp_path / "dev")])
+ assert sys.modules["s1.study"].VALUE == "s1"
+ assert sys.modules["s2.study"].VALUE == "s2"
+ finally:
+ for name in ("s1.study", "s2.study", "s1", "s2"):
+ sys.modules.pop(name, None)
+ sys.path[:] = [entry for entry in sys.path if entry != str(tmp_path / "dev")]
+
+
def test_two_files_of_one_name_are_refused(tmp_path) -> None:
- """Python keeps one module per name, and a node's body imports by it."""
+ """Python keeps one module per name, and a node's body imports by it.
+
+ Unreachable from one sync of a directory now that a file is named for
+ where it sits; this is the spelling that still gets there — two files
+ named on the command line, each rooted at its own directory.
+ """
import pytest
from fluksio.sdk import SyncError