Quarterly cold-eyes review, 2026-09-03 (PyDevices/.github#25, standards S1/S4/S6). Read-only inspection by a repo-inspector agent from a fresh clone in a scratch directory; every claim below carries the command or URL that produced it. Filed by Arthur. Nothing here was changed by the review itself.
S1 (clean clone builds per its README): PASS with a failing final step (the build itself passes; the verification command the README hands you next fails).
S4 (README to working example, first try): PASS — first try, no deviation from the README.
Findings (9)
[HIGH] The README's MicroPython build-verification command fails: tests/_env.py uses CPython-only os.environ
README.md:160 tells the reader, immediately after the unix build, to run:
./micropython/ports/unix/build-standard/micropython pygraphics/tests/test_area.py
Against the interpreter that same recipe just built (upstream master, 1.30.0-preview, pygraphics linked in and reporting native_cmod):
Traceback (most recent call last):
File "pygraphics/tests/test_area.py", line 11, in <module>
File "/.../pygraphics/tests/_env.py", line 25, in <module>
AttributeError: module 'os' has no attribute 'environ'
rc=1
tests/_env.py:25 is USE_NATIVE = os.environ.get("PYGRAPHICS_TEST_NATIVE") == "1", and :27 uses os.path.abspath. MicroPython has neither. tests/test_area.py:3 still calls itself "Area tests for MicroPython and CPython", and the test body is in fact MicroPython-clean: a scratch copy of tests/ with os.environ.get swapped for os.getenv runs test_area: ok and test_subclass: ok under the same binary. So the harness added for CI (.github/workflows/tests.yml, tests/_env.py is the implementation switch) silently took the dual-runtime tests away from MicroPython, and the README was never updated. This is the last step of S1 and the only thing that tells a stranger the cold build worked.
Suggested fix: Use os.getenv(...) and build the lib/ path from __file__ with string ops (or wrap the CPython-only calls in try/except ImportError-style guards) in tests/_env.py, so the modules the README names run on both runtimes; add a MicroPython leg to tests.yml running test_area/test_subclass under a unix-port build so the regression cannot recur silently.
[HIGH] "Same public API" is false: the native module lacks export_framebuffer and three FrameBuffer methods, and the parity test cannot detect it
Claims: README.md:11 "(same public API)"; README.md:45 "preserving full API compatibility"; README.md:54 advertises export_framebuffer by name; README.md:76-77 and docs/installation.md:18 "the public API is identical"/"identical either way". docs/graphics-files.md:18-20 documents FrameBuffer.export / export_framebuffer, from_bitmap, from_module under the heading "Built into pygraphics" with no native/pure caveat.
Measured against the pure package's own __all__ (44 names):
$ .venv/bin/python -c "...hasattr over pure __all__..."
declared in pure __all__ but ABSENT from native module: ['export_framebuffer']
native FrameBuffer lacks: ['export', 'from_bitmap', 'from_module']
Same result on the MicroPython native build: module export_framebuffer False, FrameBuffer export False, from_bitmap False, from_module False (while load_image, save_image, from_file, save are all True). So on the build the README tells desktop and embedded users to prefer, four documented entry points do not exist, and nothing anywhere records the exclusion.
The guard that exists cannot fire: tests/test_parity.py:3 says "Verify graphics cmod exports match pure-Python pygraphics.all" but hardcodes a 44-name ALL list instead of importing it. Diffed:
in pure __all__ but NOT in test_parity.ALL: ['RGB888', 'export_framebuffer']
in test_parity.ALL but NOT in pure __all__: ['capabilities', 'framebuf_backend']
It already disagrees with __all__ in both directions, and the two names it drops include a headline README feature (RGB888, README.md:50) and the exact missing one.
Suggested fix: Either implement the four entry points in the C module or state the exclusion with its cause where a stranger reads it (README feature bullet + docs/graphics-files.md row: "pure-Python only — the native module writes no .py modules"), and make tests/test_parity.py import the pure package's __all__ rather than carry a frozen copy so drift fails CI.
[MEDIUM] The parity tool the README names for MicroPython has never been runnable on MicroPython
README.md:139-143 gives three parity commands, the first of which is the repo's central native-vs-pure proof:
micropython tools/compare_graphics_run.py # single interpreter
With the stock micropython built by the README's own recipe:
$ .../build-standard/micropython tools/compare_graphics_run.py
File "tools/compare_graphics.py", line 29, in <module>
ImportError: no module named 'tempfile'
tools/compare_graphics.py:29 is import tempfile at module top (used only at :270 as a fallback after TEMP/TMPDIR/TMP). git log -S"import tempfile" -- tools/compare_graphics.py returns a single commit, 55d0a6a "Add native vs pure-Python parity compare tools" — it has been there since the tool landed, so this demonstration has never run on MicroPython or CircuitPython. python tools/compare_graphics_matrix.py prints the same failure in its summary for micropython, micropython.exe and circuitpython; only cpython-venv reports "385 checks ok". (The third command, micropython tools/compare_framebuf_mp.py, does pass.) No CI job runs any of the three — .github/workflows/tests.yml runs unittest only.
Suggested fix: Drop the unconditional import tempfile (guard it, or rely on TEMP/TMPDIR/TMP with a hardcoded '/tmp' fallback) so the runner imports on MicroPython, and either wire compare_framebuf_mp.py/compare_graphics_run.py into CI against a unix-port build or say in the README which interpreters each command is actually proven on.
[MEDIUM] README says native wheels are "TestPyPI only ... deliberate, not a placeholder" — but production PyPI serves pydevices-pygraphics 0.0.37
README.md:79-81: "Native wheels are published to TestPyPI only (pydevices-pygraphics); this is also deliberate, not a placeholder". docs/installation.md:15-16 repeats it.
$ curl -s https://pypi.org/pypi/pydevices-pygraphics/json | ...
prod version: 0.0.37
summary: Native graphics cmod for CPython (FrameBuffer, Area); import as pygraphics
files: pydevices_pygraphics-0.0.37-cp310...manylinux_2_28_x86_64.whl, ...win_amd64.whl, ...
upload_time: 2026-08-30T07:55:07
$ .venv/bin/pip download --no-deps -d ... pydevices-pygraphics
Saved .../pydevices_pygraphics-0.0.37-cp312-cp312-manylinux...whl
So the natural command a stranger types installs a real, working, one-release-stale wheel from a location the README says does not exist. TestPyPI is at 0.0.38 (matching VERSION). This is the name-park decision (a ledger item) outrunning its documentation, not a broken release — but as written the repo contradicts observable reality, and the README's own install line (TestPyPI -i plus PyPI --extra-index-url) resolves to whichever index has the higher version, so the day PyPI leads, the documented command changes meaning silently.
Suggested fix: Update README.md:79-81 and docs/installation.md:15-16 to say what is true — the name is registered on PyPI and currently carries 0.0.37; current releases go to TestPyPI — and say which one a user should install and why.
[MEDIUM] Platform table carries no proof tiers, and the macOS/aarch64 exclusion states a label instead of a cause
README.md:65-72 lists manylinux x86_64, Windows AMD64, Android arm64_v8a + x86_64, and Pyodide/Emscripten wasm32 with no tier label; docs/installation.md:5-13 repeats the matrix, also unlabeled. platform-support-tiers.md requires every platform claim in a PyDevices README to carry bench-proven / CI-proven / community-verified. Android and Pyodide in particular are shipped wheels with no statement of whether anyone has run them anywhere. Separately, README.md:74-77 records the macOS/Linux-aarch64 exclusion as "not built yet — deliberately, not as an oversight" — per the completeness charter "deliberately" is a label, not a cause; the honest cause exists org-side (platform-support-tiers.md: "No PyDevices claim assumes a Mac on our bench, because there isn't one") but the README neither states nor links it.
Suggested fix: Add a tier column to both platform tables (manylinux/Windows bench- or CI-proven per what actually runs; Android and Pyodide labeled honestly), and rewrite the macOS/aarch64 sentence to give the cause and link platform-support-tiers.md.
[LOW] Neither build recipe declares a supported upstream version for MicroPython or CircuitPython
README.md:147-161 says "Clone as a sibling of micropython/" and gives no tag, branch, or supported range; README.md:204-210 and docs/installation.md:41-45 do the same for CircuitPython. grep -rn "1\.2[0-9]|pinned|v1\.28|MICROPY_VERSION|tag" docs/*.md README.md AGENTS.md pygraphics.toml returns only README.md:13 (about release tags). apply_cp_patches.sh names no CircuitPython version either (no "10.2" anywhere in the repo). S1's criterion is "declared, pinned dependencies". Today this is not broken — I built against upstream master (5d592bd, 1.30.0-preview) and it linked and ran clean — but a stranger has no way to know what the repo is tested against, and CircuitPython (where the integration is a patch script against a moving tree) is the fragile one.
Suggested fix: State the tested upstream revisions next to each recipe (e.g. "tested against MicroPython v1.28.0 and current master; CircuitPython 10.2.1") and have apply_cp_patches.sh --status warn when the CircuitPython tree is not the declared revision.
[LOW] Pyodide wheel tag claim is right for one interpreter and wrong for the other
README.md:72 and docs/installation.md:11 name the wasm wheel pyemscripten_2026_0_wasm32; docs/installation.md:60 goes further — "Each TestPyPI release includes a pyemscripten_2026_0_wasm32 wheel". The 0.0.38 files on TestPyPI are ...-cp313-cp313-pyemscripten_2025_0_wasm32.whl and ...-cp314-cp314-pyemscripten_2026_0_wasm32.whl — the cp313 wheel carries the 2025 ABI tag. A Pyodide user matching the documented tag against their runtime is told the wrong thing half the time.
Suggested fix: Say "pyemscripten 2025_0 (cp313) and 2026_0 (cp314)", or drop the tag and say micropip selects the matching wheel.
[LOW] Workspace residue in the first test the README tells a stranger to run
tests/test_area.py:7-9, above every import:
for _p in list(sys.path):
if _p.endswith("/graphics") and "repos" in _p:
sys.path.remove(_p)
This strips sys.path entries matching a private layout (a .../repos/.../graphics checkout — the pre-rename import name, per docs/installation.md:70-75). It is dead code anywhere but that machine, it appears in exactly the file README.md:117 and README.md:160 point a newcomer at, and it is the kind of thing the rigor lens reads as "works in the author's workspace".
Suggested fix: Delete the loop; if some ordering hazard is real, express it in tests/_env.py where the sys.path policy already lives, with a comment saying what it defends against.
[LOW] The README's test recipe runs 3 of the repo's 15 test modules, and never points at the real command
README.md:117-119 lists exactly tests/test_area.py, tests/test_pygraphics.py, tests/test_subclass.py. The suite is 15 modules; CI runs python -m unittest discover -s tests twice (.github/workflows/tests.yml:41,52) — 98 tests pure, 85 native. tests/README.md has the correct invocations for both implementations, and the top-level README never links to it. A reviewer running the README's three files sees "ok" three times and has exercised a small fraction of what the project actually validates.
Suggested fix: Replace the three lines with python -m unittest discover -s tests and the PYGRAPHICS_TEST_NATIVE=1 variant, and link tests/README.md.
Each finding is independent; tick them off here or split any that deserves its own thread.
S1 (clean clone builds per its README): PASS with a failing final step (the build itself passes; the verification command the README hands you next fails).
S4 (README to working example, first try): PASS — first try, no deviation from the README.
Findings (9)
[HIGH] The README's MicroPython build-verification command fails: tests/_env.py uses CPython-only os.environ
README.md:160 tells the reader, immediately after the unix build, to run:
Against the interpreter that same recipe just built (upstream master, 1.30.0-preview, pygraphics linked in and reporting
native_cmod):tests/_env.py:25 is
USE_NATIVE = os.environ.get("PYGRAPHICS_TEST_NATIVE") == "1", and :27 usesos.path.abspath. MicroPython has neither. tests/test_area.py:3 still calls itself "Area tests for MicroPython and CPython", and the test body is in fact MicroPython-clean: a scratch copy of tests/ withos.environ.getswapped foros.getenvrunstest_area: okandtest_subclass: okunder the same binary. So the harness added for CI (.github/workflows/tests.yml,tests/_env.py is the implementation switch) silently took the dual-runtime tests away from MicroPython, and the README was never updated. This is the last step of S1 and the only thing that tells a stranger the cold build worked.Suggested fix: Use
os.getenv(...)and build the lib/ path from__file__with string ops (or wrap the CPython-only calls in try/except ImportError-style guards) in tests/_env.py, so the modules the README names run on both runtimes; add a MicroPython leg to tests.yml running test_area/test_subclass under a unix-port build so the regression cannot recur silently.[HIGH] "Same public API" is false: the native module lacks export_framebuffer and three FrameBuffer methods, and the parity test cannot detect it
Claims: README.md:11 "(same public API)"; README.md:45 "preserving full API compatibility"; README.md:54 advertises
export_framebufferby name; README.md:76-77 and docs/installation.md:18 "the public API is identical"/"identical either way". docs/graphics-files.md:18-20 documentsFrameBuffer.export/export_framebuffer,from_bitmap,from_moduleunder the heading "Built intopygraphics" with no native/pure caveat.Measured against the pure package's own
__all__(44 names):Same result on the MicroPython native build:
module export_framebuffer False,FrameBuffer export False,from_bitmap False,from_module False(whileload_image,save_image,from_file,saveare all True). So on the build the README tells desktop and embedded users to prefer, four documented entry points do not exist, and nothing anywhere records the exclusion.The guard that exists cannot fire: tests/test_parity.py:3 says "Verify graphics cmod exports match pure-Python pygraphics.all" but hardcodes a 44-name
ALLlist instead of importing it. Diffed:It already disagrees with
__all__in both directions, and the two names it drops include a headline README feature (RGB888, README.md:50) and the exact missing one.Suggested fix: Either implement the four entry points in the C module or state the exclusion with its cause where a stranger reads it (README feature bullet + docs/graphics-files.md row: "pure-Python only — the native module writes no .py modules"), and make tests/test_parity.py import the pure package's
__all__rather than carry a frozen copy so drift fails CI.[MEDIUM] The parity tool the README names for MicroPython has never been runnable on MicroPython
README.md:139-143 gives three parity commands, the first of which is the repo's central native-vs-pure proof:
With the stock micropython built by the README's own recipe:
tools/compare_graphics.py:29 is
import tempfileat module top (used only at :270 as a fallback after TEMP/TMPDIR/TMP).git log -S"import tempfile" -- tools/compare_graphics.pyreturns a single commit, 55d0a6a "Add native vs pure-Python parity compare tools" — it has been there since the tool landed, so this demonstration has never run on MicroPython or CircuitPython.python tools/compare_graphics_matrix.pyprints the same failure in its summary formicropython,micropython.exeandcircuitpython; onlycpython-venvreports "385 checks ok". (The third command,micropython tools/compare_framebuf_mp.py, does pass.) No CI job runs any of the three — .github/workflows/tests.yml runs unittest only.Suggested fix: Drop the unconditional
import tempfile(guard it, or rely on TEMP/TMPDIR/TMP with a hardcoded '/tmp' fallback) so the runner imports on MicroPython, and either wire compare_framebuf_mp.py/compare_graphics_run.py into CI against a unix-port build or say in the README which interpreters each command is actually proven on.[MEDIUM] README says native wheels are "TestPyPI only ... deliberate, not a placeholder" — but production PyPI serves pydevices-pygraphics 0.0.37
README.md:79-81: "Native wheels are published to TestPyPI only (
pydevices-pygraphics); this is also deliberate, not a placeholder". docs/installation.md:15-16 repeats it.So the natural command a stranger types installs a real, working, one-release-stale wheel from a location the README says does not exist. TestPyPI is at 0.0.38 (matching VERSION). This is the name-park decision (a ledger item) outrunning its documentation, not a broken release — but as written the repo contradicts observable reality, and the README's own install line (TestPyPI
-iplus PyPI--extra-index-url) resolves to whichever index has the higher version, so the day PyPI leads, the documented command changes meaning silently.Suggested fix: Update README.md:79-81 and docs/installation.md:15-16 to say what is true — the name is registered on PyPI and currently carries 0.0.37; current releases go to TestPyPI — and say which one a user should install and why.
[MEDIUM] Platform table carries no proof tiers, and the macOS/aarch64 exclusion states a label instead of a cause
README.md:65-72 lists manylinux x86_64, Windows AMD64, Android arm64_v8a + x86_64, and Pyodide/Emscripten wasm32 with no tier label; docs/installation.md:5-13 repeats the matrix, also unlabeled.
platform-support-tiers.mdrequires every platform claim in a PyDevices README to carry bench-proven / CI-proven / community-verified. Android and Pyodide in particular are shipped wheels with no statement of whether anyone has run them anywhere. Separately, README.md:74-77 records the macOS/Linux-aarch64 exclusion as "not built yet — deliberately, not as an oversight" — per the completeness charter "deliberately" is a label, not a cause; the honest cause exists org-side (platform-support-tiers.md: "No PyDevices claim assumes a Mac on our bench, because there isn't one") but the README neither states nor links it.Suggested fix: Add a tier column to both platform tables (manylinux/Windows bench- or CI-proven per what actually runs; Android and Pyodide labeled honestly), and rewrite the macOS/aarch64 sentence to give the cause and link platform-support-tiers.md.
[LOW] Neither build recipe declares a supported upstream version for MicroPython or CircuitPython
README.md:147-161 says "Clone as a sibling of
micropython/" and gives no tag, branch, or supported range; README.md:204-210 and docs/installation.md:41-45 do the same for CircuitPython.grep -rn "1\.2[0-9]|pinned|v1\.28|MICROPY_VERSION|tag" docs/*.md README.md AGENTS.md pygraphics.tomlreturns only README.md:13 (about release tags). apply_cp_patches.sh names no CircuitPython version either (no "10.2" anywhere in the repo). S1's criterion is "declared, pinned dependencies". Today this is not broken — I built against upstream master (5d592bd, 1.30.0-preview) and it linked and ran clean — but a stranger has no way to know what the repo is tested against, and CircuitPython (where the integration is a patch script against a moving tree) is the fragile one.Suggested fix: State the tested upstream revisions next to each recipe (e.g. "tested against MicroPython v1.28.0 and current master; CircuitPython 10.2.1") and have apply_cp_patches.sh --status warn when the CircuitPython tree is not the declared revision.
[LOW] Pyodide wheel tag claim is right for one interpreter and wrong for the other
README.md:72 and docs/installation.md:11 name the wasm wheel
pyemscripten_2026_0_wasm32; docs/installation.md:60 goes further — "Each TestPyPI release includes apyemscripten_2026_0_wasm32wheel". The 0.0.38 files on TestPyPI are...-cp313-cp313-pyemscripten_2025_0_wasm32.whland...-cp314-cp314-pyemscripten_2026_0_wasm32.whl— the cp313 wheel carries the 2025 ABI tag. A Pyodide user matching the documented tag against their runtime is told the wrong thing half the time.Suggested fix: Say "pyemscripten 2025_0 (cp313) and 2026_0 (cp314)", or drop the tag and say micropip selects the matching wheel.
[LOW] Workspace residue in the first test the README tells a stranger to run
tests/test_area.py:7-9, above every import:
This strips sys.path entries matching a private layout (a
.../repos/.../graphicscheckout — the pre-rename import name, per docs/installation.md:70-75). It is dead code anywhere but that machine, it appears in exactly the file README.md:117 and README.md:160 point a newcomer at, and it is the kind of thing the rigor lens reads as "works in the author's workspace".Suggested fix: Delete the loop; if some ordering hazard is real, express it in tests/_env.py where the sys.path policy already lives, with a comment saying what it defends against.
[LOW] The README's test recipe runs 3 of the repo's 15 test modules, and never points at the real command
README.md:117-119 lists exactly
tests/test_area.py,tests/test_pygraphics.py,tests/test_subclass.py. The suite is 15 modules; CI runspython -m unittest discover -s teststwice (.github/workflows/tests.yml:41,52) — 98 tests pure, 85 native. tests/README.md has the correct invocations for both implementations, and the top-level README never links to it. A reviewer running the README's three files sees "ok" three times and has exercised a small fraction of what the project actually validates.Suggested fix: Replace the three lines with
python -m unittest discover -s testsand thePYGRAPHICS_TEST_NATIVE=1variant, and link tests/README.md.Each finding is independent; tick them off here or split any that deserves its own thread.