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
8 changes: 4 additions & 4 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) |
| 부분 | 배포 설정, 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 없음)
```
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 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 |
Expand Down
40 changes: 32 additions & 8 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -335,15 +359,15 @@ 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` |
| `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) |
| `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)

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>91 passed, 73 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>91 passed, 73 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, 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>
<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>143 pass</strong> and only the <code>UI.ipynb</code> test skips; 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>143 개가 통과</strong>하고 <code>UI.ipynb</code> 테스트만 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 <strong>건너뜁니다</strong>. 예상된 상태이며 기록되어 있습니다.</p>
<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>
</section>

<section>
Expand Down
15 changes: 15 additions & 0 deletions docs/guide/guide-composables.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ <h2><span data-lang="en">Write a screen</span><span data-lang="ko">화면 하나
from pythonx.compose.layout import Column # Kotlin's home; same object</code></pre>
</section>

<section>
<h2><span data-lang="en">Declare the app and keep state</span><span data-lang="ko">앱 선언과 상태</span></h2>
<pre><code class="language-python">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, &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>
</section>

<section>
<h2><span data-lang="en">From the notebook's spelling to the real parameters</span><span data-lang="ko">노트북 표기에서 실제 매개변수로</span></h2>
<p data-lang="en">The notebook's signatures are examples. The surface follows Compose:</p><p data-lang="ko">노트북의 시그니처는 예시입니다. 실제 표면은 Compose 를 따릅니다:</p>
Expand Down
Loading
Loading