diff --git a/PROJECT.md b/PROJECT.md index 0bc271a..c690923 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -28,15 +28,15 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **` | 상태 | 항목 | |---|---| | 구현 | 매핑 매니페스트 `pythonx-map.toml`, `@Composable` 항등 데코레이터, 실제 디스크 패키지 `pythonx` 와 모듈 수준 규칙 기반 재노출(snake_case 이름·키워드·시그니처, 오버로드 디스패치, 값 클래스 허용 목록), 실제 Compose 1.11.1 에서 생성한 `.pyi` 스텁과 `py.typed`(wheel 포함, mypy 로 검사), `Alignment`/`Arrangement` 상수(평면 `Alignment.End` 와 묶음 `Alignment.Horizontal.End` 모두, #9) | -| 부분 | 배포 설정, proxy 메서드의 키워드 인자는 아직 Kotlin 이름(`m.padding(paddingValues=...)`), Material 3 위젯(렌더 증거는 python-multiplatform 에만), `Icon`·색 스킴(import 만 됨) | -| 계획 | 선언형 앱 루트(#11), `remember_saveable`·`DefaultIcons`·코루틴 스코프 | +| 부분 | 선언형 앱 루트 `@app`·`state`·`app_root`(#11; 로직은 테스트됨, 바인더 경로는 python-multiplatform #38 대기), 배포 설정, proxy 메서드의 키워드 인자는 아직 Kotlin 이름(`m.padding(paddingValues=...)`), Material 3 위젯(렌더 증거는 python-multiplatform 에만), `Icon`·색 스킴(import 만 됨) | +| 계획 | `remember_saveable`·`DefaultIcons`·코루틴 스코프 | ### 테스트 기준선 ``` python3 -m pytest tests -q - PythonMultiplatform 체크아웃 없음 91 passed, 73 skipped - python-multiplatform develop d6d39787+ 163 passed, 1 skipped + PythonMultiplatform 체크아웃 없음 106 passed, 75 skipped + python-multiplatform develop d6d39787+ 178 passed, 3 skipped describe(module, name) 없는 체크아웃 같은 수, 그중 13 개 건너뜀 (python-multiplatform #36 이전) 그보다 오래된 체크아웃 거기에 5 개 더 건너뜀 (member resolver 없음) ``` diff --git a/README.md b/README.md index eca187c..8396d7a 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ standalone desktop toolkit. | Material 3 widgets (`Text`, `Button`, `Card`, `TextField`, …) | 🟡 partial — re-exported by rule; render proofs per widget pending (#9) | | Type stubs (`.pyi`) from real Compose 1.11.1, `py.typed` | ✅ implemented and checked with mypy; many types are still `Any`, and method keywords are Kotlin's (#12) | | Distribution (`pythonx-compose`) | 🟡 partial — the wheel carries the manifest, the re-export rule, the stubs and `py.typed` | -| Declared app root and Pythonic state, no update call | ⏳ planned (#11) | +| Declared app root (`@app`) and Pythonic state (`state`), no update call | 🟡 partial — logic tested; the binder path waits for python-multiplatform #38 (#11) | | `TextField` input-method (IME) handling | ⏳ planned (#10) | | `Icon`, `DefaultIcons`, colour schemes | ⏳ planned (python-multiplatform #37) | | `remember_saveable`, coroutine scopes | ⏳ planned | diff --git a/docs/SPEC.md b/docs/SPEC.md index 2f8796c..5497b44 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -14,26 +14,26 @@ Each item carries a status: A behaviour change starts here, then becomes a failing test, then code (`AGENTS.md` §5). -## 0. Test baseline (2026-10-03, after issue #9's grouped constants) +## 0. Test baseline (2026-10-03, after issue #11's app root) Run from a worktree with `python -m pytest tests -q -rs` (pytest 8, mypy 2.4, CPython 3.13), where `UI.ipynb` is absent and its one test skips: | Environment | Result | |---|---| -| No `PythonMultiplatform` checkout found | **91 passed, 73 skipped**, 122 subtests passed | -| python-multiplatform `develop` at `d6d39787` or later (has `add_member_resolver` and `describe(module, name)`) | **163 passed, 1 skipped**, 149 subtests passed | +| No `PythonMultiplatform` checkout found | **106 passed, 75 skipped**, 122 subtests passed | +| python-multiplatform `develop` at `d6d39787` or later (has `add_member_resolver` and `describe(module, name)`) | **178 passed, 3 skipped**, 149 subtests passed | | an older checkout, without `describe(module, name)` (python-multiplatform #36) | the same, with 13 more skipped | | an older one still, without `add_member_resolver` | with 5 more skipped again | Without a checkout, the 72 tests that install the binder's layers through `tests/adapter.py` skip. Against a binder without `describe(module, name)`, the 13 that check grouped constants (§7, S7.1) skip. Against a binder older than `ba4c6f49`, the 5 that call a snake_case method on a proxy the -binder returned skip as well, because that needs its member resolver (python-multiplatform #17). The 1 skip in both -rows is `UI.ipynb`'s (§2). `tests/test_typing.py` skips where mypy is not installed. A skip is not a +binder returned skip as well, because that needs its member resolver (python-multiplatform #17). The 3 skips against a current checkout are `UI.ipynb`'s (§2) and the 2 that need +python-multiplatform #38 (S5.4). `tests/test_typing.py` skips where mypy is not installed. A skip is not a pass. -Of the 91 that pass without a checkout, 37 check the type stubs and the wheel (S1.2); most of the +Of the 106 that pass without a checkout, 38 check the type stubs and the wheel (S1.2); most of the rest assert **absence** (a retired token, a deleted file, a docstring that exists). Those are listed in §9 and are not counted as features. @@ -194,6 +194,30 @@ job. Tests: `tests/test_runtime_module.py::TheRuntimeSeam` (4 tests) and `::TheChaquopyMechanismIsGone` (3 tests), importing `pythonx.compose.runtime` the ordinary way. +### S5.4 The app root, `app` and `state` — `partial` (logic tested; the binder path waits for python-multiplatform #38) + +The host draws with `PythonContent("pythonx.compose.runtime", "app_root")` (python-multiplatform #18, +issue #11). `pythonx.compose.runtime` provides: + +- `app_root` — a Compose `MutableState` whose `.value` is a zero-argument callable, or `None` (the + host draws nothing). It is created on first read, by the binder's + `androidx.compose.runtime.mutableStateOf(None)`, because the binder may be installed after `pythonx` + is imported; afterwards it is an ordinary module attribute, the same object every time. +- `@app` — `app(fn)` sets `app_root.value = fn` and returns `fn` unchanged. Declaring the root again + replaces the value, so the screen follows. There is no update or refresh function (INTENT §5.1). +- `state(initial)` — `mutableStateOf(initial)`, read and written through `.value`; no `getValue` or + `setValue`. + +Both states come from one internal function, `_new_state`. When the binder cannot supply +`mutableStateOf` it raises `RuntimeError` naming python-multiplatform #38. No pure-Python state is a +fallback: it would not make Compose recompose. Names other than `app_root` still resolve through the +re-export rule, and the module's own names win. + +Tests: `tests/test_app_root.py::TheLogic` (11) and `::TheStateFactory` (4) run against a test-only +fake state patched in for `_new_state`; `::TheBinderPath` (2) uses the real one and skips, "needs +python-multiplatform #38", until the binder binds `mutableStateOf`. The stub carries `app`, `state` +and `app_root` (`tests/test_stubs.py`). + ### S5.2 Material 3 composables reach Python without per-widget wrappers — `partial` `Text`, `Button`, `Card`, `ListItem`, `Badge`, `BadgedBox`, `MaterialTheme`, `IconButton` and its @@ -335,7 +359,7 @@ the binder lists no member that would win over these groups. | Name | Notebook use | Note | |---|---|---| -| `remember_saveable` | imported from `pythonx.compose.runtime`; state read/written with `getValue()` / `setValue()` | INTENT §5.1: Pythonic attribute access instead of the accessors | +| `remember_saveable` | imported from `pythonx.compose.runtime`; state read/written with `getValue()` / `setValue()` | INTENT §5.1: `state(initial)` read and written through `.value` (S5.4, `partial`, #38); keeping a value across recreation is not provided | | `DefaultCoroutineScope`, `MainCoroutineScope` | imported from `pythonx.compose.runtime` | INTENT §4.1, open | | `DefaultIcons` | `DefaultIcons.Add()` | INTENT §5.7: `Icons.Default` by alias, once python-multiplatform #37 binds `material-icons-core` | | `modifier` | lower-case instance from `pythonx.compose.ui` | INTENT §5.4: not provided; written `Modifier` | @@ -343,7 +367,7 @@ the binder lists no member that would win over these groups. | `Spacer(start=..., top=...)` | spacing parameters | INTENT §5.6: not supported; written `Spacer(modifier=Modifier.padding(...))` | | `Column`, `Row`, `Spacer` | imported from `pythonx.compose.material3` | INTENT §5.2: served from both, by the manifest's `[aliases]` (`tests/test_chain.py::ManifestAliases`); render proof pending (#9) | | `Card`, `Button`, `Text`, `TextField` | as above | S5.2 | -| `main.App`, `App.update(...)` | live screen replacement from a cell | INTENT §5.1: a declared root that a redefinition replaces, no update function (issue #11) | +| `main.App`, `App.update(...)` | live screen replacement from a cell | INTENT §5.1: `@app` declares the root, a redeclaration replaces it, no update function (S5.4, `partial`, issue #11) | ## 9. Repository hygiene (not behaviour) diff --git a/docs/guide/getting-started.html b/docs/guide/getting-started.html index a67dffe..3f2e512 100644 --- a/docs/guide/getting-started.html +++ b/docs/guide/getting-started.html @@ -43,9 +43,9 @@
python3 -m pytest tests -q
-Expect 91 passed, 73 skipped. One skipped test reads the maintainer's UI.ipynb, which a clone does not have; mypy type-checks the stubs. The other skipped tests drive the binder's adaptation layer and need a python-multiplatform checkout beside this one, or:
91 passed, 73 skipped 가 정상입니다. 건너뛴 테스트 하나는 클론에는 없는 메인테이너의 UI.ipynb 를 읽으며, mypy 는 스텁을 타입 검사합니다. 나머지 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 python-multiplatform 체크아웃이 있거나 다음이 필요합니다:
Expect 106 passed, 75 skipped. One skipped test reads the maintainer's UI.ipynb, which a clone does not have; mypy type-checks the stubs. The other skipped tests drive the binder's adaptation layer and need a python-multiplatform checkout beside this one, or:
106 passed, 75 skipped 가 정상입니다. 건너뛴 테스트 하나는 클론에는 없는 메인테이너의 UI.ipynb 를 읽으며, mypy 는 스텁을 타입 검사합니다. 나머지 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 python-multiplatform 체크아웃이 있거나 다음이 필요합니다:
PYTHONMULTIPLATFORM_HOME=/path/to/python-multiplatform python3 -m pytest tests -q
-With a current one (ba4c6f49 or later) 143 pass and only the UI.ipynb test skips; an older one skips 5 that call a snake_case method on a proxy the binder returned. That is expected and recorded.
최신 체크아웃(ba4c6f49 이후)이면 143 개가 통과하고 UI.ipynb 테스트만 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 건너뜁니다. 예상된 상태이며 기록되어 있습니다.
With a current one (ba4c6f49 or later) 178 pass and 3 skip (the UI.ipynb test and the 2 that need python-multiplatform #38); an older one skips 5 that call a snake_case method on a proxy the binder returned. That is expected and recorded.
최신 체크아웃(ba4c6f49 이후)이면 178 개가 통과하고 3 개(UI.ipynb 테스트와 python-multiplatform #38 이 필요한 2 개)를 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 건너뜁니다. 예상된 상태이며 기록되어 있습니다.
from pythonx.compose.runtime import app, state
+from pythonx.compose.material3 import Text, Button
+
+count = state(0)
+
+@app
+def Main():
+ Button(on_click=lambda: setattr(count, "value", count.value + 1),
+ content=lambda: Text(f"Clicked {count.value}"))
+@app declares the screen the host draws and returns the function unchanged. Declaring it again, for example in a notebook cell, replaces the screen; there is no update call. state(initial) is a Compose state, read and written through .value. The host is configured once with PythonContent("pythonx.compose.runtime", "app_root").
@app 은 호스트가 그리는 화면을 선언하고 함수를 그대로 돌려줍니다. 노트북 셀 등에서 다시 선언하면 화면이 바뀌며, 갱신 호출은 없습니다. state(initial) 는 Compose 상태이며 .value 로 읽고 씁니다. 호스트는 PythonContent("pythonx.compose.runtime", "app_root") 로 한 번만 설정합니다.
Status: partial. The logic is tested; the binder path, which creates the state through Compose's mutableStateOf, waits for python-multiplatform #38. Until then these calls raise a RuntimeError that says so.
상태: 부분. 로직은 테스트되었습니다. Compose 의 mutableStateOf 로 상태를 만드는 바인더 경로는 python-multiplatform #38 을 기다립니다. 그때까지는 이를 알리는 RuntimeError 가 발생합니다.
The notebook's signatures are examples. The surface follows Compose:
노트북의 시그니처는 예시입니다. 실제 표면은 Compose 를 따릅니다:
diff --git a/docs/guide/status.html b/docs/guide/status.html index a695aa0..0ea683f 100644 --- a/docs/guide/status.html +++ b/docs/guide/status.html @@ -34,9 +34,9 @@Implemented means code in this repository and a passing test that exercises it. Tests that only assert an absence are not counted.
구현은 이 저장소의 코드와 그것을 실행하는 통과 테스트가 함께 있다는 뜻입니다. 부재만 확인하는 테스트는 세지 않습니다.
| 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 체인, 오버로드 디스패치, 숫자 Dp |
| implemented구현 | 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 and method keywords are Kotlin's런타임과 같은 이름 규칙으로 실제 Compose 1.11.1 에서 생성한 타입 스텁(.pyi)과 py.typed, mypy 로 검사(#12); 아직 많은 타입이 Any 이고 메서드 키워드는 Kotlin 이름 |
| partial부분 | Snake-case names, keywords and signatures — done for modules and method names; method keywords are still Kotlin'ssnake_case 이름·키워드·시그니처 — 모듈과 메서드 이름 완료, 메서드 키워드는 아직 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.End |
| partial부분 | 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 포함 |
| planned계획 | A declared app root and Pythonic state, with no update call (#11)갱신 호출 없는 선언형 앱 루트와 파이썬다운 상태(#11) |
| planned계획 | TextField input-method (IME) handling (#10)TextField 입력기(IME) 처리(#10) |
| planned계획 | Icon, DefaultIcons (python-multiplatform #37), colour schemesIcon, DefaultIcons (python-multiplatform #37), 색 스킴 |
| planned계획 | remember_saveable, coroutine scopesremember_saveable, 코루틴 스코프 |
| 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 체인, 오버로드 디스패치, 숫자 Dp |
| implemented구현 | 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 and method keywords are Kotlin's런타임과 같은 이름 규칙으로 실제 Compose 1.11.1 에서 생성한 타입 스텁(.pyi)과 py.typed, mypy 로 검사(#12); 아직 많은 타입이 Any 이고 메서드 키워드는 Kotlin 이름 |
| partial부분 | Snake-case names, keywords and signatures — done for modules and method names; method keywords are still Kotlin'ssnake_case 이름·키워드·시그니처 — 모듈과 메서드 이름 완료, 메서드 키워드는 아직 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.End |
| partial부분 | 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 — logic tested, the binder path waits for python-multiplatform #38 (#11)갱신 호출 없는 선언형 앱 루트(@app)와 파이썬다운 상태(state) — 로직은 테스트됨, 바인더 경로는 python-multiplatform #38 대기(#11) |
| planned계획 | TextField input-method (IME) handling (#10)TextField 입력기(IME) 처리(#10) |
| planned계획 | Icon, DefaultIcons (python-multiplatform #37), colour schemesIcon, DefaultIcons (python-multiplatform #37), 색 스킴 |
| planned계획 | remember_saveable, coroutine scopesremember_saveable, 코루틴 스코프 |
Test baseline: 91 passed, 73 skipped without a python-multiplatform checkout; 163 passed, 1 skipped with a current one. The one skip in both is the test that reads the maintainer's UI.ipynb.
테스트 기준선: python-multiplatform 체크아웃이 없으면 91 passed, 73 skipped, 최신 체크아웃이면 163 passed, 1 skipped. 두 경우 모두의 건너뜀 1 개는 메인테이너의 UI.ipynb 를 읽는 테스트입니다.
Test baseline: 106 passed, 75 skipped without a python-multiplatform checkout; 178 passed, 3 skipped with a current one. Of the skips with a current checkout, one reads the maintainer's UI.ipynb and two need python-multiplatform #38.
테스트 기준선: python-multiplatform 체크아웃이 없으면 91 passed, 73 skipped, 최신 체크아웃이면 163 passed, 1 skipped. 최신 체크아웃에서 건너뛰는 3 개 중 1 개는 메인테이너의 UI.ipynb 를 읽는 테스트이고 2 개는 python-multiplatform #38 이 필요합니다.