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: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ worktree so a test can see it. Read it in place.
- `python-multiplatform` exposes Kotlin declarations under their **Kotlin** names
(`androidx.compose.material3`). It must never map `androidx` to `pythonx` or rename any Kotlin
namespace. Do not propose, add, or depend on such a feature there, including in test harnesses.
- What the rule forbids is renaming a **namespace**. Turning a Kotlin member name or keyword into
`snake_case` (`fillMaxWidth` to `fill_max_width`, `onClick` to `on_click`) is the binder's own
feature since python-multiplatform #131, by the same rule as `pythonx.compose._reexport.python_name`;
depending on it is allowed, and this package keeps no member resolver or keyword wrapper of its
own. What stays here: the module grouping, the snake-case-only `pythonx` surface, aliases, objects
and grouped constants, the value-class allowlist, the app root and the Pythonic stubs.
- `pythonx/` is a **real package on disk**. Its modules import the `androidx.compose.*` modules the
binder exposes and restructure them Pythonically. A design in which something else synthesises
`pythonx.*` (for example with `__path__ = []`) and the on-disk files cannot be imported is ruled
Expand Down
11 changes: 7 additions & 4 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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 `describe_member` 필요; Kotlin 이름도 실행 시 그대로 통하고 스텁은 snake_case 만) |
| 구현 | 매핑 매니페스트 `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 만) |
| 부분 | 선언형 앱 루트 `@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) |
| 계획 | `remember_saveable`·색 스킴·코루틴 스코프 |

Expand All @@ -39,11 +39,11 @@ uv run --with pytest --with mypy pytest tests -q
python-multiplatform develop 31c092f0+ 251 passed, 1 skipped
describe_member 없는 체크아웃 같은 수, 그중 7 개 건너뜀 (python-multiplatform #54 이전)
describe(module, name) 없는 체크아웃 같은 수, 그중 13 개 건너뜀 (python-multiplatform #36 이전)
그보다 오래된 체크아웃 거기에 5 개 더 건너뜀 (member resolver 없음)
그보다 오래된 체크아웃 거기에 5 개 더 건너뜀 (#131 이전, snake_case 이름 없음)
```

`describe_member`(python-multiplatform #54)가 없는 바인더에서는 메서드 키워드를 확인하는 7 개가(Kotlin 키워드로 되돌아가는 2 개는 실행됨), `describe(module, name)`(python-multiplatform #36)이 없는 바인더에서는 묶음 상수를 확인하는 13 개가,
member resolver(python-multiplatform `ba4c6f49`)가 없는 바인더에서는 proxy 의 snake_case 메서드를 부르는
#131(python-multiplatform, snake_case 이름·키워드) 이전 바인더에서는 proxy 의 snake_case 메서드를 부르는
5 개가 더 건너뛴다. 두 환경 모두의 건너뜀 1 개는 worktree 에 없는 `UI.ipynb` 의 테스트다. 건너뜀은 통과가 아니다.
체크아웃 없이 통과하는 134 개 중 61 개는 타입 스텁과 wheel 을 검사하고(SPEC S1.2), 나머지 다수는 *부재*(옛 토큰·삭제된
파일이 없음)를 확인하는 것이라 기능 진척으로 세지 않는다.
Expand Down Expand Up @@ -98,7 +98,10 @@ uv run python .github/scripts/check_guide.py # 가이드: HTML 파싱, 링크
- 배포 이름은 `pythonx-compose`.
- `pythonx` 는 디스크에 존재하는 실제 패키지이고, 그 코드가 `androidx` 모듈을 가져와 재구성한다.
- 바인더(`python-multiplatform`)는 Kotlin 네임스페이스를 절대 바꾸지 않는다. `androidx → pythonx`
매핑은 이 저장소의 일이고, `pythonx-map.toml` 이 그 매니페스트다.
매핑은 이 저장소의 일이고, `pythonx-map.toml` 이 그 매니페스트다. 이름과 키워드의 snake_case 변환은
python-multiplatform #131 부터 바인더의 기능이다(규칙은 이 패키지의 `python_name` 과 같다). 이 저장소는
모듈 묶음, snake_case 만 나열하는 pythonx 표면, 별칭, 객체와 묶음 상수, 값 클래스 허용 목록, 앱 루트,
Pythonic 스텁을 맡고, 프록시의 snake_case 에는 python-multiplatform #131 이상이 필요하다(#79).
- Kotlin 확장 함수는 수신 객체 프록시의 메서드로 보인다.
- `@Composable` 데코레이터는 유지한다.
- `.pyi` 스텁은 패키지에 포함되어 배포된다.
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ proofs are still pending. See [Status](#-status).</sub>
`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.
reshapes it. The binder never renames a namespace. Since python-multiplatform #131 it serves
`snake_case` names and keywords itself, by this package's rule; this package keeps the module
grouping and the `snake_case`-only surface.
- **One manifest.** [`pythonx-map.toml`](https://github.com/thisisthepy/pythonx-compose/blob/main/pythonx/compose/pythonx-map.toml) says which `pythonx.compose.*` module
stands for which Kotlin package. It lives inside the package and ships in the wheel. The runtime
and the `.pyi` generator read the same file, so what your editor completes is what the interpreter
Expand Down
17 changes: 11 additions & 6 deletions docs/INTENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,17 @@ package `pythonx.compose`.
modules** and **restructures them** so they can be used Pythonically. It is not a namespace that
something else synthesises at run time.

### 2.3 The binder never renames; renaming is this package's job
### 2.3 The binder never renames a namespace; this package groups the namespaces

`python-multiplatform` (the binder) exposes Kotlin declarations to Python **under their Kotlin
names**: `androidx.compose.material3` in Kotlin is `androidx.compose.material3` in Python. The
binder must never turn `androidx` into `pythonx` or rename any other Kotlin namespace. Mapping
`androidx.compose.*` to `pythonx.compose.*` is done **here, in real Python code**.
namespaces**: `androidx.compose.material3` in Kotlin is `androidx.compose.material3` in Python. The
binder must never turn `androidx` into `pythonx` or rename any other Kotlin namespace. Grouping
`androidx.compose.*` into `pythonx.compose.*` is done **here, in real Python code**.

Pythonic spellings *inside* a namespace (snake_case names and keyword arguments) are the binder's own
feature. The maintainer confirmed it (2026-10-03, relayed by the ecosystem lead): "파이썬 형태로
인자랑 바꿔주는건 원래 python-multiplatform 자체에서 지원하는 기능". python-multiplatform #131
restored it, by this package's rule, after a narrower reading of this section had removed it.

`pythonx-map.toml` in this repository is the mapping manifest: which `pythonx.compose.*` module
stands for which Kotlin package, and which value classes may be written as raw numbers.
Expand Down Expand Up @@ -101,8 +106,8 @@ types) even though the runtime resolves bindings on demand.

- **Not the binder.** Embedding CPython, the FFI, upcalls and the artefact walker belong to
`python-multiplatform`. This package depends on that surface; it does not reimplement it.
- **Not a renamer inside the binder.** Nothing that maps `androidx` to `pythonx` may live in
`python-multiplatform` (§2.3).
- **Not a namespace renamer inside the binder.** Nothing that maps `androidx` to `pythonx` may live in
`python-multiplatform` (§2.3). Snake_case names and keywords within a namespace are the binder's.
- **Not a reimplementation of Compose.** Widgets are Compose's own; this package changes how they
are named and reached from Python, not what they do.
- **Not an invented API.** Parameters that do not exist in Kotlin (for example the notebook's
Expand Down
35 changes: 29 additions & 6 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ and the name the interpreter resolves cannot drift.
- **What is generated.** `pythonx/compose/**/__init__.pyi` for every module in `pythonx-map.toml`,
beside its `__init__.py`, and an empty `pythonx/compose/py.typed`. The stubs are committed;
regenerating them from the same input produces no diff.
- **From what.** python-multiplatform's CI artefact `kotlin-stubs` (workflow run 37103737430,
commit `a5028618`): Kotlin-named stubs for Compose 1.11.1, one `androidx/compose/.../__init__.pyi`
- **From what.** python-multiplatform's CI artefact `kotlin-stubs` (workflow run 37131041879,
commit `452cc807`, python-multiplatform #131): Kotlin-named stubs for Compose 1.11.1, one `androidx/compose/.../__init__.pyi`
per Kotlin package. `uv run python scripts/gen_stubs.py <kotlin-stubs.zip or directory>` converts them;
the first line of every generated stub records the artefact, run and commit it came from.
- **The rule is the runtime's** (`_reexport.py`), applied by `scripts/gen_stubs.py`:
Expand All @@ -84,11 +84,21 @@ and the name the interpreter resolves cannot drift.
snake_case, except the positional-only `receiver` and anonymous `__aN` slots; keyword-only
markers and the binder's `@overload` sets, in the binder's order, are kept;
- a stub class keeps its name. Its extension members (`Modifier.fill_max_width`, a
`ClassVar` of a callable `Protocol`) are renamed with `python_name`, the member resolver's rule,
`ClassVar` of a callable `Protocol`) are renamed with `python_name`, the binder's rule since #131,
and the protocol's `__call__` parameters are snake_case like a module function's (the receiver and
anonymous slots keep their names), because the runtime translates a method's keywords (S4.1);
the stubs are Pythonic-only, so a method's Kotlin keyword, which still works at run time, fails
a type check;
- one name per declaration. Since python-multiplatform #131 the binder's stubs name a name twice:
a module stub carries `fill_max_width = fillMaxWidth` beside the Kotlin-named def, a proxy class
an alias `ClassVar` or property per member, and its signature keywords are already `snake_case`
where it has an alias. The generator drops the alias lines and the repeated member, because
renaming the Kotlin-named declaration by the same rule yields the same name, so no Kotlin
second name and no line such as `fill_max_width = fill_max_width` reaches the stubs;
`python_name` and `snake_case` are idempotent on a `snake_case` name, so a keyword the binder
already spells that way is unchanged. A name the binder declares as a callable module
(`PaddingValues: _PaddingValues_callable_module`, whose `__call__` holds the explicit overloads
and whose members are its constants) is that declaration, with no base-name `def` beside it;
- a Kotlin object served as a sub-package (`Alignment`, `Arrangement`) becomes a class of that
name in its parent module's stub, its constants `ClassVar`s and its functions snake_case
static methods (an explicit overload set, `spaced_by__Dp`, also gets its base name `spaced_by`). A sub-package directory holding only `__init__.pyi` would be a namespace
Expand Down Expand Up @@ -124,11 +134,12 @@ and the name the interpreter resolves cannot drift.
from `pythonx.compose.layout`; a name lent under another one is assigned its path in the
source's stub, `DefaultIcons = Icons.Default`, which upstream types as `Icons.Filled`); names a package defines itself (`runtime.Composable`) are
carried over.
- **How it is verified.** `tests/test_stubs.py` converts three fixtures: the fake host's
- **How it is verified.** `tests/test_stubs.py` converts five fixtures: the fake host's
declarations in the old and the current upstream format (and the current format with an object
in its parent stub), and a third in the format of python-multiplatform #53/#44/#38 (nested types,
object functions, property setters), and a fourth in the format of #71/#68 (a nested type with two bases, icons as typed
properties), and checks the generated signatures against `inspect.signature` at run time.
properties), and a fifth in the format of #131 (alias lines, alias members, a callable module), and checks
the generated signatures against `inspect.signature` at run time.
`TheCommittedStubs` regenerates from `.tmp/kotlin-stubs.zip` and requires the committed files to
be identical (it skips where the artefact is absent, which includes CI). `tests/test_typing.py`
runs mypy over small programs against the committed stubs: correct code passes, a misspelled
Expand Down Expand Up @@ -229,6 +240,13 @@ working, and lists Kotlin names only in `dir()`. This package no longer register
the same. The stubs are Pythonic-only (§1, S1.2), so a type checker flags a Kotlin keyword the
runtime accepts.

Division of labour since #131: the binder renames names and keywords (it never renames a namespace,
INTENT §2.3). This package keeps the module grouping (`pythonx-map.toml`), the snake-case-only
`pythonx` surface (a module lists and serves `snake_case` names only, where the binder's own
`androidx.*` serves both), the aliases, the objects and their grouped constants, the value-class
allowlist, the app root, and the Pythonic stubs. pythonx-compose needs python-multiplatform at #131 or
later for `snake_case` names and keywords on a proxy; on an older binder those calls skip in the suite.

## 4. Naming

### S4.1 Kotlin parameters, `snake_case`: `implemented`
Expand All @@ -239,6 +257,11 @@ type suffix (`padding__Dp_Dp`); a name the reverse rule cannot invert still reso
rule is only ever applied forward to the module's real names; the camelCase spelling is not a second
name.

Who does it: the binder renames names and keywords since python-multiplatform #131, with this
package's rule (`pythonx.compose._reexport.python_name`, pinned equal to the binder's by
`tests/test_chain.py::TheBindersNamingRuleIsThisPackages`). This package keeps the policy that a
`pythonx` module lists and serves the `snake_case` spelling only, and the stubs name only it.

Implemented for module-level names and keywords and for method names on a binder proxy
(`tests/test_chain.py::Names`, 8 tests; `TheChain`) and for a method's keywords on a binder proxy
(`tests/test_chain.py::MethodKeywords`, 5 tests; served by the binder since python-multiplatform
Expand Down Expand Up @@ -379,7 +402,7 @@ proxy class itself.

Evidence: `tests/test_chain.py::TheChain` (8 tests) and
`tests/test_modifier_module.py::TheModifierSeam` (5 tests), against python-multiplatform `ba4c6f49`.
A snake_case method resolved through the member rule leaves the binder's class Kotlin-named.
A snake_case method is served by the binder (python-multiplatform #131) and leaves its class Kotlin-named in `dir()`.

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

Expand Down
Loading
Loading