Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
| 부분 | 선언형 앱 루트 `@app`·`state`·`app_root`(#11; 로직은 테스트됨, 바인더 경로는 python-multiplatform #38 대기), 배포 설정, proxy 메서드의 키워드 인자는 아직 Kotlin 이름(`m.padding(paddingValues=...)`), Material 3 위젯(렌더 증거는 python-multiplatform 에만), `Icon`·색 스킴(import 만 됨) |
| 부분 | 선언형 앱 루트 `@app`·`state`·`app_root`(#11; 로직은 테스트됨, 바인더 경로는 #38 모양의 가짜 호스트로 테스트됨, 실제 Compose 증거는 E2E 모듈 #19, 바인더가 `Any?` 칸의 int 를 거부하므로 `state(1)` 은 아직 TypeError), 배포 설정, proxy 메서드의 키워드 인자는 아직 Kotlin 이름(`m.padding(paddingValues=...)`), Material 3 위젯(렌더 증거는 python-multiplatform 에만), `Icon`·색 스킴(import 만 됨) |
| 계획 | `remember_saveable`·`DefaultIcons`·코루틴 스코프 |

### 테스트 기준선

```
python3 -m pytest tests -q
PythonMultiplatform 체크아웃 없음 106 passed, 75 skipped
python-multiplatform develop d6d39787+ 178 passed, 3 skipped
PythonMultiplatform 체크아웃 없음 106 passed, 78 skipped
python-multiplatform develop 31c092f0+ 183 passed, 1 skipped
describe(module, name) 없는 체크아웃 같은 수, 그중 13 개 건너뜀 (python-multiplatform #36 이전)
그보다 오래된 체크아웃 거기에 5 개 더 건너뜀 (member resolver 없음)
```
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`@app`) and Pythonic state (`state`), no update call | 🟡 partial — logic tested; the binder path waits for python-multiplatform #38 (#11) |
| 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); `state(1)` is refused by the binder (int in an `Any?` slot) |
| `TextField` input-method (IME) handling | ⏳ planned (#10) |
| `Icon`, `DefaultIcons`, colour schemes | ⏳ planned (python-multiplatform #37) |
| `remember_saveable`, coroutine scopes | ⏳ planned |
Expand Down
30 changes: 19 additions & 11 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ Run from a worktree with `python -m pytest tests -q -rs` (pytest 8, mypy 2.4, CP

| Environment | Result |
|---|---|
| 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 |
| No `PythonMultiplatform` checkout found | **106 passed, 78 skipped**, 122 subtests passed |
| python-multiplatform `develop` at `31c092f0` or later (has property rows, #38, besides `add_member_resolver` and `describe(module, name)`) | **183 passed, 1 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.
Without a checkout, the 75 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 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
binder returned skip as well, because that needs its member resolver (python-multiplatform #17). The skip against a current checkout is `UI.ipynb`'s (§2). A binder older than #38 skips the 5
`TheBinderPath` tests (S5.4). `tests/test_typing.py` skips where mypy is not installed. A skip is not a
pass.

Of the 106 that pass without a checkout, 38 check the type stubs and the wheel (S1.2); most of the
Expand Down Expand Up @@ -194,7 +194,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` (logic tested; the binder path waits for python-multiplatform #38)
### 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)

The host draws with `PythonContent("pythonx.compose.runtime", "app_root")` (python-multiplatform #18,
issue #11). `pythonx.compose.runtime` provides:
Expand All @@ -213,10 +213,18 @@ Both states come from one internal function, `_new_state`. When the binder canno
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`).
Evidence: python-multiplatform #38 (`31c092f0`) binds generic functions and property getters and
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. One consequence: the binder refuses an
`int` in an `Any?` slot (it would cross as an object handle), so `state(1)` raises `TypeError` today
and `pythonx` does not box it. python-multiplatform #69 makes the binder box scalars in an `Any`
slot (bool, int, float, str) and unbox them on read; when it lands this refusal and its test flip. 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`
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`,
`state` and `app_root` (`tests/test_stubs.py`).

### S5.2 Material 3 composables reach Python without per-widget wrappers — `partial`

Expand Down Expand Up @@ -359,7 +367,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: `state(initial)` read and written through `.value` (S5.4, `partial`, #38); keeping a value across recreation is not provided |
| `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`, #11; an `int` initial value is refused by the binder until python-multiplatform #69); 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` |
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/getting-started.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ <h2><span data-lang="en">1. Get the source</span><span data-lang="ko">1. 소스
<section>
<h2><span data-lang="en">2. Run the tests</span><span data-lang="ko">2. 테스트 실행</span></h2>
<pre><code class="language-bash">python3 -m pytest tests -q</code></pre>
<p data-lang="en">Expect <strong>106 passed, 75 skipped</strong>. One skipped test reads the maintainer's <code>UI.ipynb</code>, which a clone does not have; mypy type-checks the stubs. The other skipped tests drive the binder's adaptation layer and need a <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> checkout beside this one, or:</p><p data-lang="ko"><strong>106 passed, 75 skipped</strong> 가 정상입니다. 건너뛴 테스트 하나는 클론에는 없는 메인테이너의 <code>UI.ipynb</code> 를 읽으며, mypy 는 스텁을 타입 검사합니다. 나머지 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> 체크아웃이 있거나 다음이 필요합니다:</p>
<p data-lang="en">Expect <strong>106 passed, 78 skipped</strong>. One skipped test reads the maintainer's <code>UI.ipynb</code>, which a clone does not have; mypy type-checks the stubs. The other skipped tests drive the binder's adaptation layer and need a <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> checkout beside this one, or:</p><p data-lang="ko"><strong>106 passed, 78 skipped</strong> 가 정상입니다. 건너뛴 테스트 하나는 클론에는 없는 메인테이너의 <code>UI.ipynb</code> 를 읽으며, mypy 는 스텁을 타입 검사합니다. 나머지 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> 체크아웃이 있거나 다음이 필요합니다:</p>
<pre><code class="language-bash">PYTHONMULTIPLATFORM_HOME=/path/to/python-multiplatform python3 -m pytest tests -q</code></pre>
<p data-lang="en">With a current one (<code>ba4c6f49</code> or later) <strong>178 pass</strong> and 3 skip (the <code>UI.ipynb</code> 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.</p><p data-lang="ko">최신 체크아웃(<code>ba4c6f49</code> 이후)이면 <strong>178 개가 통과</strong>하고 3 개(<code>UI.ipynb</code> 테스트와 python-multiplatform #38 이 필요한 2 개)를 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 <strong>건너뜁니다</strong>. 예상된 상태이며 기록되어 있습니다.</p>
<p data-lang="en">With a current one (<code>31c092f0</code> or later) <strong>183 pass</strong> and 1 skips (the <code>UI.ipynb</code> test); an older one skips 5 that call a snake_case method on a proxy the binder returned. That is expected and recorded.</p><p data-lang="ko">최신 체크아웃(<code>31c092f0</code> 이후)이면 <strong>183 개가 통과</strong>하고 1 개(<code>UI.ipynb</code> 테스트)를 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 <strong>건너뜁니다</strong>. 예상된 상태이며 기록되어 있습니다.</p>
</section>

<section>
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/guide-composables.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ <h2><span data-lang="en">Declare the app and keep state</span><span data-lang="k
Button(on_click=lambda: setattr(count, &quot;value&quot;, count.value + 1),
content=lambda: Text(f&quot;Clicked {count.value}&quot;))</code></pre>
<p data-lang="en"><code>@app</code> 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. <code>state(initial)</code> is a Compose state, read and written through <code>.value</code>. The host is configured once with <code>PythonContent(&quot;pythonx.compose.runtime&quot;, &quot;app_root&quot;)</code>.</p><p data-lang="ko"><code>@app</code> 은 호스트가 그리는 화면을 선언하고 함수를 그대로 돌려줍니다. 노트북 셀 등에서 다시 선언하면 화면이 바뀌며, 갱신 호출은 없습니다. <code>state(initial)</code> 는 Compose 상태이며 <code>.value</code> 로 읽고 씁니다. 호스트는 <code>PythonContent(&quot;pythonx.compose.runtime&quot;, &quot;app_root&quot;)</code> 로 한 번만 설정합니다.</p>
<p data-lang="en"><strong>Status: partial.</strong> The logic is tested; the binder path, which creates the state through Compose's <code>mutableStateOf</code>, waits for python-multiplatform #38. Until then these calls raise a <code>RuntimeError</code> that says so.</p><p data-lang="ko"><strong>상태: 부분.</strong> 로직은 테스트되었습니다. Compose 의 <code>mutableStateOf</code> 로 상태를 만드는 바인더 경로는 python-multiplatform #38 을 기다립니다. 그때까지는 이를 알리는 <code>RuntimeError</code> 가 발생합니다.</p>
<p data-lang="en"><strong>Status: partial.</strong> The binder path, which creates the state through Compose's <code>mutableStateOf</code>, is tested against a fake host shaped after python-multiplatform #38; proof against real Compose is still the E2E module. The binder refuses an <code>int</code> in that slot, so <code>state(1)</code> raises a <code>TypeError</code> today.</p><p data-lang="ko"><strong>상태: 부분.</strong> Compose 의 <code>mutableStateOf</code> 로 상태를 만드는 바인더 경로는 python-multiplatform #38 모양의 가짜 호스트로 테스트되었으며, 실제 Compose 증거는 아직 E2E 모듈입니다. 바인더가 그 칸의 <code>int</code> 를 거부하므로 <code>state(1)</code> 은 현재 <code>TypeError</code> 를 냅니다.</p>
</section>

<section>
Expand Down
Loading
Loading