Skip to content

Commit b7a29b5

Browse files
tannewtclaude
andcommitted
Import: .py module wins over namespace-package dir (#10614)
When both `foo/` (no __init__.py) and `foo.py` existed on the filesystem, `import foo` silently imported the empty namespace package and skipped `foo.py`. CPython picks the `.py` in this case, and the docs define that precedence: regular package > module > namespace package. stat_module now probes for `__init__.py`/`.mpy` inside the directory first, then falls back to `.py`/`.mpy`, and only treats the bare directory as a namespace package when neither exists. Adds a native_sim regression test and extends the test harness so `circuitpy_drive` can include files inside subdirectories. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b8d92f9 commit b7a29b5

File tree

3 files changed

+52
-1
lines changed

3 files changed

+52
-1
lines changed

ports/zephyr-cp/tests/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,12 +289,22 @@ def circuitpython(request, board, sim_id, native_sim_binary, native_sim_env, tmp
289289
tmp_drive = tmp_path / f"drive{i}"
290290
tmp_drive.mkdir(exist_ok=True)
291291

292+
created_dirs: set[str] = set()
292293
for name, content in files.items():
293294
src = tmp_drive / name
295+
src.parent.mkdir(parents=True, exist_ok=True)
294296
if isinstance(content, bytes):
295297
src.write_bytes(content)
296298
else:
297299
src.write_text(content)
300+
parent = Path(name).parent
301+
if parent != Path("."):
302+
parts = parent.parts
303+
for depth in range(1, len(parts) + 1):
304+
sub = "/".join(parts[:depth])
305+
if sub not in created_dirs:
306+
subprocess.run(["mmd", "-i", str(flash), f"::{sub}"], check=True)
307+
created_dirs.add(sub)
298308
subprocess.run(["mcopy", "-i", str(flash), str(src), f"::{name}"], check=True)
299309

300310
trace_file = tmp_path / f"trace-{i}.perfetto"

ports/zephyr-cp/tests/test_basics.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,28 @@ def test_ctrl_c_interrupt(circuitpython):
9292
assert "completed" not in output
9393

9494

95+
IMPORT_PRECEDENCE_CODE = """\
96+
import fake_lib
97+
print("done")
98+
"""
99+
100+
101+
@pytest.mark.circuitpy_drive(
102+
{
103+
"code.py": IMPORT_PRECEDENCE_CODE,
104+
"fake_lib/a_spritesheet.bmp": b"",
105+
"fake_lib.py": 'print("hello fake_lib.py")\n',
106+
}
107+
)
108+
def test_py_file_wins_over_namespace_dir(circuitpython):
109+
"""#10614: a .py module beats a sibling directory lacking __init__.py."""
110+
circuitpython.wait_until_done()
111+
112+
output = circuitpython.serial.all_output
113+
assert "hello fake_lib.py" in output
114+
assert "done" in output
115+
116+
95117
RELOAD_CODE = """\
96118
print("first run")
97119
import time

py/builtinimport.c

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,26 @@ static mp_import_stat_t stat_module(vstr_t *path) {
105105
mp_import_stat_t stat = stat_path(path);
106106
DEBUG_printf("stat %s: %d\n", vstr_str(path), stat);
107107
if (stat == MP_IMPORT_STAT_DIR) {
108-
return stat;
108+
// CIRCUITPY-CHANGE: match CPython import precedence. A regular
109+
// package (directory with __init__.py/.mpy) takes precedence, then a
110+
// sibling .py/.mpy module, and only then a namespace package
111+
// (directory without __init__). See
112+
// https://docs.python.org/3/reference/import.html#regular-packages
113+
size_t orig_len = path->len;
114+
vstr_add_str(path, PATH_SEP_CHAR "__init__.py");
115+
mp_import_stat_t init_stat = stat_file_py_or_mpy(path);
116+
path->len = orig_len;
117+
if (init_stat == MP_IMPORT_STAT_FILE) {
118+
return MP_IMPORT_STAT_DIR;
119+
}
120+
121+
vstr_add_str(path, ".py");
122+
mp_import_stat_t file_stat = stat_file_py_or_mpy(path);
123+
if (file_stat == MP_IMPORT_STAT_FILE) {
124+
return file_stat;
125+
}
126+
path->len = orig_len;
127+
return MP_IMPORT_STAT_DIR;
109128
}
110129

111130
// Not a directory, add .py and try as a file.

0 commit comments

Comments
 (0)