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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,8 @@ 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 one, 33 of them currently **fail** until the re-export lands (issue #8;
see `docs/SPEC.md` §0). Report both numbers and say which environment you ran in. A skip is not
one they **skip**; with one, 4 of them still skip until the binder's member resolver lands
(python-multiplatform #17; see `docs/SPEC.md` §0). Report both numbers and say which environment you ran in. A skip is not
a pass.
- `PythonMultiplatform` is read-only from this repository. Never write to it from here.
- Record the before and after counts of every change; a change that only touches documentation
Expand Down
19 changes: 9 additions & 10 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,27 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **`
노트북의 함수 시그니처는 **이름 예시**다. 실제 규칙은 "Kotlin 원래 매개변수를 `snake_case` 로
노출한다" 이다 (`onclick` 이 아니라 `on_click`).

## 3. 현황 (2026-10-02)
## 3. 현황 (2026-10-03)

`docs/SPEC.md` 기준. 구현 = 이 저장소의 코드 + 읽고 통과를 확인한 테스트가 있는 것.

| 상태 | 항목 |
|---|---|
| 구현 | 매핑 매니페스트 `pythonx-map.toml`, `@Composable` 항등 데코레이터 |
| 부분 | 배포 설정(스텁·`py.typed` 없음, 매니페스트가 패키지 밖), `Modifier` 체인·오버로드·`Dp` 숫자 허용(바인더 계층 대상 테스트가 실패 중), Material 3 위젯(렌더 증거는 python-multiplatform 에만), `Icon`·색 스킴(import 만 됨), `Alignment`/`Arrangement` 상수(괄호 없이 읽는 표기로 확정, `pythonx.*` 재노출 전) |
| 계획 | `pythonx` 를 실제 디스크 패키지로 재구성, `.pyi` 동봉, `remember_saveable`·`DefaultIcons`·코루틴 스코프 |
| 구현 | 매핑 매니페스트 `pythonx-map.toml`, `@Composable` 항등 데코레이터, 실제 디스크 패키지 `pythonx` 와 모듈 수준 규칙 기반 재노출(snake_case 이름·키워드·시그니처, 오버로드 디스패치, 값 클래스 허용 목록) |
| 부분 | 배포 설정(스텁·`py.typed` 없음, 매니페스트가 패키지 밖), proxy 위의 snake_case 확장 메서드(`m.fill_max_width()`, python-multiplatform #17 대기), Material 3 위젯(렌더 증거는 python-multiplatform 에만), `Icon`·색 스킴(import 만 됨), `Alignment`/`Arrangement` 상수(괄호 없이 읽음, 묶음 표기는 #9) |
| 계획 | `.pyi` 동봉, 선언형 앱 루트(#11), `remember_saveable`·`DefaultIcons`·코루틴 스코프 |

### 테스트 기준선

```
python3 -m pytest tests -q
PythonMultiplatform 체크아웃 없음 40 passed, 37 skipped
PYTHONMULTIPLATFORM_HOME 지정 44 passed, 33 failed (재노출 전, #8)
PythonMultiplatform 체크아웃 없음 40 passed, 45 skipped
PYTHONMULTIPLATFORM_HOME 지정 81 passed, 4 skipped (4 개는 python-multiplatform #17 대기)
```

33 개 실패는 **예상된 것**이다. `test_chain.py`·`test_modifier_module.py` 가 바인더의 Kotlin 이름
표면을 `pythonx` 처럼 다루는 옛 하네스 호출을 쓰고 있어서, 이 패키지의 재노출 규칙(#8)에 맞춰 다시
써야 한다. 그래서 해당 항목은 "부분" 으로 기록한다.
통과하는 40 개 중 다수는 *부재*(옛 토큰·삭제된 파일이 없음)를 확인하는 것이라 기능 진척으로 세지
건너뛰는 4 개는 바인더가 돌려준 proxy 위의 camelCase 확장을 snake_case 이름으로 부르는 테스트다.
바인더의 member resolver(python-multiplatform #17)가 들어오면 실행된다. 건너뜀은 통과가 아니다.
체크아웃 없이 통과하는 40 개 중 다수는 *부재*(옛 토큰·삭제된 파일이 없음)를 확인하는 것이라 기능 진척으로 세지
않는다.

### 마일스톤
Expand Down
97 changes: 57 additions & 40 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,20 @@ 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)
## 0. Test baseline (2026-10-03, after issue #8)

Run from a worktree with `python3 -m pytest tests -q` (pytest 8, CPython 3.13):

| Environment | Result |
|---|---|
| No `PythonMultiplatform` checkout found | **40 passed, 37 skipped**, 44 subtests passed |
| `PYTHONMULTIPLATFORM_HOME` → the `PythonMultiplatform` checkout | **44 passed, 33 failed**, 44 subtests passed |

Without a checkout, the 37 tests that install the binder's layers through `tests/adapter.py` skip.
With one, 4 of them pass (`test_runtime_module.py::TheModuleIsTheFileOnDisk`: 2,
`test_ui_init_module.py::TheUiInitModuleIsTheFileOnDisk`: 2) and 33 fail (`test_chain.py`: 28,
`test_modifier_module.py::TheModifierSeam`: 5). The 33 still call the old harness signature and
exercise the binder's Kotlin-named surface as if it were `pythonx`; they are rewritten against this
package's re-export rule (issue #8). That failure is expected and is why the items below that depend
on those tests are `partial`, not `implemented`.

Of the 40 that pass, most assert **absence** (a retired token, a deleted file, a docstring that
| No `PythonMultiplatform` checkout found | **40 passed, 45 skipped**, 44 subtests passed |
| `PYTHONMULTIPLATFORM_HOME` → the `PythonMultiplatform` checkout | **81 passed, 4 skipped**, 44 subtests passed |

Without a checkout, the 45 tests that install the binder's layers through `tests/adapter.py` skip.
With one, all pass except 4 that skip because they call a camelCase extension on a binder proxy by
its snake_case name, which needs python-multiplatform #17 (§3). A skip is not a pass.

Of the 40 that pass without a checkout, most 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 @@ -82,34 +78,48 @@ notebook is git-ignored, so in a worktree or CI checkout it is absent, the expec
and **the test passes without checking anything.** It is only meaningful in the maintainer's main
checkout.

## 3. `pythonx` is a real package — `planned`
## 3. `pythonx` is a real package — `implemented` (module level)

Required by INTENT §2.2: `import pythonx.compose.material3` (and every other mapped module) loads
**this repository's files on disk**, and those files import the binder-exposed
`androidx.compose.*` modules and present them Pythonically.

Half of it holds. `import pythonx.compose.runtime` and `import pythonx.compose.ui` load this
repository's files with the binder installed (`TheModuleIsTheFileOnDisk`, `TheUiInitModuleIsTheFileOnDisk`),
because PythonMultiplatform `d00f413f` moved its layer to `python_multiplatform.binding`. The other
half has not landed: no on-disk module re-exports anything from `androidx.compose.*` yet (issue #8).
Every module in `pythonx-map.toml` is a file on disk whose `__init__.py` hands its name to one rule,
`pythonx/compose/_reexport.py` (issue #8). A name read from it resolves, on first use, against the
Kotlin package the manifest maps it to: upper-case names keep their Kotlin spelling, every other
name is reached by its snake_case spelling only, keywords are matched to the declaration's own
parameter names, `inspect.signature` reports snake_case names, and the manifest's value-class
allowlist is handed to the binder. Names the package defines itself (`runtime.Composable`) win.

Evidence: `tests/test_runtime_module.py::TheModuleIsTheFileOnDisk`,
`tests/test_ui_init_module.py::TheUiInitModuleIsTheFileOnDisk`, and `tests/test_chain.py`
(`Names`, `Laziness`, `OverloadDispatch::test_the_module_function_dispatches_with_snake_case_keywords`,
`ValueClasses::test_the_manifest_allowlist_is_what_lets_a_number_through`), against the binder's
real Python and the fake host. Disabling the name rule fails 15 tests, the keyword mapping 1, the
allowlist priming 12.

Not covered by this: **methods on a proxy the binder returned** (`m.fill_max_width()`). Those are
attributes of the binder's own class, which this package can only reach through the member-resolver
hook python-multiplatform #17 adds; until then a camelCase extension is reachable on a proxy only by
its Kotlin name, and four tests skip (S4.1, S6.1, S6.4).

## 4. Naming

### S4.1 Kotlin parameters, `snake_case` — `partial`

`onClick` → `on_click`, `fillMaxWidth` → `fill_max_width`, `zIndex` → `z_index`,
`toURLString` → `to_url_string`; type names (`Modifier`) unchanged; a name the reverse rule cannot
invert still resolves.
`toURLString` → `to_url_string`; type names (`Modifier`) unchanged; an explicit overload keeps its
type suffix (`padding__Dp_Dp`); a name the reverse rule cannot invert still resolves, because the
rule is only ever applied forward to the module's real names; the camelCase spelling is not a second
name.

Evidence: `tests/test_chain.py::Names` (4 tests). These exercise the conversion in the binder's
adaptation layer through `tests/adapter.py` and are currently skipped or failing (§0). No code in
this repository performs the conversion.
Implemented for module-level names and keywords (`tests/test_chain.py::Names`, 8 tests). Partial
because receiver methods on a binder proxy need python-multiplatform #17 (§3, issue #20).

### S4.2 The notebook's spellings are examples, not the contract — `implemented` as a rule

Where `UI.ipynb` writes `onclick`, the surface is `on_click`
(`tests/test_chain.py::Names::test_on_click_not_onclick` records the decision; it is subject to the
same failure as S4.1).
(`tests/test_chain.py::Names::test_on_click_not_onclick` records the decision).

## 5. Composables

Expand All @@ -120,7 +130,7 @@ given, which still runs and keeps its name and docstring. Composer threading is
job.

Tests: `tests/test_runtime_module.py::TheRuntimeSeam` (4 tests) and `::TheChaquopyMechanismIsGone`
(3 tests). The module is loaded **by file path** in these tests, not by `import`, because of §3.
(3 tests), importing `pythonx.compose.runtime` the ordinary way.

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

Expand All @@ -134,7 +144,9 @@ toggle family, `TextField`, `Checkbox` and `Switch` are meant to be reached thro
- The evidence that the widgets actually render and deliver callbacks is in `python-multiplatform`
(commits `a6742a1c`, `3fde8bd6`, `cac8243f`, cited in `pythonx/compose/material3/__init__.py`)
and was not re-run for this document.
- Nothing in this repository yet re-exports them under `pythonx.compose.material3` (§3).
- `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.

### S5.3 `Icon` and colour schemes — `partial`

Expand All @@ -148,13 +160,14 @@ the two colour-scheme factories take 36 `Color` parameters. The notebook uses `I

### S6.1 `Modifier` extensions are methods on the receiver proxy — `partial`

`Modifier.padding(16).size(24)` chains; each link returns a new receiver; a method is attached to
the proxy type once; an unbound name raises `AttributeError` naming where it looked; the
class-object spelling without a registered empty factory raises `TypeError` naming
`register_empty`.
`Modifier.padding(16).size(24)` chains; each link returns a new receiver; an unbound name raises
`AttributeError` naming where it looked; the class-object spelling without a registered empty
factory raises `TypeError` naming `register_empty`; `pythonx.compose.ui.Modifier` is the binder's
proxy class itself.

Evidence: `tests/test_chain.py::TheChain` (6 tests) and
`tests/test_modifier_module.py::TheModifierSeam` (5 tests) — currently skipped or failing (§0).
Evidence: `tests/test_chain.py::TheChain` (6 tests, 1 skipped) and
`tests/test_modifier_module.py::TheModifierSeam` (5 tests). Partial because a camelCase extension
(`fill_max_width`) on a proxy waits for python-multiplatform #17.

### S6.2 The empty-`Modifier` seam — `partial`

Expand All @@ -166,24 +179,28 @@ one-line Kotlin factory. The instance spelling (`m.padding(16)` on a `Modifier`
does not need it. `tests/test_modifier_module.py::TheShellIsGone` (3 tests, passing) checks the old
hand-written shell is gone.

### S6.3 Overload dispatch — `partial`
### S6.3 Overload dispatch — `implemented`

Among Kotlin overloads of one name, a call is dispatched by keyword name, argument count, then
declared type; a non-match names the candidates; an explicit overload spelling (`padding__Dp`)
bypasses dispatch. Evidence: `tests/test_chain.py::OverloadDispatch` (6 tests), skipped/failing.
bypasses dispatch; the module function takes snake_case keywords. Evidence:
`tests/test_chain.py::OverloadDispatch` (7 tests). Status: `implemented`.

### S6.4 Value classes — `partial`

A raw number is accepted for a `Dp` parameter; a `Dp` value is accepted too; a plain `Float`
parameter is not treated as a value class; a packed value class (`TextUnit`) refuses a raw number and
says why; the allow-list can be extended at run time. The allow-list itself is the manifest's
(S2, implemented). Runtime evidence: `tests/test_chain.py::ValueClasses` (5 tests), skipped/failing.
(S2, implemented) and reaches the binder when a `pythonx.compose` module first resolves a name.
Runtime evidence: `tests/test_chain.py::ValueClasses` (6 tests, 3 skipped until python-multiplatform
#17, because they call camelCase extensions on a proxy).

### S6.5 Lazy resolution and handle lifetime — `partial`
### S6.5 Lazy resolution and handle lifetime — `implemented`

A mapped package with bindings is importable, one without is not; a name is adapted once and then
lives in the module dict; `dir()` reports what is bound; dropping a proxy releases its Kotlin
handle. Evidence: `tests/test_chain.py::Laziness` (4 tests), `::Handles` (1 test), skipped/failing.
A mapped module is a file on disk and imports without a binder; one with no file is not importable;
a name is adapted once and then lives in the module dict; `dir()` reports the Pythonic names only;
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`

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>40 passed, 37 skipped</strong>. The 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>40 passed, 37 skipped</strong> 가 정상입니다. 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> 체크아웃이 있거나 다음이 필요합니다:</p>
<p data-lang="en">Expect <strong>40 passed, 45 skipped</strong>. The 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>40 passed, 45 skipped</strong> 가 정상입니다. 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <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 it, 4 of them pass and 33 currently <strong>fail</strong>: the binder no longer renames namespaces, and this package's re-export is not wired yet. That is expected and recorded.</p><p data-lang="ko">지정하면 그중 4 개는 통과하고 33 개는 현재 <strong>실패</strong>합니다: 바인더는 더 이상 네임스페이스 이름을 바꾸지 않고, 이 패키지의 재노출은 아직 연결되지 않았습니다. 예상된 상태이며 기록되어 있습니다.</p>
<p data-lang="en">With it, all but 4 pass; those 4 <strong>skip</strong> until the binder can resolve a snake_case method on a proxy it returned (python-multiplatform #17). That is expected and recorded.</p><p data-lang="ko">지정하면 4 개를 뺀 전부가 통과합니다. 그 4 개는 바인더가 돌려준 proxy 의 snake_case 메서드를 바인더가 풀 수 있을 때까지(python-multiplatform #17) <strong>건너뜁니다</strong>. 예상된 상태이며 기록되어 있습니다.</p>
</section>

<section>
Expand Down
Loading