diff --git a/PROJECT.md b/PROJECT.md index f06716e..38c78da 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -28,7 +28,7 @@ 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 메서드의 snake_case 이름과 키워드 인자(`m.padding(padding_values=...)`; python-multiplatform #131 이 바인더 쪽에서 제공하므로 #131 이상 필요; Kotlin 이름도 실행 시 그대로 통하고 스텁은 snake_case 만), 함수이기도 한 Kotlin object 의 호출(`Color(0xFFFFFFFF)` 과 `Color.Red` 둘 다; #88, 설치한 wheel 에서도 테스트됨. Int/Long 오버로드 선택은 python-multiplatform #146) | -| 부분 | 선언형 앱 루트 `@app`·`state`·`app_root`(#11; 로직은 테스트됨, 바인더 경로는 #38 모양의 가짜 호스트로 테스트됨, 실제 Compose 증거는 E2E 모듈 #19; 숫자·문자열 상태는 python-multiplatform #69 부터 왕복됨), 배포 설정, Material 3 위젯(렌더 증거는 python-multiplatform 에만), `Icon`(import 만 됨)·`DefaultIcons`(`Icons.Default` alias, 가짜 호스트로만 테스트됨, 스텁에서는 `DefaultIcons.Add` 가 `ImageVector`; `DefaultIcons.Add` 로 쓴다; 노트북의 `DefaultIcons.Add()` 가 아님), `TextField(state=...)`·`TextFieldState`(#10; `pythonx.compose.foundation.text.input`, 가짜 호스트로 테스트됨, 노트북의 `text_state=`/`padding=` 는 `state=`/`modifier=Modifier.padding(8)`; IME 조합 증거는 python-multiplatform E2E #26) | +| 부분 | 선언형 앱 루트 `@app`·`state`·`app_root`(#11; 로직은 테스트됨, 바인더 경로는 #38 모양의 가짜 호스트로 테스트됨, 실제 Compose 증거는 python-multiplatform 노트북 E2E #26 에서 통과; 숫자·문자열 상태는 python-multiplatform #69 부터 왕복됨), 배포 설정, Material 3 위젯(렌더 증거는 python-multiplatform 노트북 E2E 에만, 설치한 wheel 에서 Kotlin 과 픽셀 일치), `Icon`(import 만 됨)·`DefaultIcons`(`Icons.Default` alias, 가짜 호스트로만 테스트됨, 스텁에서는 `DefaultIcons.Add` 가 `ImageVector`; `DefaultIcons.Add` 로 쓴다; 노트북의 `DefaultIcons.Add()` 가 아님), `TextField(state=...)`·`TextFieldState`(#10; `pythonx.compose.foundation.text.input`, 가짜 호스트로 테스트됨, 노트북의 `text_state=`/`padding=` 는 `state=`/`modifier=Modifier.padding(8)`; IME 조합 증거는 python-multiplatform 노트북 E2E #26 에서 통과) | | 계획 | `remember_saveable`·색 스킴·코루틴 스코프 | ### 테스트 기준선 @@ -61,7 +61,7 @@ GitHub 마일스톤과 같은 내용이다. 날짜는 2026-10-03 에 정했고, 날짜 근거: - **M1 (2주):** 바인더 계약(`inspect.signature`, `python_multiplatform.describe`)이 PythonMultiplatform `d00f413f` 에 착지했다. 남은 일은 이 저장소의 순수 Python 과 가짜 호스트 하네스뿐이라 JVM 빌드가 필요 없다. -- **M2 (M1 뒤 3주):** 위젯마다 JVM 렌더 테스트가 필요하다. 처음엔 Gradle 캐시 링크가 끊겼다고 보고 4주로 잡았으나, 캐시는 `PythonMultiplatform/.caches` 로 다시 걸려 Compose 픽스처가 통과하고 있었고, 노트북 관련 질문도 INTENT §5 로 결정됐다. 남은 위험은 `TextField(state=...)` 의 IME 조합 증거(python-multiplatform E2E #26)와, 앱 루트 교체를 Kotlin 컴포지션이 관찰하는 진입점이다. 진입점은 바인더 몫으로 정해졌다(2026-10-04): python-multiplatform 의 `PythonAppView` 가 `app_root` State 를 컴포지션 안에서 읽고, #26 진단에서 다시 선언한 루트가 실제로 다시 그려짐을 확인했다. +- **M2 (M1 뒤 3주):** 위젯마다 JVM 렌더 테스트가 필요하다. 처음엔 Gradle 캐시 링크가 끊겼다고 보고 4주로 잡았으나, 캐시는 `PythonMultiplatform/.caches` 로 다시 걸려 Compose 픽스처가 통과하고 있었고, 노트북 관련 질문도 INTENT §5 로 결정됐다. 남은 위험이던 `TextField(state=...)` 의 IME 조합 증거와 앱 루트 교체 진입점은 python-multiplatform 노트북 E2E 로 해소됐다(#26, develop `844cf291`; 2026-10-04 에 이 패키지의 `develop` wheel 로 20개 중 20개 통과, 아직 CI 는 돌리지 않음). 그래서 #9·#10·#11·#19 를 닫았다. 진입점은 바인더 몫으로 정해졌다(2026-10-04): python-multiplatform 의 `PythonAppView` 가 `app_root` State 를 컴포지션 안에서 읽고, #26 진단에서 다시 선언한 루트가 실제로 다시 그려짐을 확인했다. - **M3 (M2 뒤 2주):** 스텁은 M1 규칙만 있으면 만들 수 있어 M2 와 겹쳐 진행할 수 있다. 다만 노트북 위젯의 최종 이름이 M2 에서 정해지므로 마감은 M2 뒤로 둔다. ## 4. 구조 diff --git a/docs/SPEC.md b/docs/SPEC.md index 2f3112e..0f309f0 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -284,7 +284,7 @@ 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` (the binder path is tested against a fake host shaped after python-multiplatform #38; real Compose is the E2E module, issue #19) +### S5.4 The app root, `app` and `state`: `partial` (the binder path is tested here against a fake host shaped after python-multiplatform #38; the real-Compose evidence is outside this repository, in the notebook E2E module) The host draws with `PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root")` (python-multiplatform #18 and #105; issue #11). The binder owns the redraw: `PythonAppView` reads @@ -315,7 +315,10 @@ reason. `pythonx` does none of this itself. Tests: `tests/test_app_root.py::TheL real `_new_state` through the binder's Python layer and `tests/fake_host.py`, whose `mutableStateOf` and `MutableState.value` rows are shaped after #38 (`KotlinSurface.kt`, `PythonxAdapter.kt`), not walked from a jar. It skips against a binder older than #38. That Compose observes a write and -recomposes is not shown here; it is the E2E module (python-multiplatform #26, issue #19). The stub carries `app`, +recomposes is not shown here. It is shown in python-multiplatform `ksp-fixtures/notebook-e2e` (#26, develop `844cf291`; 20 of 20 passed against this package's `develop` wheel on 2026-10-04, not run here, and no CI workflow runs it yet): `NotebookRootTest` declares the root, shares one state between the +notebook and the screen, and redeclares the root with `@app`, which reaches the screen within four +frames with no update call; with `@app` stubbed out, or the host handed the root once, the screen +does not change. The stub carries `app`, `state` and `app_root` (`tests/test_stubs.py`). ### S5.2 Material 3 composables reach Python without per-widget wrappers: `partial` @@ -331,8 +334,10 @@ toggle family, `TextField`, `Checkbox` and `Switch` are meant to be reached thro (commits `a6742a1c`, `3fde8bd6`, `cac8243f`, cited in `pythonx/compose/material3/__init__.py`) and was not re-run for this document. - `pythonx.compose.material3` re-exports `androidx.compose.material3` by the §3 rule, but the fake - host binds no material3 declaration, so no test here exercises one; the render proof per widget - is issue #9. + host binds no material3 declaration, so no test here exercises one. +- The render proof per widget is in python-multiplatform `ksp-fixtures/notebook-e2e` (#26, develop `844cf291`; 20 of 20 passed against this package's `develop` wheel on 2026-10-04, not run here, and no CI workflow runs it yet): `NotebookPracticeTest` (13 tests) draws `Text`, + `Button`, `Card`, `Icon`, `Column`, `Row` (with an `Arrangement` and an `Alignment.Horizontal`) + and `Spacer` from the installed wheel, each pixel-equal to the same composables drawn from Kotlin. ### S5.3 `DefaultIcons`, `Icon` and colour schemes: `partial` @@ -346,7 +351,7 @@ the notebook's `DefaultIcons.Add()` would call an `ImageVector`, which is not ca says the intent is `DefaultIcons.Add`. The rows are shaped after python-multiplatform `31c092f0` (`_PROPERTIES`, `_read_property`) and the ecosystem report for #37; no walked `material-icons-core` row exists here, and `Icon(DefaultIcons.Add, ...)` is drawn only in -python-multiplatform's render test. Tests: `tests/test_chain.py::DefaultIconsAlias`; stubs: the +python-multiplatform's render test and its notebook E2E (cells 28 to 30). Tests: `tests/test_chain.py::DefaultIconsAlias`; stubs: the `material3` stub assigns `DefaultIcons = Icons.Default` (typed `Icons.Filled`; `Add` is an `ImageVector`, python-multiplatform #68, whose package `androidx.compose.ui.graphics.vector` is the manifest row `pythonx.compose.ui.graphics.vector`), checked by `tests/test_typing.py::test_default_icons_imports_from_material3` and `test_a_default_icon_is_an_image_vector` @@ -391,7 +396,10 @@ Status `partial`: this is wired and tested against the fake host, whose rows are python-multiplatform `26485a02` (#73), not a walked jar (`tests/fake_host.py` says which fields). The proof that matters, typing a composing input-method sequence with no Python callback per keystroke and the field's text asserted afterwards, is python-multiplatform's E2E test (#26), judged over up to -four frames; it is not run here. Not modelled: a function-typed slot (`label`, `on_value_change`), which needs +four frames. It passes there (`NotebookTextFieldTest`; python-multiplatform `ksp-fixtures/notebook-e2e` (#26, develop `844cf291`; 20 of 20 passed against this package's `develop` wheel on 2026-10-04, not run here, and no CI workflow runs it yet)): a Hangul sequence +keeps its composing range in Compose, no Python function starts while it types, and the notebook +reads the committed text. Not exercised there: the AWT layer that decodes an `InputMethodEvent`, +which needs a window. Not modelled here: a function-typed slot (`label`, `on_value_change`), which needs the binder's `NewFunction` rows. ## 6. Modifiers: extension functions as methods @@ -532,10 +540,10 @@ the binder lists no member that would win over these groups. | `modifier` | lower-case instance from `pythonx.compose.ui` | INTENT §5.4: not provided; written `Modifier` | | `color=0xFFFF0000` | ARGB integer for a colour | INTENT §5.5: not accepted; written `Color(0xFFFF0000)` | | `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) | -| `TextField(text_state=..., padding=8)` | state object and spacing parameter | INTENT §5.8: written `TextField(state=..., modifier=Modifier.padding(8))`; S5.5, `partial` (#10) | +| `Column`, `Row`, `Spacer` | imported from `pythonx.compose.material3` | INTENT §5.2: served from both, by the manifest's `[aliases]` (`tests/test_chain.py::ManifestAliases`); rendered from the installed wheel in python-multiplatform's notebook E2E (#9, S5.2) | +| `TextField(text_state=..., padding=8)` | state object and spacing parameter | INTENT §5.8: written `TextField(state=..., modifier=Modifier.padding(8))`; S5.5, `partial`: IME evidence only in python-multiplatform's notebook E2E (#10) | | `Card`, `Button`, `Text`, `TextField` | as above | S5.2 | -| `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) | +| `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`: real-Compose evidence only in python-multiplatform's notebook E2E, #11) | ## 9. Repository hygiene (not behaviour)