From cfb7994a61330f5f869e4cd9fea4c9a0dd5b467b Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 3 Oct 2026 07:19:41 +0900 Subject: [PATCH] Feat: Reach snake_case methods on proxies the binder returns m.fill_max_width() on a Modifier Kotlin handed back is an attribute of the binder's own proxy class, which resolves Kotlin names only, so the module rule could not reach it. python-multiplatform ba4c6f49 added python_multiplatform.binding.add_member_resolver; the re-export layer now registers the same naming rule there (member_name), once per binding layer, alongside the value-class allowlist. The binder caches each alias in its own registry, so its class keeps Kotlin names in dir() -- which a new test pins. On an older binder without the hook nothing changes and the five tests that need it skip, saying why. Keyword arguments to such a method are still Kotlin's (m.padding(paddingValues=...)): the resolver maps names, not keywords, and that is recorded in SPEC section 3 and S4.1. feature added: the member resolver registration. test added: test_a_resolved_snake_case_method_leaves_the_binders_class_kotlin_named. Red: against python-multiplatform ba4c6f49 before this change, the 4 tests that used to skip ran and failed. Unregistering the resolver fails 5. documentation corrected: SPEC 0/3/S4.1/S6.1/S6.4, PROJECT.md, AGENTS.md section 14, guide status and baseline. Suite against python-multiplatform ba4c6f49 (sources exported read-only with git show): 92 passed, 4 failed -> 97 passed. Against the older sibling checkout: 92 passed, 5 skipped. Without a checkout: 50 passed, 47 skipped. check_guide.py: 0. Refs #20 --- AGENTS.md | 4 ++-- PROJECT.md | 11 +++++---- docs/SPEC.md | 42 ++++++++++++++++++--------------- docs/guide/getting-started.html | 4 ++-- docs/guide/status.html | 4 ++-- pythonx/compose/_reexport.py | 23 +++++++++++++++++- tests/test_chain.py | 8 +++++++ 7 files changed, 65 insertions(+), 31 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 24e14c5..b205170 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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, 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 + 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 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 diff --git a/PROJECT.md b/PROJECT.md index 6c7f778..2358b18 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -28,19 +28,20 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **` | 상태 | 항목 | |---|---| | 구현 | 매핑 매니페스트 `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) | +| 부분 | 배포 설정(스텁·`py.typed` 없음, 매니페스트가 패키지 밖), proxy 메서드의 키워드 인자는 아직 Kotlin 이름(`m.padding(paddingValues=...)`), Material 3 위젯(렌더 증거는 python-multiplatform 에만), `Icon`·색 스킴(import 만 됨), `Alignment`/`Arrangement` 상수(괄호 없이 읽음, 묶음 표기는 #9) | | 계획 | `.pyi` 동봉, 선언형 앱 루트(#11), `remember_saveable`·`DefaultIcons`·코루틴 스코프 | ### 테스트 기준선 ``` python3 -m pytest tests -q - PythonMultiplatform 체크아웃 없음 50 passed, 46 skipped - PYTHONMULTIPLATFORM_HOME 지정 92 passed, 4 skipped (4 개는 python-multiplatform #17 대기) + PythonMultiplatform 체크아웃 없음 50 passed, 47 skipped + python-multiplatform develop ba4c6f49+ 97 passed + 그보다 오래된 체크아웃 92 passed, 5 skipped (member resolver 없음) ``` -건너뛰는 4 개는 바인더가 돌려준 proxy 위의 camelCase 확장을 snake_case 이름으로 부르는 테스트다. -바인더의 member resolver(python-multiplatform #17)가 들어오면 실행된다. 건너뜀은 통과가 아니다. +member resolver(python-multiplatform `ba4c6f49`)가 없는 바인더에서는 proxy 의 snake_case 메서드를 부르는 +5 개가 건너뛴다. 건너뜀은 통과가 아니다. 체크아웃 없이 통과하는 50 개 중 다수는 *부재*(옛 토큰·삭제된 파일이 없음)를 확인하는 것이라 기능 진척으로 세지 않는다. diff --git a/docs/SPEC.md b/docs/SPEC.md index 3fd8c73..5681a54 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -20,12 +20,14 @@ Run from a worktree with `python3 -m pytest tests -q` (pytest 8, CPython 3.13): | Environment | Result | |---|---| -| No `PythonMultiplatform` checkout found | **50 passed, 46 skipped**, 66 subtests passed | -| `PYTHONMULTIPLATFORM_HOME` → the `PythonMultiplatform` checkout | **92 passed, 4 skipped**, 72 subtests passed | +| No `PythonMultiplatform` checkout found | **50 passed, 47 skipped**, 66 subtests passed | +| python-multiplatform `develop` at `ba4c6f49` or later (has `add_member_resolver`) | **97 passed**, 72 subtests passed | +| an older checkout, without `add_member_resolver` | **92 passed, 5 skipped**, 72 subtests passed | -Without a checkout, the 46 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. +Without a checkout, the 47 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). A skip is not a +pass. Of the 50 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. @@ -107,14 +109,16 @@ Evidence: `tests/test_runtime_module.py::TheModuleIsTheFileOnDisk`, 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). +Methods on a proxy the binder returned (`m.fill_max_width()`) are attributes of the binder's own +class. The same rule reaches them as the binder's member resolver (`member_name`, registered through +`python_multiplatform.binding.add_member_resolver`, python-multiplatform `ba4c6f49`); the binder +caches the alias in its own registry, so its class keeps Kotlin names in `dir()` (issue #20). +Keyword arguments to such a method are still Kotlin's own (`m.padding(paddingValues=...)`): the +resolver maps names, not keywords. ## 4. Naming -### S4.1 Kotlin parameters, `snake_case` — `partial` +### S4.1 Kotlin parameters, `snake_case` — `partial` (method keywords) `onClick` → `on_click`, `fillMaxWidth` → `fill_max_width`, `zIndex` → `z_index`, `toURLString` → `to_url_string`; type names (`Modifier`) unchanged; an explicit overload keeps its @@ -122,8 +126,9 @@ 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. -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). +Implemented for module-level names and keywords and for method names on a binder proxy +(`tests/test_chain.py::Names`, 8 tests; `TheChain`). Partial because a method's keyword arguments are +still Kotlin's (§3). ### S4.2 The notebook's spellings are examples, not the contract — `implemented` as a rule @@ -167,16 +172,16 @@ the two colour-scheme factories take 36 `Color` parameters. The notebook uses `I ## 6. Modifiers — extension functions as methods -### S6.1 `Modifier` extensions are methods on the receiver proxy — `partial` +### S6.1 `Modifier` extensions are methods on the receiver proxy — `implemented` `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, 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. +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. ### S6.2 The empty-`Modifier` seam — `partial` @@ -195,14 +200,13 @@ declared type; a non-match names the candidates; an explicit overload spelling ( bypasses dispatch; the module function takes snake_case keywords. Evidence: `tests/test_chain.py::OverloadDispatch` (7 tests). Status: `implemented`. -### S6.4 Value classes — `partial` +### S6.4 Value classes — `implemented` 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) 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). +Runtime evidence: `tests/test_chain.py::ValueClasses` (6 tests). ### S6.5 Lazy resolution and handle lifetime — `implemented` diff --git a/docs/guide/getting-started.html b/docs/guide/getting-started.html index 9d75ff9..8983083 100644 --- a/docs/guide/getting-started.html +++ b/docs/guide/getting-started.html @@ -43,9 +43,9 @@
python3 -m pytest tests -q
-Expect 50 passed, 46 skipped. The skipped tests drive the binder's adaptation layer and need a python-multiplatform checkout beside this one, or:
50 passed, 46 skipped 가 정상입니다. 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 python-multiplatform 체크아웃이 있거나 다음이 필요합니다:
+Expect 50 passed, 47 skipped. The skipped tests drive the binder's adaptation layer and need a python-multiplatform checkout beside this one, or:
50 passed, 47 skipped 가 정상입니다. 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 python-multiplatform 체크아웃이 있거나 다음이 필요합니다:
PYTHONMULTIPLATFORM_HOME=/path/to/python-multiplatform python3 -m pytest tests -q
-With it, all but 4 pass; those 4 skip until the binder can resolve a snake_case method on a proxy it returned (python-multiplatform #17). That is expected and recorded.
지정하면 4 개를 뺀 전부가 통과합니다. 그 4 개는 바인더가 돌려준 proxy 의 snake_case 메서드를 바인더가 풀 수 있을 때까지(python-multiplatform #17) 건너뜁니다. 예상된 상태이며 기록되어 있습니다.
+With a current one (ba4c6f49 or later) all pass; an older one skips 5 that call a snake_case method on a proxy the binder returned. That is expected and recorded.
최신 체크아웃(ba4c6f49 이후)이면 전부 통과하고, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 건너뜁니다. 예상된 상태이며 기록되어 있습니다.
Implemented means code in this repository and a passing test that exercises it. Tests that only assert an absence are not counted.
구현은 이 저장소의 코드와 그것을 실행하는 통과 테스트가 함께 있다는 뜻입니다. 부재만 확인하는 테스트는 세지 않습니다.
| State상태 | Item항목 |
|---|---|
| implemented구현 | Mapping manifest pythonx-map.toml매핑 매니페스트 pythonx-map.toml |
| implemented구현 | @Composable identity decorator@Composable 항등 데코레이터 |
| partial부분 | Distribution as pythonx-compose — configured; no stubs yet, manifest outside the package directorypythonx-compose 배포 — 설정됨; 스텁 없음, 매니페스트가 패키지 디렉터리 밖 |
| implemented구현 | Modifier chains, overload dispatch, Dp as a numberModifier 체인, 오버로드 디스패치, 숫자 Dp |
| partial부분 | Snake-case names, keywords and signatures — module level done; methods on a returned proxy wait for python-multiplatform #17snake_case 이름·키워드·시그니처 — 모듈 수준 완료, 반환된 proxy 의 메서드는 python-multiplatform #17 대기 |
| partial부분 | Material 3 widgets — re-exported by rule; render proofs per widget pending (#9)Material 3 위젯 — 규칙으로 재노출됨; 위젯별 렌더 증거 대기(#9) |
| partial부분 | Icon, colour schemes — importable, not callableIcon, 색 스킴 — import 가능, 호출 불가 |
| partial부분 | Alignment / Arrangement constants — read without parentheses; grouped spelling pending (#9)Alignment / Arrangement 상수 — 괄호 없이 읽음; 묶음 표기 대기(#9) |
| implemented구현 | pythonx as a real on-disk package re-exporting androidx.compose.* by one ruleandroidx.compose.* 를 규칙 하나로 재노출하는 실제 디스크 패키지 pythonx |
| planned계획 | .pyi stubs in the wheelwheel 안의 .pyi 스텁 |
| planned계획 | remember_saveable, DefaultIcons, coroutine scopesremember_saveable, DefaultIcons, 코루틴 스코프 |
| implemented구현 | Mapping manifest pythonx-map.toml매핑 매니페스트 pythonx-map.toml |
| implemented구현 | @Composable identity decorator@Composable 항등 데코레이터 |
| partial부분 | Distribution as pythonx-compose — configured; no stubs yet, manifest outside the package directorypythonx-compose 배포 — 설정됨; 스텁 없음, 매니페스트가 패키지 디렉터리 밖 |
| implemented구현 | Modifier chains, overload dispatch, Dp as a numberModifier 체인, 오버로드 디스패치, 숫자 Dp |
| partial부분 | Snake-case names, keywords and signatures — done for modules and method names; method keywords are still Kotlin'ssnake_case 이름·키워드·시그니처 — 모듈과 메서드 이름 완료, 메서드 키워드는 아직 Kotlin 이름 |
| partial부분 | Material 3 widgets — re-exported by rule; render proofs per widget pending (#9)Material 3 위젯 — 규칙으로 재노출됨; 위젯별 렌더 증거 대기(#9) |
| partial부분 | Icon, colour schemes — importable, not callableIcon, 색 스킴 — import 가능, 호출 불가 |
| partial부분 | Alignment / Arrangement constants — read without parentheses; grouped spelling pending (#9)Alignment / Arrangement 상수 — 괄호 없이 읽음; 묶음 표기 대기(#9) |
| implemented구현 | pythonx as a real on-disk package re-exporting androidx.compose.* by one ruleandroidx.compose.* 를 규칙 하나로 재노출하는 실제 디스크 패키지 pythonx |
| planned계획 | .pyi stubs in the wheelwheel 안의 .pyi 스텁 |
| planned계획 | remember_saveable, DefaultIcons, coroutine scopesremember_saveable, DefaultIcons, 코루틴 스코프 |
Test baseline: 50 passed, 46 skipped without a python-multiplatform checkout; 92 passed, 4 skipped with one.
테스트 기준선: python-multiplatform 체크아웃이 없으면 50 passed, 46 skipped, 있으면 92 passed, 4 skipped.
Test baseline: 50 passed, 47 skipped without a python-multiplatform checkout; 97 passed with a current one.
테스트 기준선: python-multiplatform 체크아웃이 없으면 50 passed, 47 skipped, 최신 체크아웃이면 97 passed.