From 526adbe96f9a4159c1ea06fd9a0b52389d6c07c4 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sun, 4 Oct 2026 01:40:29 +0900 Subject: [PATCH] Refactor: Remove state() from pythonx.compose.runtime INTENT 5.11 (maintainer, 2026-10-04): Kotlin's mutable_state_of does the same job, so there is no second name and no alias. _new_state stays internal behind app_root. The stubs drop def state (regenerated; only those lines change). Tests and the guide use mutable_state_of; README, SPEC and the style example follow; the guide's stale 'app root planned' item goes. Red first: test_state_is_not_a_name_of_the_module, test_state_is_not_served_even_with_a_binder and the stub check failed before the change. Counts: 149/112 -> 149/113 without a checkout and 261 -> 262 with one (main checkout); worktree baselines 148/114, 261/1. Closes #101 --- PROJECT.md | 4 +-- README.md | 2 +- docs/SPEC.md | 21 ++++++++------ docs/guide/guide-composables.html | 9 +++--- docs/locale/README_ko.md | 2 +- docs/style/writing.md | 2 +- pythonx/compose/runtime/__init__.py | 22 ++++++--------- pythonx/compose/runtime/__init__.pyi | 3 -- tests/test_app_root.py | 42 +++++++++++++++++----------- tests/test_stubs.py | 2 +- 10 files changed, 56 insertions(+), 53 deletions(-) diff --git a/PROJECT.md b/PROJECT.md index 424419d..befc6a5 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 체크아웃 없음 148 passed, 113 skipped - python-multiplatform develop 31c092f0+ 260 passed, 1 skipped + PythonMultiplatform 체크아웃 없음 148 passed, 114 skipped + python-multiplatform develop 31c092f0+ 261 passed, 1 skipped describe_member 없는 체크아웃 같은 수, 그중 7 개 건너뜀 (python-multiplatform #54 이전) describe(module, name) 없는 체크아웃 같은 수, 그중 13 개 건너뜀 (python-multiplatform #36 이전) 그보다 오래된 체크아웃 거기에 5 개 더 건너뜀 (#131 이전, snake_case 이름 없음) diff --git a/README.md b/README.md index 52d19f0..9b90adb 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,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` (#12) | | Distribution (`pythonx-compose`) | 🟡 partial: the wheel carries the manifest, the re-export rule, the stubs and `py.typed` | -| Declared app root (`@app`) and Pythonic state (`state`), no update call | 🟡 partial: binder path tested against a fake host shaped after python-multiplatform #38; real Compose is the E2E module (#11, #19); numbers and strings round-trip through `state` | +| Declared app root (`@app`), no update call; state through Kotlin's `mutable_state_of` | 🟡 partial: binder path tested against a fake host shaped after python-multiplatform #38; against real Compose, python-multiplatform's notebook E2E passes (#11, #19); numbers and strings round-trip | | `TextField(state=...)` with `TextFieldState` (`pythonx.compose.foundation.text.input`) | 🟡 partial: tested against a fake host shaped after python-multiplatform #73; the input-method (IME) composing proof is python-multiplatform E2E #26 (#10) | | `DefaultIcons` (`Icons.Default`), written `DefaultIcons.Add` | 🟡 partial: tested against a fake host shaped after python-multiplatform #37/#38; `Icon(DefaultIcons.Add, …)` is drawn in python-multiplatform's render test | | Colour schemes | ⏳ planned | diff --git a/docs/SPEC.md b/docs/SPEC.md index a4589a7..44a0bdb 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -21,14 +21,14 @@ Run from a worktree with `uv run --with pytest --with mypy pytest tests -q -rs` | Environment | Result | |---|---| -| No `PythonMultiplatform` checkout found | **148 passed, 113 skipped**, 167 subtests passed | -| python-multiplatform `develop` at `31c092f0` or later (property rows, #38; `describe_member`, #54; besides `add_member_resolver` and `describe(module, name)`) | **260 passed, 1 skipped**, 198 subtests passed | +| No `PythonMultiplatform` checkout found | **148 passed, 114 skipped**, 164 subtests passed | +| python-multiplatform `develop` at `31c092f0` or later (property rows, #38; `describe_member`, #54; besides `add_member_resolver` and `describe(module, name)`) | **261 passed, 1 skipped**, 195 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 | Measured in a worktree, which has `.tmp/kotlin-stubs.zip` but no `UI.ipynb`. Without a checkout, the -112 tests that install the binder's layers through `tests/adapter.py` skip (one of them against the +113 tests that install the binder's layers through `tests/adapter.py` skip (one of them against the installed wheel, #88), plus the notebook test. Against a binder without `describe(module, name)`, the 13 that check grouped constants (§7, S7.1) skip. Against a binder before python-multiplatform #131, the tests that call a snake_case method or @@ -297,11 +297,14 @@ python-multiplatform #26 diagnosis). `pythonx.compose.runtime` provides: 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)` is **removed** (INTENT §5.11, issue #101; `planned` until that lands): Kotlin's - `mutable_state_of` does the same job. A screen keeps state with `remember_saveable` inside `App` - (S5.6) or with `mutable_state_of`. `_new_state` stays as the internal function behind `app_root`. - -Until #101 lands, both states come from one internal function, `_new_state`. When the binder cannot supply +- `state(initial)` is **removed** (INTENT §5.11, issue #101): Kotlin's `mutable_state_of` does the + same job. A screen keeps state with `remember_saveable` inside `App` (S5.6) or with + `mutable_state_of`. `_new_state` stays as the internal function behind `app_root`. Tests: + `tests/test_app_root.py::TheLogic::test_state_is_not_a_name_of_the_module`, + `::TheBinderPath::test_state_is_not_served_even_with_a_binder`, and the stub check in + `tests/test_stubs.py`. + +`app_root` comes 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. @@ -310,7 +313,7 @@ Evidence: python-multiplatform #38 (`31c092f0`) binds generic functions and prop setters, and an `Any?` slot carries a Python object as itself, so `mutableStateOf(x)` is callable and the returned `MutableState`'s `.value` reads and writes. Since python-multiplatform #69 (PR #81) the binder boxes a Python scalar for an `Any` slot (bool, int within 64 bits, float, str) and unboxes it -on read, so `state(0)` and `counter.value += 1` work; an int beyond 64 bits is refused with the +on read, so `mutable_state_of(0)` and `counter.value += 1` work; an int beyond 64 bits is refused with the reason. `pythonx` does none of this itself. Tests: `tests/test_app_root.py::TheLogic` (11) and `::TheStateFactory` (4) run against a test-only fake state patched in for `_new_state`; `::TheBinderPath` (5) uses the real `_new_state` through the binder's Python layer and `tests/fake_host.py`, whose `mutableStateOf` diff --git a/docs/guide/guide-composables.html b/docs/guide/guide-composables.html index a9346ea..4dc7c25 100644 --- a/docs/guide/guide-composables.html +++ b/docs/guide/guide-composables.html @@ -51,17 +51,17 @@
from pythonx.compose.runtime import app, state
+from pythonx.compose.runtime import app, mutable_state_of
from pythonx.compose.material3 import Text, Button
-count = state(0)
+count = mutable_state_of(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 PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root").
@app 은 호스트가 그리는 화면을 선언하고 함수를 그대로 돌려줍니다. 노트북 셀 등에서 다시 선언하면 화면이 바뀌며, 갱신 호출은 없습니다. state(initial) 는 Compose 상태이며 .value 로 읽고 씁니다. 호스트는 PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root") 로 한 번만 설정합니다.
-Status: partial. The binder path, which creates the state through Compose's mutableStateOf, is tested against a fake host shaped after python-multiplatform #38; proof against real Compose is still the E2E module. Numbers and strings round-trip: state(0) and count.value += 1 work (python-multiplatform #69).
상태: 부분. Compose 의 mutableStateOf 로 상태를 만드는 바인더 경로는 python-multiplatform #38 모양의 가짜 호스트로 테스트되었으며, 실제 Compose 증거는 아직 E2E 모듈입니다. 숫자와 문자열은 왕복됩니다: state(0) 과 count.value += 1 이 동작합니다(python-multiplatform #69).
+@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. mutable_state_of(initial) is Compose's own mutableStateOf, read and written through .value; there is no second name for it. The host is configured once with PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root").
@app 은 호스트가 그리는 화면을 선언하고 함수를 그대로 돌려줍니다. 노트북 셀 등에서 다시 선언하면 화면이 바뀌며, 갱신 호출은 없습니다. mutable_state_of(initial) 는 Compose 의 mutableStateOf 그대로이며 .value 로 읽고 씁니다. 다른 이름은 두지 않습니다. 호스트는 PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root") 로 한 번만 설정합니다.
+Status: partial. The binder path, which creates the state through Compose's mutableStateOf, is tested against a fake host shaped after python-multiplatform #38; against real Compose, python-multiplatform's notebook E2E passes. Numbers and strings round-trip: mutable_state_of(0) and count.value += 1 work (python-multiplatform #69).
상태: 부분. Compose 의 mutableStateOf 로 상태를 만드는 바인더 경로는 python-multiplatform #38 모양의 가짜 호스트로 테스트되었으며, 실제 Compose 에서는 python-multiplatform 의 노트북 E2E 가 통과합니다. 숫자와 문자열은 왕복됩니다: mutable_state_of(0) 과 count.value += 1 이 동작합니다(python-multiplatform #69).
DefaultIcons is Icons.Default, an icon is a value (an ImageVector to a type checker) read without parentheses: Icon(DefaultIcons.Add, content_description=None), not the notebook's DefaultIcons.Add(). It is tested here against a fake host only; the real draw is in python-multiplatform's render test.색 스킴 팩토리는 색 매개변수를 36 개 받아 아직 호출할 수 없습니다. DefaultIcons 는 Icons.Default 이고, 아이콘은 괄호 없이 읽는 값(타입 검사기에는 ImageVector)입니다. 노트북의 DefaultIcons.Add() 가 아니라 Icon(DefaultIcons.Add, content_description=None). 여기서는 가짜 호스트로만 테스트되며, 실제 그리기는 python-multiplatform 의 렌더 테스트에 있습니다.TextField(state=…) is wired and tested against a fake host only; that the input method's composing sequence survives, with no Python callback per keystroke, is proved in python-multiplatform's end-to-end test (#26).TextField(state=…) 는 가짜 호스트로만 테스트되었습니다. 입력기의 조합 입력이 키 입력마다 파이썬 콜백 없이 유지되는지는 python-multiplatform 의 종단 간 테스트(#26)에서 증명합니다.remember_saveable and the coroutine scopes the notebook imports are planned.노트북이 import 하는 remember_saveable, 코루틴 스코프는 계획 단계입니다.