Skip to content

Commit 383c1d8

Browse files
committed
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
1 parent 47befcb commit 383c1d8

23 files changed

Lines changed: 333 additions & 160 deletions

‎PROJECT.md‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,25 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **`
2727

2828
| 상태 | 항목 |
2929
|---|---|
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 만 됨) |
3232
| 계획 | `remember_saveable`·`DefaultIcons`·코루틴 스코프 |
3333

3434
### 테스트 기준선
3535

3636
```
3737
python3 -m pytest tests -q
38-
PythonMultiplatform 체크아웃 없음 106 passed, 78 skipped
39-
python-multiplatform develop 31c092f0+ 183 passed, 1 skipped
38+
PythonMultiplatform 체크아웃 없음 107 passed, 87 skipped
39+
python-multiplatform develop 31c092f0+ 193 passed, 1 skipped
40+
describe_member 없는 체크아웃 같은 수, 그중 7 개 건너뜀 (python-multiplatform #54 이전)
4041
describe(module, name) 없는 체크아웃 같은 수, 그중 13 개 건너뜀 (python-multiplatform #36 이전)
4142
그보다 오래된 체크아웃 거기에 5 개 더 건너뜀 (member resolver 없음)
4243
```
4344

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

5051
### 마일스톤

‎README.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ proofs are still pending. See [Status](#-status).</sub>
4545
only (`fillMaxWidth` → `fill_max_width`, `toURLString` → `to_url_string`), and an explicit
4646
overload keeps its suffix (`padding__Dp`).
4747
- **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.
5052
- **Kotlin objects are namespaces.** `Alignment.Center` and `Arrangement.End` are read without
5153
parentheses; functions inside them are `snake_case`: `Arrangement.spaced_by(8)`. Constants can
5254
also be reached grouped by their declared type, as the notebook writes them:
@@ -150,11 +152,11 @@ standalone desktop toolkit.
150152
| `pythonx` as a real on-disk package re-exporting `androidx.compose.*` by one rule | ✅ implemented and tested |
151153
| `Modifier` chains with snake_case methods, overload dispatch, `Dp` as a number | ✅ implemented and tested against the binder's layer |
152154
| `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`) |
154156
| The empty `Modifier` | 🟡 partial — `Modifier.padding(16)` from the class needs an app-supplied factory against real Compose |
155157
| `Alignment` / `Arrangement` | ✅ implemented and tested — `Alignment.Center`, `Arrangement.spaced_by(8)`, and grouped `Alignment.Horizontal.End` beside `Alignment.End` |
156158
| 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) |
158160
| Distribution (`pythonx-compose`) | 🟡 partial — the wheel carries the manifest, the re-export rule, the stubs and `py.typed` |
159161
| 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) |
160162
| `TextField` input-method (IME) handling | ⏳ planned (#10) |

‎docs/SPEC.md‎

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,29 @@ Each item carries a status:
1414

1515
A behaviour change starts here, then becomes a failing test, then code (`AGENTS.md` §5).
1616

17-
## 0. Test baseline (2026-10-03, after issue #11's app root)
17+
## 0. Test baseline (2026-10-03, after method keywords)
1818

1919
Run from a worktree with `python -m pytest tests -q -rs` (pytest 8, mypy 2.4, CPython 3.13), where
2020
`UI.ipynb` is absent and its one test skips:
2121

2222
| Environment | Result |
2323
|---|---|
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 |
2728
| an older one still, without `add_member_resolver` | with 5 more skipped again |
2829

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.
3032
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
3235
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
3336
`TheBinderPath` tests (S5.4). `tests/test_typing.py` skips where mypy is not installed. A skip is not a
3437
pass.
3538

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
3740
rest assert **absence** (a retired token, a deleted file, a docstring that exists). Those are listed
3841
in §9 and are not counted as features.
3942

@@ -71,8 +74,10 @@ and the name the interpreter resolves cannot drift.
7174
markers and the binder's `@overload` sets, in the binder's order, are kept;
7275
- a stub class keeps its name. Its extension members (`Modifier.fill_max_width`, a
7376
`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;
7681
- a Kotlin object served as a sub-package (`Alignment`, `Arrangement`) becomes a class of that
7782
name in its parent module's stub, its constants `ClassVar`s and its functions snake_case
7883
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.
104109
- Many parameter and return types are `Any`: upstream emits `Any` for a class that shares its
105110
name with a function (`PaddingValues`, `TextStyle`, `Color`) and for packages it does not stub;
106111
`Dp` is `float`.
107-
- A method's keywords are Kotlin's in the stub because they are Kotlin's at run time (S4.1).
108112
- The `pythonx.compose` root module maps `androidx.compose`, which has no stub, so its stub is
109113
empty.
110114
- 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
161165
class. The same rule reaches them as the binder's member resolver (`member_name`, registered through
162166
`python_multiplatform.binding.add_member_resolver`, python-multiplatform `ba4c6f49`); the binder
163167
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
175+
only, and a method's keywords are Kotlin's.
166176

167177
## 4. Naming
168178

169-
### S4.1 Kotlin parameters, `snake_case` — `partial` (method keywords)
179+
### S4.1 Kotlin parameters, `snake_case` — `implemented`
170180

171181
`onClick` → `on_click`, `fillMaxWidth` → `fill_max_width`, `zIndex` → `z_index`,
172182
`toURLString` → `to_url_string`; type names (`Modifier`) unchanged; an explicit overload keeps its
@@ -175,8 +185,9 @@ rule is only ever applied forward to the module's real names; the camelCase spel
175185
name.
176186

177187
Implemented for module-level names and keywords and for method names on a binder proxy
178-
(`tests/test_chain.py::Names`, 8 tests; `TheChain`). Partial because a method's keyword arguments are
179-
still Kotlin's (§3).
188+
(`tests/test_chain.py::Names`, 8 tests; `TheChain`) and for a method's keywords on a binder proxy
189+
(`tests/test_chain.py::MethodKeywords`, 7 tests, and `MethodKeywordsNeedTheBindersDescription`, 2; needs
190+
python-multiplatform's `describe_member`, #54). Disabling the keyword map fails 4 tests.
180191

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

‎docs/guide/concepts.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ <h2><span data-lang="en">One re-export rule</span><span data-lang="ko">재노출
6969
<h2><span data-lang="en">Extension functions are methods</span><span data-lang="ko">확장 함수는 메서드</span></h2>
7070
<p data-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><p data-lang="ko"><code>Modifier</code> 의 대부분은 Kotlin 확장 함수입니다. 파이썬에서는 각각이 수신 객체 프록시의 메서드가 되어, 체인이 Kotlin 에서처럼 읽힙니다. 바인더의 프록시 클래스는 Kotlin 이름을 유지하며, 같은 이름 규칙이 바인더의 멤버 리졸버로 등록되어 snake_case 표기가 Kotlin 멤버에 닿습니다.</p>
7171
<pre><code class="language-python">Modifier.padding(16).size(24).fill_max_width()</code></pre>
72-
<p data-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><p data-lang="ko">차이가 하나 남아 있습니다: 이런 메서드의 키워드 인자는 아직 Kotlin 그대로입니다 — <code>m.padding(paddingValues=…)</code> 이고, 모듈 함수는 <code>padding(m, padding_values=…)</code> 를 받습니다.</p>
72+
<p data-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><p data-lang="ko">이런 메서드의 키워드 인자도 <code>snake_case</code> 입니다 — <code>m.padding(padding_values=…)</code> 이며, 모듈 함수는 <code>padding(m, padding_values=…)</code> 를 받습니다. 리졸버는 선언의 실제 파라미터 이름에서 읽은 키워드 맵도 바인더에 넘깁니다. Kotlin 이름(<code>paddingValues=</code>)도 실행 시에는 그대로 통하지만, 타입 스텁은 Pythonic 이름만 제시합니다.</p>
7373
</section>
7474

7575
<section>

‎docs/guide/getting-started.html‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ <h2><span data-lang="en">1. Get the source</span><span data-lang="ko">1. 소스
4343
<section>
4444
<h2><span data-lang="en">2. Run the tests</span><span data-lang="ko">2. 테스트 실행</span></h2>
4545
<pre><code class="language-bash">python3 -m pytest tests -q</code></pre>
46-
<p data-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 <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> checkout beside this one, or:</p><p data-lang="ko"><strong>106 passed, 78 skipped</strong> 가 정상입니다. 건너뛴 테스트 하나는 클론에는 없는 메인테이너의 <code>UI.ipynb</code> 를 읽으며, mypy 는 스텁을 타입 검사합니다. 나머지 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> 체크아웃이 있거나 다음이 필요합니다:</p>
46+
<p data-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 <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> checkout beside this one, or:</p><p data-lang="ko"><strong>107 passed, 87 skipped</strong> 가 정상입니다. 건너뛴 테스트 하나는 클론에는 없는 메인테이너의 <code>UI.ipynb</code> 를 읽으며, mypy 는 스텁을 타입 검사합니다. 나머지 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> 체크아웃이 있거나 다음이 필요합니다:</p>
4747
<pre><code class="language-bash">PYTHONMULTIPLATFORM_HOME=/path/to/python-multiplatform python3 -m pytest tests -q</code></pre>
48-
<p data-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><p data-lang="ko">최신 체크아웃(<code>31c092f0</code> 이후)이면 <strong>183 개가 통과</strong>하고 1 개(<code>UI.ipynb</code> 테스트)를 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 <strong>건너뜁니다</strong>. 예상된 상태이며 기록되어 있습니다.</p>
48+
<p data-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><p data-lang="ko">최신 체크아웃(<code>31c092f0</code> 이후)이면 <strong>193 개가 통과</strong>하고 1 개(<code>UI.ipynb</code> 테스트)를 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 <strong>건너뜁니다</strong>. 예상된 상태이며 기록되어 있습니다.</p>
4949
</section>
5050

5151
<section>

0 commit comments

Comments
 (0)