From fe75fd0819bc2d541188d427fd8a011543b01838 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sun, 4 Oct 2026 04:31:32 +0900 Subject: [PATCH] Chore: Keep pythonx a namespace package shared by every pythonx library AGENTS 12 states the rule. tests/test_wheel.py and a publish-pypi step refuse a wheel that carries pythonx/__init__.py. Today packages.find includes only pythonx.compose*, so even a stray pythonx/__init__.py on disk does not ship (checked); the guards catch a packaging change. Counts +1 test (worktree baselines 152/123, 274/1). Closes #106 --- .github/workflows/publish-pypi.yml | 5 +++++ AGENTS.md | 5 +++++ PROJECT.md | 4 ++-- docs/SPEC.md | 4 ++-- docs/guide/status.html | 2 +- tests/test_wheel.py | 4 ++++ 6 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 7e97030..15f1ad4 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -31,6 +31,11 @@ jobs: - name: Build the sdist and the wheel run: python -m build - run: twine check dist/* + - name: Keep pythonx a namespace package + run: | + if unzip -l dist/*.whl | grep -q ' pythonx/__init__.py$'; then + echo 'pythonx/__init__.py would hide the other pythonx libraries'; exit 1 + fi # Importing needs no binder: names resolve lazily, on first use. - name: Smoke-install the wheel in a fresh venv run: | diff --git a/AGENTS.md b/AGENTS.md index e620e91..7b9a4bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -188,6 +188,11 @@ worktree so a test can see it. Read it in place. depending on it is allowed, and this package keeps no member resolver or keyword wrapper of its own. What stays here: the module grouping, the snake-case-only `pythonx` surface, aliases, objects and grouped constants, the value-class allowlist, the app root and the Pythonic stubs. +- **`pythonx` is a namespace package shared by every pythonx library.** Never add + `pythonx/__init__.py`: pythonx-compose, pythonx-concurrent, pythonx-graphics and pythonx-platform + all install into `pythonx`, and an `__init__.py` from any one of them would hide the others. This + package owns only `pythonx/compose/`. `tests/test_wheel.py` and the publish workflow refuse a + wheel that contains `pythonx/__init__.py`. - `pythonx/` is a **real package on disk**. Its modules import the `androidx.compose.*` modules the binder exposes and restructure them Pythonically. A design in which something else synthesises `pythonx.*` (for example with `__path__ = []`) and the on-disk files cannot be imported is ruled diff --git a/PROJECT.md b/PROJECT.md index 71a74a9..ca5a9e7 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -35,8 +35,8 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **` ``` uv run --with pytest --with mypy pytest tests -q - PythonMultiplatform 체크아웃 없음 151 passed, 123 skipped - python-multiplatform develop 31c092f0+ 273 passed, 1 skipped + PythonMultiplatform 체크아웃 없음 152 passed, 123 skipped + python-multiplatform develop 31c092f0+ 274 passed, 1 skipped describe_member 없는 체크아웃 같은 수, 그중 7 개 건너뜀 (python-multiplatform #54 이전) describe(module, name) 없는 체크아웃 같은 수, 그중 13 개 건너뜀 (python-multiplatform #36 이전) 그보다 오래된 체크아웃 거기에 5 개 더 건너뜀 (#131 이전, snake_case 이름 없음) diff --git a/docs/SPEC.md b/docs/SPEC.md index 99a7e5b..8257ea4 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -21,8 +21,8 @@ Run from a worktree with `uv run --with pytest --with mypy pytest tests -q -rs` | Environment | Result | |---|---| -| No `PythonMultiplatform` checkout found | **151 passed, 123 skipped**, 186 subtests passed | -| python-multiplatform `develop` at `31c092f0` or later (property rows, #38; `describe_member`, #54; besides `add_member_resolver` and `describe(module, name)`) | **273 passed, 1 skipped**, 232 subtests passed | +| No `PythonMultiplatform` checkout found | **152 passed, 123 skipped**, 186 subtests passed | +| python-multiplatform `develop` at `31c092f0` or later (property rows, #38; `describe_member`, #54; besides `add_member_resolver` and `describe(module, name)`) | **274 passed, 1 skipped**, 232 subtests passed | | an older checkout, before python-multiplatform #131 (no binder-side snake_case names) | the same, with the snake_case method tests skipped | | an older one, without `describe(module, name)` (python-multiplatform #36) | with 13 more skipped again | | an older one still, without `add_member_resolver` | with 5 more skipped again | diff --git a/docs/guide/status.html b/docs/guide/status.html index ff0c1a3..1f6aa7f 100644 --- a/docs/guide/status.html +++ b/docs/guide/status.html @@ -36,7 +36,7 @@ State상태Item항목 implemented구현Mapping manifest pythonx/compose/pythonx-map.toml: inside the package, ships in the wheel매핑 매니페스트 pythonx/compose/pythonx-map.toml: 패키지 안에 있으며 wheel 에 포함implemented구현@Composable identity decorator@Composable 항등 데코레이터implemented구현pythonx as a real on-disk package re-exporting androidx.compose.* by one rule: names, keywords, signatures, Dp allowlistandroidx.compose.* 를 규칙 하나로 재노출하는 실제 디스크 패키지 pythonx: 이름, 키워드, 시그니처, Dp 허용 목록implemented구현Modifier chains with snake_case methods, overload dispatch, Dp as a numbersnake_case 메서드를 쓰는 Modifier 체인, 오버로드 디스패치, 숫자 Dpimplemented구현Column, Row, Spacer importable from material3 as well as layout (manifest [aliases])layout 뿐 아니라 material3 에서도 import 되는 Column, Row, Spacer (매니페스트 [aliases])implemented구현Type stubs (.pyi) generated from real Compose 1.11.1 by the runtime's naming rule, with py.typed, checked with mypy (#12); many types are still Any런타임과 같은 이름 규칙으로 실제 Compose 1.11.1 에서 생성한 타입 스텁(.pyi)과 py.typed, mypy 로 검사(#12); 아직 많은 타입이 Any 입니다implemented구현Snake-case names, keywords and signatures: modules and methods; the names and keywords on a proxy are the binder's since python-multiplatform #131 (an older binder spells them in Kotlin), and Kotlin's own spelling still works at run timesnake_case 이름·키워드·시그니처: 모듈과 메서드; 프록시의 이름과 키워드는 python-multiplatform #131 부터 바인더가 제공하고(오래된 바인더는 Kotlin 이름으로 씁니다), Kotlin 이름도 실행 시에는 그대로 통합니다partial부분The empty Modifier: Modifier.padding(16) from the class needs an app-supplied factory against real Compose빈 Modifier: 클래스에서 시작하는 Modifier.padding(16) 은 실제 Compose 에서 앱이 제공하는 팩토리가 필요implemented구현Alignment / Arrangement: Alignment.Center, Arrangement.End, Arrangement.spaced_by(8), and the grouped Alignment.Horizontal.End beside Alignment.EndAlignment / Arrangement: Alignment.Center, Arrangement.End, Arrangement.spaced_by(8), 그리고 Alignment.End 와 나란히 묶음 표기 Alignment.Horizontal.Endpartial부분Material 3 widgets: re-exported by rule; render proofs per widget pending (#9)Material 3 위젯: 규칙으로 재노출됨; 위젯별 렌더 증거 대기(#9)partial부분Distribution as pythonx-compose: the wheel carries the manifest, the re-export rule, the type stubs and py.typedpythonx-compose 배포: wheel 에 매니페스트, 재노출 규칙, 타입 스텁, py.typed 포함partial부분A declared app root (@app) and Pythonic state (state), with no update call: binder path tested against a fake host shaped after python-multiplatform #38; real Compose is the E2E module (#11, #19)갱신 호출 없는 선언형 앱 루트(@app)와 파이썬다운 상태(state): 바인더 경로는 python-multiplatform #38 모양의 가짜 호스트로 테스트됨, 실제 Compose 는 E2E 모듈(#11, #19)partial부분DefaultIcons is Icons.Default, written DefaultIcons.Add, tested against a fake host shaped after python-multiplatform #37/#38; the real draw is in python-multiplatform's render testDefaultIcons 는 Icons.Default 이며 DefaultIcons.Add 로 씁니다. python-multiplatform #37/#38 모양의 가짜 호스트로 테스트됨; 실제 그리기는 python-multiplatform 의 렌더 테스트에 있음partial부분TextField(state=…) with TextFieldState from pythonx.compose.foundation.text.input: tested against a fake host shaped after python-multiplatform #73; the input-method (IME) composing proof is python-multiplatform E2E #26 (#10)pythonx.compose.foundation.text.input 의 TextFieldState 를 쓰는 TextField(state=…): python-multiplatform #73 모양의 가짜 호스트로 테스트됨; 입력기(IME) 조합 증거는 python-multiplatform E2E #26 (#10)planned계획Colour schemes (36 colour parameters)색 스킴 (색 매개변수 36 개)partial부분remember_saveable with the notebook's getValue() / setValue(): tested here against a fake host; saving and restoring is python-multiplatform's test (#174)노트북의 getValue() / setValue() 를 쓰는 remember_saveable: 여기서는 가짜 호스트로 테스트, 저장과 복원은 python-multiplatform 의 테스트(#174)planned계획Coroutine scopes (after pythonx-concurrent)코루틴 스코프 (pythonx-concurrent 이후) -

Test baseline: 151 passed, 123 skipped without a python-multiplatform checkout; 273 passed, 1 skipped with a current one. The one skip with a current checkout reads the maintainer's UI.ipynb.

테스트 기준선: python-multiplatform 체크아웃이 없으면 151 passed, 123 skipped, 최신 체크아웃이면 273 passed, 1 skipped. 최신 체크아웃에서 건너뛰는 1 개는 메인테이너의 UI.ipynb 를 읽는 테스트입니다.

+

Test baseline: 152 passed, 123 skipped without a python-multiplatform checkout; 274 passed, 1 skipped with a current one. The one skip with a current checkout reads the maintainer's UI.ipynb.

테스트 기준선: python-multiplatform 체크아웃이 없으면 152 passed, 123 skipped, 최신 체크아웃이면 274 passed, 1 skipped. 최신 체크아웃에서 건너뛰는 1 개는 메인테이너의 UI.ipynb 를 읽는 테스트입니다.

FAQ

diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 86d76c5..fb681d3 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -95,6 +95,10 @@ def test_no_build_tooling_or_tests_ship(self): with self.subTest(prefix=prefix): self.assertEqual([], sorted(n for n in self.names if n.startswith(prefix))) + def test_pythonx_stays_a_namespace_package(self): + # Every pythonx library installs into `pythonx`; an `__init__.py` from one would hide the rest. + self.assertNotIn("pythonx/__init__.py", self.names) + def test_the_re_export_rule_ships(self): self.assertIn("pythonx/compose/_reexport.py", self.names)