diff --git a/AGENTS.md b/AGENTS.md index fc60f824..57f2efe4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -288,6 +288,51 @@ Two things follow: repository and the whole Python bridge are instrumented. Rule 12 is about what genuinely needs a device; this rule is about not sending it things that do not. +### 14. A Python dependency is pinned in four places, and they all move together + +There is no single manifest. `opentagviewer_export` is shared code that runs under **both** +interpreters — Chaquopy packages it into the APK, and the desktop exporter runs the same files +from source — so every Python dependency is declared once per consumer: + +| Where | What it feeds | How to change it | +| --- | --- | --- | +| `app/build.gradle.kts` — the `install(…)` lines in the Chaquopy block | **what ships in the APK** | by hand | +| `app/src/test/python/requirements.txt` | the Chaquopy bridge tests | by hand | +| `python/pyproject.toml` | the desktop exporter and its tests | by hand | +| `python/uv.lock` | what `uv sync --frozen` actually installs | `cd python && uv lock` — never by hand | + +**Miss one and the two halves ship different libraries.** That is not version skew: FindMy.py is +pinned by commit off a fork, and its API moves between commits (rule 3), so one consumer gets +methods the other does not. It has happened — `requirements.txt` said `FindMy==0.9.8` from PyPI +while the app built the fork, and both lines were individually true for months. + +Two tests in `app/src/test/python/test_main.py` enforce it, and they are worth knowing by name +because they fail in a place that looks unrelated: + +- `test_the_whole_repository_pins_one_findmy` +- `test_pinned_versions_match_the_app_build` — every install, including the `git+…` ones, which + is the shape that was invisible to it before + +They run in the **Chaquopy bridge tests** step of `build-debug.yml`, which is *before* the APK is +built. So a missed pin presents as **"the APK build failed"** with the APK steps merely skipped, +and the actual assertion is several steps up the log. Read the step list before believing the +build broke: + +```bash +gh api repos/parawanderer/OpenTagViewer/actions/jobs/ \ + --jq '.steps[] | "\(.conclusion // .status)\t\(.name)"' +``` + +Run them before pushing — they need no device and take under a second: + +```bash +python -m venv .venv && .venv/bin/pip install -r app/src/test/python/requirements.txt +.venv/bin/python -m pytest app/src/test/python -q +``` + +The same applies to every other shared dependency, not just FindMy.py: `PyYAML` is pinned in +`app/build.gradle.kts` and `python/pyproject.toml` for exactly this reason. + --- ## Building and testing diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ffb7bf4a..39ea4d84 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -368,7 +368,7 @@ chaquopy { // wheel for desktop platforms and a pure-Python `py3-none-any` one as well. // There is no Android wheel, so pip falls back to the pure-Python build - which // is correct but markedly slower. The messages here are small enough not to care. - install("git+https://github.com/parawanderer/FindMy.py@337381dedf4662d730752e727d896f964feae1f8") + install("git+https://github.com/parawanderer/FindMy.py@4f940158c437b17ae61a8e90af084d658750629c") install("NSKeyedUnArchiver==1.5") diff --git a/app/src/test/python/requirements.txt b/app/src/test/python/requirements.txt index 6464bc3a..b96abbc8 100644 --- a/app/src/test/python/requirements.txt +++ b/app/src/test/python/requirements.txt @@ -9,7 +9,7 @@ # `FindMy==0.9.8` here for as long as the app built the fork, so every bridge test # ran against a library the app does not ship - which is not a small difference: # the fork's Anisette providers take `serial=` and PyPI's do not. -git+https://github.com/parawanderer/FindMy.py@337381dedf4662d730752e727d896f964feae1f8 +git+https://github.com/parawanderer/FindMy.py@4f940158c437b17ae61a8e90af084d658750629c NSKeyedUnArchiver==1.5 PyYAML==6.0.2 diff --git a/python/pyproject.toml b/python/pyproject.toml index c8d45e89..fbc09a45 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -69,7 +69,7 @@ constraint-dependencies = [ ] [tool.uv.sources] -FindMy = { git = "https://github.com/parawanderer/FindMy.py", rev = "337381dedf4662d730752e727d896f964feae1f8" } +FindMy = { git = "https://github.com/parawanderer/FindMy.py", rev = "4f940158c437b17ae61a8e90af084d658750629c" } [dependency-groups] # Only the release build installs this, with `uv sync --no-default-groups --group build`. It is diff --git a/python/uv.lock b/python/uv.lock index 26b0ac17..8cde8c92 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -529,8 +529,8 @@ resolution-markers = [ "platform_machine == 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "cffi", marker = "platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" } wheels = [ @@ -547,8 +547,8 @@ resolution-markers = [ "platform_machine != 'x86_64' or sys_platform != 'darwin'", ] dependencies = [ - { name = "cffi", marker = "(platform_machine != 'x86_64' and platform_python_implementation != 'PyPy') or (platform_python_implementation != 'PyPy' and sys_platform != 'darwin')" }, - { name = "typing-extensions", marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } wheels = [ @@ -649,7 +649,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -659,7 +659,7 @@ wheels = [ [[package]] name = "findmy" version = "0.10.1" -source = { git = "https://github.com/parawanderer/FindMy.py?rev=337381dedf4662d730752e727d896f964feae1f8#337381dedf4662d730752e727d896f964feae1f8" } +source = { git = "https://github.com/parawanderer/FindMy.py?rev=4f940158c437b17ae61a8e90af084d658750629c#4f940158c437b17ae61a8e90af084d658750629c" } dependencies = [ { name = "aiohttp" }, { name = "anisette" }, @@ -1032,7 +1032,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "findmy", git = "https://github.com/parawanderer/FindMy.py?rev=337381dedf4662d730752e727d896f964feae1f8" }, + { name = "findmy", git = "https://github.com/parawanderer/FindMy.py?rev=4f940158c437b17ae61a8e90af084d658750629c" }, { name = "pycryptodome", specifier = "==3.22.0" }, { name = "pyyaml", specifier = "==6.0.2" }, { name = "pyzipper", specifier = "==0.4.0" }, @@ -1717,7 +1717,7 @@ name = "winrt-runtime" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/dd/acdd527c1d890c8f852cc2af644aa6c160974e66631289420aa871b05e65/winrt_runtime-3.2.1.tar.gz", hash = "sha256:c8dca19e12b234ae6c3dadf1a4d0761b51e708457492c13beb666556958801ea", size = 21721, upload-time = "2025-06-06T14:40:27.593Z" } wheels = [ @@ -1743,7 +1743,7 @@ name = "winrt-windows-devices-bluetooth" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/a0/1c8a0c469abba7112265c6cb52f0090d08a67c103639aee71fc690e614b8/winrt_windows_devices_bluetooth-3.2.1.tar.gz", hash = "sha256:db496d2d92742006d5a052468fc355bf7bb49e795341d695c374746113d74505", size = 23732, upload-time = "2025-06-06T14:41:20.489Z" } wheels = [ @@ -1769,7 +1769,7 @@ name = "winrt-windows-devices-bluetooth-advertisement" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/fc/7ffe66ca4109b9e994b27c00f3d2d506e6e549e268791f755287ad9106d8/winrt_windows_devices_bluetooth_advertisement-3.2.1.tar.gz", hash = "sha256:0223852a7b7fa5c8dea3c6a93473bd783df4439b1ed938d9871f947933e574cc", size = 16906, upload-time = "2025-06-06T14:41:21.448Z" } wheels = [ @@ -1795,7 +1795,7 @@ name = "winrt-windows-devices-bluetooth-genericattributeprofile" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/44/21/aeeddc0eccdfbd25e543360b5cc093233e2eab3cdfb53ad3cabae1b5d04d/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1.tar.gz", hash = "sha256:cdf6ddc375e9150d040aca67f5a17c41ceaf13a63f3668f96608bc1d045dde71", size = 38896, upload-time = "2025-06-06T14:41:22.687Z" } wheels = [ @@ -1821,7 +1821,7 @@ name = "winrt-windows-devices-enumeration" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9e/dd/75835bfbd063dffa152109727dedbd80f6e92ea284855f7855d48cdf31c9/winrt_windows_devices_enumeration-3.2.1.tar.gz", hash = "sha256:df316899e39bfc0ffc1f3cb0f5ee54d04e1d167fbbcc1484d2d5121449a935cf", size = 23538, upload-time = "2025-06-06T14:41:26.787Z" } wheels = [ @@ -1847,7 +1847,7 @@ name = "winrt-windows-devices-radios" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/02/9704ea359ad8b0d6faa1011f98fb477e8fb6eac5201f39d19e73c2407e7b/winrt_windows_devices_radios-3.2.1.tar.gz", hash = "sha256:4dc9b9d1501846049eb79428d64ec698d6476c27a357999b78a8331072e18a0b", size = 5908, upload-time = "2025-06-06T14:41:44.868Z" } wheels = [ @@ -1873,7 +1873,7 @@ name = "winrt-windows-foundation" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0c/55/098ce7ea0679efcc1298b269c48768f010b6c68f90c588f654ec874c8a74/winrt_windows_foundation-3.2.1.tar.gz", hash = "sha256:ad2f1fcaa6c34672df45527d7c533731fdf65b67c4638c2b4aca949f6eec0656", size = 30485, upload-time = "2025-06-06T14:41:53.344Z" } wheels = [ @@ -1899,7 +1899,7 @@ name = "winrt-windows-foundation-collections" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/62/d21e3f1eeb8d47077887bbf0c3882c49277a84d8f98f7c12bda64d498a07/winrt_windows_foundation_collections-3.2.1.tar.gz", hash = "sha256:0eff1ad0d8d763ad17e9e7bbd0c26a62b27215016393c05b09b046d6503ae6d5", size = 16043, upload-time = "2025-06-06T14:41:53.983Z" } wheels = [ @@ -1925,7 +1925,7 @@ name = "winrt-windows-storage-streams" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "winrt-runtime", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "winrt-runtime" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/50/f4488b07281566e3850fcae1021f0285c9653992f60a915e15567047db63/winrt_windows_storage_streams-3.2.1.tar.gz", hash = "sha256:476f522722751eb0b571bc7802d85a82a3cae8b1cce66061e6e758f525e7b80f", size = 34335, upload-time = "2025-06-06T14:43:23.905Z" } wheels = [