You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 383c1d8
Browse filesBrowse the repository at this point in the historyBrowse files
Feat: Take snake_case keyword arguments on proxy methods too
m.padding(padding_values=...) failed: the member resolver mapped a method's
name to Kotlin but not its keywords, so a method took Kotlin's parameter names
while a module function took snake_case ones. python-multiplatform #54 (PR
keyword map. member_name now answers (kotlin_name, {snake_case(p): p}) over
every overload's parameters -- for a snake_case member name, and for a
Kotlin-named member called with keywords -- and the bare name when there is
nothing to map. The map is metadata, cached per (type, member) under the
binder that answered. Kotlin keywords still pass through and work; an unknown
keyword is still the binder's TypeError listing the candidates. On a binder
without describe_member, behaviour is as before.
The stubs follow: a method Protocol's __call__ parameters are snake_case,
and the header note that said they were Kotlin's is gone. The stubs are
Pythonic-only -- mypy rejects paddingValues= although the runtime accepts it,
which SPEC says.
Drafted by a subagent in a worktree; reviewed here.
feature added: snake_case method keywords, at run time and in the stubs.
test added: MethodKeywords (7), MethodKeywordsNeedTheBindersDescription (2),
stub and mypy tests replaced. Red: 4 of 7, 3 stub tests and the mypy case
failed. Forcing the keyword map to {} fails the same 4.
documentation corrected: SPEC 0/3/S1.2/S4.1 (now implemented), PROJECT.md,
guide (concepts, modifiers, status, getting-started), README and README_ko.
Suite: with python-multiplatform 31c092f0, 178 passed, 3 skipped -> 188
passed, 3 skipped; without a checkout, 106 passed, 75 skipped -> 107 passed,
84 skipped. check_guide.py: 0. Stubs regenerated from the run 37077627980
artefact; regenerating from a #53 artefact follows separately.
Refs #9
Copy file name to clipboardExpand all lines: PROJECT.md
+7-6Lines changed: 7 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -27,24 +27,25 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **`
27
27
28
28
| 상태 | 항목 |
29
29
|---|---|
30
-
| 구현 | 매핑 매니페스트 `pythonx-map.toml`, `@Composable` 항등 데코레이터, 실제 디스크 패키지 `pythonx` 와 모듈 수준 규칙 기반 재노출(snake_case 이름·키워드·시그니처, 오버로드 디스패치, 값 클래스 허용 목록), 실제 Compose 1.11.1 에서 생성한 `.pyi` 스텁과 `py.typed`(wheel 포함, mypy 로 검사), `Alignment`/`Arrangement` 상수(평면 `Alignment.End` 와 묶음 `Alignment.Horizontal.End` 모두, #9) |
31
-
| 부분 | 선언형 앱 루트 `@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 만 됨) |
30
+
| 구현 | 매핑 매니페스트 `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 만)|
31
+
| 부분 | 선언형 앱 루트 `@app`·`state`·`app_root`(#11; 로직은 테스트됨, 바인더 경로는 #38 모양의 가짜 호스트로 테스트됨, 실제 Compose 증거는 E2E 모듈 #19, 바인더가 `Any?` 칸의 int 를 거부하므로 `state(1)` 은 아직 TypeError, python-multiplatform #69 대기), 배포 설정, Material 3 위젯(렌더 증거는 python-multiplatform 에만), `Icon`·색 스킴(import 만 됨) |
32
32
| 계획 |`remember_saveable`·`DefaultIcons`·코루틴 스코프 |
Copy file name to clipboardExpand all lines: README.md
+6-4Lines changed: 6 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -45,8 +45,10 @@ proofs are still pending. See [Status](#-status).</sub>
45
45
only (`fillMaxWidth` → `fill_max_width`, `toURLString` → `to_url_string`), and an explicit
46
46
overload keeps its suffix (`padding__Dp`).
47
47
-**Extension functions are methods.**`Modifier.padding(16).size(24).fill_max_width()` chains
48
-
exactly as it does in Kotlin. Keyword arguments to such a method are still Kotlin's own
49
-
(`m.padding(paddingValues=...)`); the module function takes `padding(m, padding_values=...)`.
48
+
exactly as it does in Kotlin. Keyword arguments to such a method are `snake_case`
49
+
too (`m.padding(padding_values=...)`), as they are on the module function
50
+
`padding(m, padding_values=...)`; Kotlin's own spelling still works at run time, and the type
51
+
stubs offer only the Pythonic one.
50
52
-**Kotlin objects are namespaces.**`Alignment.Center` and `Arrangement.End` are read without
51
53
parentheses; functions inside them are `snake_case`: `Arrangement.spaced_by(8)`. Constants can
52
54
also be reached grouped by their declared type, as the notebook writes them:
@@ -150,11 +152,11 @@ standalone desktop toolkit.
150
152
|`pythonx` as a real on-disk package re-exporting `androidx.compose.*` by one rule | ✅ implemented and tested |
151
153
|`Modifier` chains with snake_case methods, overload dispatch, `Dp` as a number | ✅ implemented and tested against the binder's layer |
152
154
|`Column`, `Row`, `Spacer` from `material3` as well as `layout`| ✅ implemented and tested |
153
-
| Method keyword arguments in `snake_case`|🟡 partial — module functions yes; methods still take Kotlin's keywords|
155
+
| Method keyword arguments in `snake_case`|✅ implemented and tested — module functions and methods (methods need python-multiplatform's `describe_member`)|
154
156
| The empty `Modifier`| 🟡 partial — `Modifier.padding(16)` from the class needs an app-supplied factory against real Compose |
155
157
|`Alignment` / `Arrangement`| ✅ implemented and tested — `Alignment.Center`, `Arrangement.spaced_by(8)`, and grouped `Alignment.Horizontal.End` beside `Alignment.End`|
156
158
| Material 3 widgets (`Text`, `Button`, `Card`, `TextField`, …) | 🟡 partial — re-exported by rule; render proofs per widget pending (#9) |
157
-
| 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) |
159
+
| Type stubs (`.pyi`) from real Compose 1.11.1, `py.typed`| ✅ implemented and checked with mypy; many types are still `Any` (#12) |
158
160
| Distribution (`pythonx-compose`) | 🟡 partial — the wheel carries the manifest, the re-export rule, the stubs and `py.typed`|
159
161
| 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) |
Copy file name to clipboardExpand all lines: docs/SPEC.md
+26-15Lines changed: 26 additions & 15 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -14,26 +14,29 @@ Each item carries a status:
14
14
15
15
A behaviour change starts here, then becomes a failing test, then code (`AGENTS.md` §5).
16
16
17
-
## 0. Test baseline (2026-10-03, after issue #11's app root)
17
+
## 0. Test baseline (2026-10-03, after method keywords)
18
18
19
19
Run from a worktree with `python -m pytest tests -q -rs` (pytest 8, mypy 2.4, CPython 3.13), where
20
20
`UI.ipynb` is absent and its one test skips:
21
21
22
22
| Environment | Result |
23
23
|---|---|
24
-
| No `PythonMultiplatform` checkout found |**106 passed, 78 skipped**, 122 subtests passed |
25
-
| 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 |
26
-
| an older checkout, without `describe(module, name)` (python-multiplatform #36) | the same, with 13 more skipped |
24
+
| No `PythonMultiplatform` checkout found |**107 passed, 87 skipped**, 122 subtests passed |
25
+
| python-multiplatform `develop` at `31c092f0` or later (property rows, #38; `describe_member`, #54; besides `add_member_resolver` and `describe(module, name)`) |**193 passed, 1 skipped**, 149 subtests passed |
26
+
| an older checkout, without `describe_member` (python-multiplatform #54) | the same, with 7 more skipped |
27
+
| an older one, without `describe(module, name)` (python-multiplatform #36) | with 13 more skipped again |
27
28
| an older one still, without `add_member_resolver`| with 5 more skipped again |
28
29
29
-
Without a checkout, the 75 tests that install the binder's layers through `tests/adapter.py` skip.
30
+
Measured in a worktree, which has `.tmp/kotlin-stubs.zip` but no `UI.ipynb`. Without a checkout, the
31
+
86 tests that install the binder's layers through `tests/adapter.py` skip, plus the notebook test.
30
32
Against a binder without `describe(module, name)`, the 13 that check grouped constants (§7, S7.1)
31
-
skip. Against a binder older than `ba4c6f49`, the 5 that call a snake_case method on a proxy the
33
+
skip. Against a binder without `describe_member`, the 7 that check a method's keywords (S4.1) skip; the 2
34
+
that check the fallback (Kotlin keywords, names resolved) run. Against a binder older than `ba4c6f49`, the 5 that call a snake_case method on a proxy the
32
35
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
33
36
`TheBinderPath` tests (S5.4). `tests/test_typing.py` skips where mypy is not installed. A skip is not a
34
37
pass.
35
38
36
-
Of the 106 that pass without a checkout, 38 check the type stubs and the wheel (S1.2); most of the
39
+
Of the 107 that pass without a checkout, 39 check the type stubs and the wheel (S1.2); most of the
37
40
rest assert **absence** (a retired token, a deleted file, a docstring that exists). Those are listed
38
41
in §9 and are not counted as features.
39
42
@@ -71,8 +74,10 @@ and the name the interpreter resolves cannot drift.
71
74
markers and the binder's `@overload` sets, in the binder's order, are kept;
72
75
- a stub class keeps its name. Its extension members (`Modifier.fill_max_width`, a
73
76
`ClassVar` of a callable `Protocol`) are renamed with `python_name`, the member resolver's rule,
74
-
but the protocol's parameter names stay Kotlin's, because the runtime does not translate a
75
-
method's keywords yet (§3); every generated stub says so in its header;
77
+
and the protocol's `__call__` parameters are snake_case like a module function's (the receiver and
78
+
anonymous slots keep their names), because the runtime translates a method's keywords (S4.1);
79
+
the stubs are Pythonic-only, so a method's Kotlin keyword, which still works at run time, fails
80
+
a type check;
76
81
- a Kotlin object served as a sub-package (`Alignment`, `Arrangement`) becomes a class of that
77
82
name in its parent module's stub, its constants `ClassVar`s and its functions snake_case
78
83
static methods. A sub-package directory holding only `__init__.pyi` would be a namespace
@@ -104,7 +109,6 @@ and the name the interpreter resolves cannot drift.
104
109
- Many parameter and return types are `Any`: upstream emits `Any` for a class that shares its
105
110
name with a function (`PaddingValues`, `TextStyle`, `Color`) and for packages it does not stub;
106
111
`Dp` is `float`.
107
-
- A method's keywords are Kotlin's in the stub because they are Kotlin's at run time (S4.1).
108
112
- The `pythonx.compose` root module maps `androidx.compose`, which has no stub, so its stub is
109
113
empty.
110
114
- Object constants named like a Python keyword (`FilterQuality.None`) cannot be written as an
@@ -161,12 +165,18 @@ Methods on a proxy the binder returned (`m.fill_max_width()`) are attributes of
161
165
class. The same rule reaches them as the binder's member resolver (`member_name`, registered through
162
166
`python_multiplatform.binding.add_member_resolver`, python-multiplatform `ba4c6f49`); the binder
163
167
caches the alias in its own registry, so its class keeps Kotlin names in `dir()` (issue #20).
164
-
Keyword arguments to such a method are still Kotlin's own (`m.padding(paddingValues=...)`): the
165
-
resolver maps names, not keywords.
168
+
Keyword arguments to such a method are snake_case too (`m.padding(padding_values=...)`): the resolver
169
+
answers `(kotlin_name, {python_keyword: kotlinParameter})`, built from every overload's parameter
170
+
names read with `python_multiplatform.describe_member(type, member)` (python-multiplatform #54;
171
+
receiver slot skipped, cached per type and member because it is metadata, nothing invoked). The
172
+
Kotlin spelling still works at run time -- the binder passes a keyword the map does not name
173
+
through -- so `m.padding(paddingValues=...)` is accepted; the stubs are Pythonic-only (§1, S1.2),
174
+
so a type checker flags it. On a binder without `describe_member` the resolver answers the name
<h2><spandata-lang="en">Extension functions are methods</span><spandata-lang="ko">확장 함수는 메서드</span></h2>
70
70
<pdata-lang="en">Most of <code>Modifier</code> is Kotlin extension functions. In Python each one is a method on the receiver's proxy, so a chain reads as it does in Kotlin. The binder's proxy class keeps Kotlin names; the same naming rule is registered as the binder's member resolver, so the snake_case spelling reaches the Kotlin member.</p><pdata-lang="ko"><code>Modifier</code> 의 대부분은 Kotlin 확장 함수입니다. 파이썬에서는 각각이 수신 객체 프록시의 메서드가 되어, 체인이 Kotlin 에서처럼 읽힙니다. 바인더의 프록시 클래스는 Kotlin 이름을 유지하며, 같은 이름 규칙이 바인더의 멤버 리졸버로 등록되어 snake_case 표기가 Kotlin 멤버에 닿습니다.</p>
<pdata-lang="en">One difference remains: keyword arguments to such a method are still Kotlin's own — <code>m.padding(paddingValues=…)</code>, while the module function takes <code>padding(m, padding_values=…)</code>.</p><pdata-lang="ko">차이가 하나 남아 있습니다: 이런 메서드의 키워드 인자는 아직 Kotlin 그대로입니다 — <code>m.padding(paddingValues=…)</code>이고, 모듈 함수는 <code>padding(m, padding_values=…)</code> 를 받습니다.</p>
72
+
<pdata-lang="en">Keyword arguments to such a method are <code>snake_case</code> too — <code>m.padding(padding_values=…)</code>, as the module function takes <code>padding(m, padding_values=…)</code>. The resolver also hands the binder a keyword map read from the declaration's own parameter names. Kotlin's spelling (<code>paddingValues=</code>) still works at run time; the type stubs offer only the Pythonic one.</p><pdata-lang="ko">이런 메서드의 키워드 인자도 <code>snake_case</code> 입니다 — <code>m.padding(padding_values=…)</code>이며, 모듈 함수는 <code>padding(m, padding_values=…)</code> 를 받습니다. 리졸버는 선언의 실제 파라미터 이름에서 읽은 키워드 맵도 바인더에 넘깁니다. Kotlin 이름(<code>paddingValues=</code>)도 실행 시에는 그대로 통하지만, 타입 스텁은 Pythonic 이름만 제시합니다.</p>
<pdata-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 <ahref="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> checkout beside this one, or:</p><pdata-lang="ko"><strong>106 passed, 78 skipped</strong> 가 정상입니다. 건너뛴 테스트 하나는 클론에는 없는 메인테이너의 <code>UI.ipynb</code> 를 읽으며, mypy 는 스텁을 타입 검사합니다. 나머지 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <ahref="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> 체크아웃이 있거나 다음이 필요합니다:</p>
46
+
<pdata-lang="en">Expect <strong>107 passed, 87 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 <ahref="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> checkout beside this one, or:</p><pdata-lang="ko"><strong>107 passed, 87 skipped</strong> 가 정상입니다. 건너뛴 테스트 하나는 클론에는 없는 메인테이너의 <code>UI.ipynb</code> 를 읽으며, mypy 는 스텁을 타입 검사합니다. 나머지 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <ahref="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> 체크아웃이 있거나 다음이 필요합니다:</p>
<pdata-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><pdata-lang="ko">최신 체크아웃(<code>31c092f0</code> 이후)이면 <strong>183 개가 통과</strong>하고 1 개(<code>UI.ipynb</code> 테스트)를 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 <strong>건너뜁니다</strong>. 예상된 상태이며 기록되어 있습니다.</p>
48
+
<pdata-lang="en">With a current one (<code>31c092f0</code> or later) <strong>193 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><pdata-lang="ko">최신 체크아웃(<code>31c092f0</code> 이후)이면 <strong>193 개가 통과</strong>하고 1 개(<code>UI.ipynb</code> 테스트)를 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 <strong>건너뜁니다</strong>. 예상된 상태이며 기록되어 있습니다.</p>
0 commit comments