From b7a29b5cfff6da983c9c675a5752fb5e6b08a505 Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Thu, 16 Apr 2026 11:40:49 -0700 Subject: [PATCH 1/2] 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) --- ports/zephyr-cp/tests/conftest.py | 10 ++++++++++ ports/zephyr-cp/tests/test_basics.py | 22 ++++++++++++++++++++++ py/builtinimport.c | 21 ++++++++++++++++++++- 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/ports/zephyr-cp/tests/conftest.py b/ports/zephyr-cp/tests/conftest.py index 03451048324de..321bb8ae7735f 100644 --- a/ports/zephyr-cp/tests/conftest.py +++ b/ports/zephyr-cp/tests/conftest.py @@ -289,12 +289,22 @@ def circuitpython(request, board, sim_id, native_sim_binary, native_sim_env, tmp tmp_drive = tmp_path / f"drive{i}" tmp_drive.mkdir(exist_ok=True) + created_dirs: set[str] = set() for name, content in files.items(): src = tmp_drive / name + src.parent.mkdir(parents=True, exist_ok=True) if isinstance(content, bytes): src.write_bytes(content) else: src.write_text(content) + parent = Path(name).parent + if parent != Path("."): + parts = parent.parts + for depth in range(1, len(parts) + 1): + sub = "/".join(parts[:depth]) + if sub not in created_dirs: + subprocess.run(["mmd", "-i", str(flash), f"::{sub}"], check=True) + created_dirs.add(sub) subprocess.run(["mcopy", "-i", str(flash), str(src), f"::{name}"], check=True) trace_file = tmp_path / f"trace-{i}.perfetto" diff --git a/ports/zephyr-cp/tests/test_basics.py b/ports/zephyr-cp/tests/test_basics.py index 84b31849a8e81..7247f375f1524 100644 --- a/ports/zephyr-cp/tests/test_basics.py +++ b/ports/zephyr-cp/tests/test_basics.py @@ -92,6 +92,28 @@ def test_ctrl_c_interrupt(circuitpython): assert "completed" not in output +IMPORT_PRECEDENCE_CODE = """\ +import fake_lib +print("done") +""" + + +@pytest.mark.circuitpy_drive( + { + "code.py": IMPORT_PRECEDENCE_CODE, + "fake_lib/a_spritesheet.bmp": b"", + "fake_lib.py": 'print("hello fake_lib.py")\n', + } +) +def test_py_file_wins_over_namespace_dir(circuitpython): + """#10614: a .py module beats a sibling directory lacking __init__.py.""" + circuitpython.wait_until_done() + + output = circuitpython.serial.all_output + assert "hello fake_lib.py" in output + assert "done" in output + + RELOAD_CODE = """\ print("first run") import time diff --git a/py/builtinimport.c b/py/builtinimport.c index 8fcac22ccb491..5d77ac42859aa 100644 --- a/py/builtinimport.c +++ b/py/builtinimport.c @@ -105,7 +105,26 @@ static mp_import_stat_t stat_module(vstr_t *path) { mp_import_stat_t stat = stat_path(path); DEBUG_printf("stat %s: %d\n", vstr_str(path), stat); if (stat == MP_IMPORT_STAT_DIR) { - return stat; + // CIRCUITPY-CHANGE: match CPython import precedence. A regular + // package (directory with __init__.py/.mpy) takes precedence, then a + // sibling .py/.mpy module, and only then a namespace package + // (directory without __init__). See + // https://docs.python.org/3/reference/import.html#regular-packages + size_t orig_len = path->len; + vstr_add_str(path, PATH_SEP_CHAR "__init__.py"); + mp_import_stat_t init_stat = stat_file_py_or_mpy(path); + path->len = orig_len; + if (init_stat == MP_IMPORT_STAT_FILE) { + return MP_IMPORT_STAT_DIR; + } + + vstr_add_str(path, ".py"); + mp_import_stat_t file_stat = stat_file_py_or_mpy(path); + if (file_stat == MP_IMPORT_STAT_FILE) { + return file_stat; + } + path->len = orig_len; + return MP_IMPORT_STAT_DIR; } // Not a directory, add .py and try as a file. From 2fd4bce95c6f5a90f1374dc4c54be2abcb14d65e Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Mon, 20 Apr 2026 11:48:49 -0700 Subject: [PATCH 2/2] Switch to a micropython test --- ports/zephyr-cp/tests/conftest.py | 10 ---------- ports/zephyr-cp/tests/test_basics.py | 22 ---------------------- tests/import/import_shared_name.py | 7 +++++++ tests/import/shared_name.py | 1 + tests/import/shared_name/spritesheet.bmp | 0 5 files changed, 8 insertions(+), 32 deletions(-) create mode 100644 tests/import/import_shared_name.py create mode 100644 tests/import/shared_name.py create mode 100644 tests/import/shared_name/spritesheet.bmp diff --git a/ports/zephyr-cp/tests/conftest.py b/ports/zephyr-cp/tests/conftest.py index 321bb8ae7735f..03451048324de 100644 --- a/ports/zephyr-cp/tests/conftest.py +++ b/ports/zephyr-cp/tests/conftest.py @@ -289,22 +289,12 @@ def circuitpython(request, board, sim_id, native_sim_binary, native_sim_env, tmp tmp_drive = tmp_path / f"drive{i}" tmp_drive.mkdir(exist_ok=True) - created_dirs: set[str] = set() for name, content in files.items(): src = tmp_drive / name - src.parent.mkdir(parents=True, exist_ok=True) if isinstance(content, bytes): src.write_bytes(content) else: src.write_text(content) - parent = Path(name).parent - if parent != Path("."): - parts = parent.parts - for depth in range(1, len(parts) + 1): - sub = "/".join(parts[:depth]) - if sub not in created_dirs: - subprocess.run(["mmd", "-i", str(flash), f"::{sub}"], check=True) - created_dirs.add(sub) subprocess.run(["mcopy", "-i", str(flash), str(src), f"::{name}"], check=True) trace_file = tmp_path / f"trace-{i}.perfetto" diff --git a/ports/zephyr-cp/tests/test_basics.py b/ports/zephyr-cp/tests/test_basics.py index 7247f375f1524..84b31849a8e81 100644 --- a/ports/zephyr-cp/tests/test_basics.py +++ b/ports/zephyr-cp/tests/test_basics.py @@ -92,28 +92,6 @@ def test_ctrl_c_interrupt(circuitpython): assert "completed" not in output -IMPORT_PRECEDENCE_CODE = """\ -import fake_lib -print("done") -""" - - -@pytest.mark.circuitpy_drive( - { - "code.py": IMPORT_PRECEDENCE_CODE, - "fake_lib/a_spritesheet.bmp": b"", - "fake_lib.py": 'print("hello fake_lib.py")\n', - } -) -def test_py_file_wins_over_namespace_dir(circuitpython): - """#10614: a .py module beats a sibling directory lacking __init__.py.""" - circuitpython.wait_until_done() - - output = circuitpython.serial.all_output - assert "hello fake_lib.py" in output - assert "done" in output - - RELOAD_CODE = """\ print("first run") import time diff --git a/tests/import/import_shared_name.py b/tests/import/import_shared_name.py new file mode 100644 index 0000000000000..b76695f671e4c --- /dev/null +++ b/tests/import/import_shared_name.py @@ -0,0 +1,7 @@ +# https://github.com/adafruit/circuitpython/issues/10614 +# When a directory `shared_name/` (no __init__.py) and a module `shared_name.py` +# share a name, `import shared_name` must pick the .py module per PEP 420 +# precedence: regular package > module > namespace package. +import shared_name + +print("done") diff --git a/tests/import/shared_name.py b/tests/import/shared_name.py new file mode 100644 index 0000000000000..0ba98242ed9f0 --- /dev/null +++ b/tests/import/shared_name.py @@ -0,0 +1 @@ +print("hello shared_name.py") diff --git a/tests/import/shared_name/spritesheet.bmp b/tests/import/shared_name/spritesheet.bmp new file mode 100644 index 0000000000000..e69de29bb2d1d