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
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,9 @@ worktree so a test can see it. Read it in place.
`.tmp/` (rule 2), not in the home directory.
- Tests that exercise the binder's adaptation layer read it, read-only, from a `PythonMultiplatform`
checkout: the sibling directory `../PythonMultiplatform`, or `PYTHONMULTIPLATFORM_HOME`. Without
one they **skip**; with a checkout older than python-multiplatform `ba4c6f49`, 5 of them skip for
want of its member resolver (see `docs/SPEC.md` §0). Report both numbers and say which environment you ran in. A skip is not
one they **skip**; against an older python-multiplatform some still skip, each saying which binder
feature it needs (the member resolver, `ba4c6f49`; `describe(module, name)`, #36; see
`docs/SPEC.md` §0). Report both numbers and say which environment you ran in. A skip is not
a pass.
- The suite runs on GitHub Actions (`.github/workflows/tests.yml`) in both environments, without and
with a python-multiplatform checkout, for every pull request into `develop`. Local runs are for
Expand Down
16 changes: 9 additions & 7 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,24 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **`

| 상태 | 항목 |
|---|---|
| 구현 | 매핑 매니페스트 `pythonx-map.toml`, `@Composable` 항등 데코레이터, 실제 디스크 패키지 `pythonx` 와 모듈 수준 규칙 기반 재노출(snake_case 이름·키워드·시그니처, 오버로드 디스패치, 값 클래스 허용 목록), 실제 Compose 1.11.1 에서 생성한 `.pyi` 스텁과 `py.typed`(wheel 포함, mypy 로 검사) |
| 부분 | 배포 설정, proxy 메서드의 키워드 인자는 아직 Kotlin 이름(`m.padding(paddingValues=...)`), Material 3 위젯(렌더 증거는 python-multiplatform 에만), `Icon`·색 스킴(import 만 됨), `Alignment`/`Arrangement` 상수(`pythonx` 에서 읽힘; 묶음 표기는 python-multiplatform #36 대기) |
| 구현 | 매핑 매니페스트 `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`·코루틴 스코프 |

### 테스트 기준선

```
python3 -m pytest tests -q
PythonMultiplatform 체크아웃 없음 84 passed, 60 skipped
python-multiplatform develop ba4c6f49+ 143 passed, 1 skipped
그보다 오래된 체크아웃 같은 수, 그중 5 개 건너뜀 (member resolver 없음)
PythonMultiplatform 체크아웃 없음 91 passed, 73 skipped
python-multiplatform develop d6d39787+ 163 passed, 1 skipped
describe(module, name) 없는 체크아웃 같은 수, 그중 13 개 건너뜀 (python-multiplatform #36 이전)
그보다 오래된 체크아웃 거기에 5 개 더 건너뜀 (member resolver 없음)
```

`describe(module, name)`(python-multiplatform #36)이 없는 바인더에서는 묶음 상수를 확인하는 13 개가,
member resolver(python-multiplatform `ba4c6f49`)가 없는 바인더에서는 proxy 의 snake_case 메서드를 부르는
5 개가 건너뛴다. 두 환경 모두의 건너뜀 1 개는 worktree 에 없는 `UI.ipynb` 의 테스트다. 건너뜀은 통과가 아니다.
체크아웃 없이 통과하는 84 개 중 30 개는 타입 스텁과 wheel 을 검사하고(SPEC S1.2), 나머지 다수는 *부재*(옛 토큰·삭제된
5 개가 더 건너뛴다. 두 환경 모두의 건너뜀 1 개는 worktree 에 없는 `UI.ipynb` 의 테스트다. 건너뜀은 통과가 아니다.
체크아웃 없이 통과하는 91 개 중 37 개는 타입 스텁과 wheel 을 검사하고(SPEC S1.2), 나머지 다수는 *부재*(옛 토큰·삭제된
파일이 없음)를 확인하는 것이라 기능 진척으로 세지 않는다.

### 마일스톤
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ proofs are still pending. See [Status](#-status).</sub>
exactly as it does in Kotlin. Keyword arguments to such a method are still Kotlin's own
(`m.padding(paddingValues=...)`); the module function takes `padding(m, padding_values=...)`.
- **Kotlin objects are namespaces.** `Alignment.Center` and `Arrangement.End` are read without
parentheses; functions inside them are `snake_case`: `Arrangement.spaced_by(8)`.
parentheses; functions inside them are `snake_case`: `Arrangement.spaced_by(8)`. Constants can
also be reached grouped by their declared type, as the notebook writes them:
`Alignment.Horizontal.End` is `Alignment.End`.
- **`@Composable` stays.** Screens are decorated Python functions.
- **A real package.** `pythonx/` is ordinary Python source that imports `androidx.compose.*` and
reshapes it. The binder never renames anything — renaming is this package's job.
Expand Down Expand Up @@ -150,7 +152,7 @@ standalone desktop toolkit.
| `Column`, `Row`, `Spacer` from `material3` as well as `layout` | ✅ implemented and tested |
| Method keyword arguments in `snake_case` | 🟡 partial — module functions yes; methods still take Kotlin's keywords |
| The empty `Modifier` | 🟡 partial — `Modifier.padding(16)` from the class needs an app-supplied factory against real Compose |
| `Alignment` / `Arrangement` | 🟡 partial — `Alignment.Center`, `Arrangement.spaced_by(8)` work; grouped `Alignment.Horizontal.End` pending |
| `Alignment` / `Arrangement` | ✅ implemented and tested — `Alignment.Center`, `Arrangement.spaced_by(8)`, and grouped `Alignment.Horizontal.End` beside `Alignment.End` |
| 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` |
Expand Down
78 changes: 62 additions & 16 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +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 #12)
## 0. Test baseline (2026-10-03, after issue #9's grouped constants)

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 | **84 passed, 60 skipped**, 115 subtests passed |
| python-multiplatform `develop` at `ba4c6f49` or later (has `add_member_resolver`) | **143 passed, 1 skipped**, 139 subtests passed |
| an older checkout, without `add_member_resolver` | the same, with 5 more skipped |

Without a checkout, the 59 tests that install the binder's layers through `tests/adapter.py` skip.
Against a binder older than `ba4c6f49`, the 5 that call a snake_case method on a proxy the binder
returned skip, because that needs its member resolver (python-multiplatform #17). The 1 skip in both
| 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 |
| 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
pass.

Of the 84 that pass without a checkout, 30 check the type stubs and the wheel (S1.2); most of the
Of the 91 that pass without a checkout, 37 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 @@ -261,7 +263,7 @@ a name is adapted once and then lives in the module dict; `dir()` reports the Py
without the binding layer a name read says the host never installed it; dropping a proxy releases
its Kotlin handle. Evidence: `tests/test_chain.py::Laziness` (5 tests), `::Handles` (1 test).

## 7. Layout constants — `Alignment` and `Arrangement` — `partial`
## 7. Layout constants — `Alignment` and `Arrangement` — `implemented`

The bound constant names are documented in `pythonx/compose/ui/alignment.py` (15 `Alignment`
names) and `pythonx/compose/layout/arrangement.py` (8 `Arrangement` names), checked by
Expand All @@ -277,11 +279,55 @@ the Kotlin module does not list is tried as a Kotlin object sub-package and serv
`KotlinObject` namespace by the §3 rule. Constants keep their Kotlin spelling and are read again on
every access; a function inside the object is snake_case (`Arrangement.spaced_by`). Evidence:
`tests/test_chain.py::ObjectNamespaces` (6 tests); without the sub-package step 6 fail. The binder
will list these objects itself (python-multiplatform #35).

Decided (INTENT §5.3) and not built: the notebook's grouping by type, `Alignment.Horizontal.End`.
It needs a constant's declared type without reading it, `python_multiplatform.describe(module,
name)` (python-multiplatform #36; issue #9).
now lists these objects itself (python-multiplatform #35), and either way reaches them.

### S7.1 Grouped constants — `Alignment.Horizontal.End` — `implemented`

INTENT §5.3: the notebook groups constants by type, `Alignment.Horizontal.End`, and Kotlin writes
them flat, `Alignment.End` (which *is* an `Alignment.Horizontal`). Both spellings are served, by one
rule that names no declaration:

- Inside the `KotlinObject` for a Kotlin object `P`, an upper-case name `G` that is **not** a member
of `P` but is the simple name of a type nested in `P` -- some constant of `P` is declared as
exactly `P.G` -- is a **group**: a namespace whose members are exactly the constants of `P` whose
declared type is `P.G`. `Alignment.Horizontal` holds `CenterHorizontally`, `End`, `Start`;
`Alignment.Vertical` holds `Bottom`, `CenterVertically`, `Top`; `Arrangement.HorizontalOrVertical`
holds `Center`, `SpaceAround`, `SpaceBetween`, `SpaceEvenly`.
- A constant's declared type is read with `python_multiplatform.describe(module, name)`
(python-multiplatform #36), which answers from the binder's table and never runs the getter. A
constant's value is never read to classify it. The classification is metadata and is computed
once per object; values are not cached.
- Reading a group member reads that constant then and there, exactly as the flat spelling does:
`Alignment.Horizontal.End` and `Alignment.End` are the same read of the same Kotlin getter.
- **A group holds a constant only under its exact declared type.** A constant declared as a
supertype is in no narrower group: `Alignment.Center` (declared `Alignment`) is in none, and
`Arrangement.SpaceBetween` (declared `Arrangement.HorizontalOrVertical`) is in
`Arrangement.HorizontalOrVertical` only, not in `Arrangement.Horizontal`, although Kotlin accepts
it wherever an `Arrangement.Horizontal` is expected. The notebook groups by declared type, and the
declared type is what the binder describes; subtyping is not modelled.
- **Kotlin members win.** If `P` has a member named `G` (a constant, a function, a nested object
the binder lists), that member is what `P.G` reads, and there is no group of that name.
- `dir()` of an object lists its group names next to its members; `dir()` of a group lists its
constants. A name a group does not hold raises `AttributeError` naming the group's Kotlin type
(`androidx.compose.ui.Alignment.Horizontal`), so `Alignment.Horizontal.Top` fails: `Top` is an
`Alignment.Vertical`.
- On a binder without `describe(module, name)` there are no groups; the flat spelling is unchanged.
- **Stubs.** `tools/gen_stubs.py` emits each group as a class nested in the object's stub class,
holding the same `ClassVar` constants, grouped by the declared type the upstream stub's docstring
carries (`"""Kotlin: androidx.compose.ui.Alignment.End(): androidx.compose.ui.Alignment.Horizontal"""`),
so a type checker accepts `Alignment.Horizontal.End` and rejects `Alignment.Horizontal.Top`. A
reference to the Kotlin type `Alignment.Horizontal` in a signature stays `typing.Any`: the group
class is a namespace, not the type of the constants.

Evidence: `tests/test_chain.py::GroupedObjectConstants` (12 tests, runtime, against the fake host;
with the group lookup disabled 11 fail, and the one that stays green checks the binder without
`describe(module, name)`), `tests/test_stubs.py::TheConstantGroups` (5 tests, generator),
`::TheCommittedStubs::test_the_committed_alignment_and_arrangement_carry_their_groups`,
`::TheStubMatchesTheRuntime::test_a_group_in_the_stub_holds_what_the_runtime_group_holds`, and
`tests/test_typing.py::TheStubsTypeCheck::test_a_grouped_constant_type_checks` (mypy). In the real
`kotlin-stubs` artefact only `Alignment` and `Arrangement` have such groups. Nothing in the walked
Compose surface is an extension receiver named `Alignment.Horizontal` or `Arrangement.Horizontal`, so
the binder lists no member that would win over these groups.

## 8. The notebook surface not yet covered — `planned`

Expand Down Expand Up @@ -330,7 +376,7 @@ Found in the repository; not covered by `docs/INTENT.md`, or in conflict with it
`pythonx.compose.foundation.layout` (plus `pythonx.compose.foundation` and `pythonx.compose`)
"while the spelling settles".
5. *(Resolved, #31.)* `material3/icon.py` and `color_scheme.py`, dead reflection code, are deleted.
6. *(Decided, INTENT §5.3.)* Grouped alignment constants: both spellings are to be served; the grouped one is not built yet (§7).
6. *(Resolved, INTENT §5.3, issue #9.)* Grouped alignment constants: both spellings are served (§7, S7.1).
7. The submodule `pythonx/compose/native` → `thisisthepy/swing-graalvm-demo`, which INTENT does not
mention. (The 29 empty `material3/*.py` files are deleted, #31.)
8. **The `test/` directory** — a 2023–2024 Kotlin Multiplatform sample (`pycomposeui`, chaquopy
Expand Down
5 changes: 3 additions & 2 deletions docs/guide/concepts.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ <h2><span data-lang="en">Kotlin objects</span><span data-lang="ko">Kotlin 객체

Alignment.Center
Arrangement.End
Arrangement.spaced_by(8)</code></pre>
<p data-lang="en">The notebook's grouping by type, <code>Alignment.Horizontal.End</code>, is decided but not built yet; write <code>Alignment.End</code>.</p><p data-lang="ko">노트북의 타입별 묶음 표기 <code>Alignment.Horizontal.End</code> 는 결정되었지만 아직 만들어지지 않았습니다. <code>Alignment.End</code> 로 쓰세요.</p>
Arrangement.spaced_by(8)
Alignment.Horizontal.End # the same constant as Alignment.End</code></pre>
<p data-lang="en">A constant can also be read grouped by its declared type, as the notebook writes it: <code>Alignment.Horizontal.End</code> is <code>Alignment.End</code>, read the same way. A group holds only the constants declared as exactly that type, so <code>Alignment.Center</code> (declared <code>Alignment</code>) is in no group, and <code>Arrangement.SpaceBetween</code> is under <code>Arrangement.HorizontalOrVertical</code>, not <code>Arrangement.Horizontal</code>.</p><p data-lang="ko">노트북처럼 상수를 선언된 타입별로 묶어서 읽을 수도 있습니다. <code>Alignment.Horizontal.End</code> 는 <code>Alignment.End</code> 이며 같은 방식으로 읽힙니다. 묶음에는 정확히 그 타입으로 선언된 상수만 들어가므로 <code>Alignment.Center</code>(선언 타입 <code>Alignment</code>)는 어느 묶음에도 없고, <code>Arrangement.SpaceBetween</code> 은 <code>Arrangement.Horizontal</code> 이 아니라 <code>Arrangement.HorizontalOrVertical</code> 아래에 있습니다.</p>
</section>

<section>
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/getting-started.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ <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>84 passed, 60 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>84 passed, 60 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>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>
<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>
</section>
Expand Down
Loading
Loading