diff --git a/PROJECT.md b/PROJECT.md index 178a568..5879281 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -35,8 +35,8 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **` ``` uv run --with pytest --with mypy pytest tests -q - PythonMultiplatform 체크아웃 없음 140 passed, 112 skipped - python-multiplatform develop 31c092f0+ 251 passed, 1 skipped + PythonMultiplatform 체크아웃 없음 148 passed, 109 skipped + python-multiplatform develop 31c092f0+ 256 passed, 1 skipped describe_member 없는 체크아웃 같은 수, 그중 7 개 건너뜀 (python-multiplatform #54 이전) describe(module, name) 없는 체크아웃 같은 수, 그중 13 개 건너뜀 (python-multiplatform #36 이전) 그보다 오래된 체크아웃 거기에 5 개 더 건너뜀 (#131 이전, snake_case 이름 없음) diff --git a/docs/SPEC.md b/docs/SPEC.md index 61eeca2..55ec7fa 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -21,21 +21,21 @@ Run from a worktree with `uv run --with pytest --with mypy pytest tests -q -rs` | Environment | Result | |---|---| -| No `PythonMultiplatform` checkout found | **140 passed, 112 skipped**, 167 subtests passed | -| python-multiplatform `develop` at `31c092f0` or later (property rows, #38; `describe_member`, #54; besides `add_member_resolver` and `describe(module, name)`) | **251 passed, 1 skipped**, 198 subtests passed | +| No `PythonMultiplatform` checkout found | **148 passed, 109 skipped**, 167 subtests passed | +| python-multiplatform `develop` at `31c092f0` or later (property rows, #38; `describe_member`, #54; besides `add_member_resolver` and `describe(module, name)`) | **256 passed, 1 skipped**, 198 subtests passed | | an older checkout, before python-multiplatform #131 (no binder-side snake_case names) | the same, with the snake_case method tests skipped | | an older one, without `describe(module, name)` (python-multiplatform #36) | with 13 more skipped again | | an older one still, without `add_member_resolver` | with 5 more skipped again | Measured in a worktree, which has `.tmp/kotlin-stubs.zip` but no `UI.ipynb`. Without a checkout, the -110 tests that install the binder's layers through `tests/adapter.py` skip, plus the notebook test. +108 tests that install the binder's layers through `tests/adapter.py` skip, plus the notebook test. Against a binder without `describe(module, name)`, the 13 that check grouped constants (§7, S7.1) skip. Against a binder before python-multiplatform #131, the tests that call a snake_case method or pass a snake_case keyword on a proxy the binder returned skip, because those names are the binder's. The skip against a current checkout is `UI.ipynb`'s (§2). A binder older than #38 skips the 5 `TheBinderPath` tests (S5.4). A binder without the `@Composable` binding (`push_composer`) skips the 13 text-field tests (S5.5). `tests/test_typing.py` skips where mypy is not installed. A skip is not a pass. -Of the 140 that pass without a checkout, 73 check the type stubs and the wheel (S1.2); most of the +Of the 148 that pass without a checkout, 73 check the type stubs and the wheel (S1.2); most of the rest assert **absence** (a retired token, a deleted file, a docstring that exists). Those are listed in §9 and are not counted as features. diff --git a/docs/guide/getting-started.html b/docs/guide/getting-started.html index 946011a..2e63831 100644 --- a/docs/guide/getting-started.html +++ b/docs/guide/getting-started.html @@ -41,9 +41,9 @@

1. Get the source1. 소스

2. Run the tests2. 테스트 실행

uv run --with pytest --with mypy pytest tests -q
-

Expect 140 passed, 112 skipped. One skipped test reads the maintainer's UI.ipynb, which a clone does not have; mypy type-checks the stubs. The other skipped tests drive the binder's adaptation layer and need a python-multiplatform checkout beside this one, or:

140 passed, 112 skipped 가 정상입니다. 건너뛴 테스트 하나는 클론에는 없는 메인테이너의 UI.ipynb 를 읽으며, mypy 는 스텁을 타입 검사합니다. 나머지 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 python-multiplatform 체크아웃이 있거나 다음이 필요합니다:

+

Expect 148 passed, 109 skipped. One skipped test reads the maintainer's UI.ipynb, which a clone does not have; mypy type-checks the stubs. The other skipped tests drive the binder's adaptation layer and need a python-multiplatform checkout beside this one, or:

148 passed, 109 skipped 가 정상입니다. 건너뛴 테스트 하나는 클론에는 없는 메인테이너의 UI.ipynb 를 읽으며, mypy 는 스텁을 타입 검사합니다. 나머지 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 python-multiplatform 체크아웃이 있거나 다음이 필요합니다:

PYTHONMULTIPLATFORM_HOME=/path/to/python-multiplatform uv run --with pytest --with mypy pytest tests -q
-

With a current one (31c092f0 or later) 251 pass and 1 skips (the UI.ipynb test); an older one skips 5 that call a snake_case method on a proxy the binder returned. That is expected and recorded.

최신 체크아웃(31c092f0 이후)이면 251 개가 통과하고 1 개(UI.ipynb 테스트)를 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 건너뜁니다. 예상된 상태이며 기록되어 있습니다.

+

With a current one (31c092f0 or later) 256 pass and 1 skips (the UI.ipynb test); an older one skips 5 that call a snake_case method on a proxy the binder returned. That is expected and recorded.

최신 체크아웃(31c092f0 이후)이면 256 개가 통과하고 1 개(UI.ipynb 테스트)를 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 건너뜁니다. 예상된 상태이며 기록되어 있습니다.

diff --git a/docs/guide/status.html b/docs/guide/status.html index 4f8e828..05055b8 100644 --- a/docs/guide/status.html +++ b/docs/guide/status.html @@ -36,7 +36,7 @@ State상태Item항목 implemented구현Mapping manifest pythonx/compose/pythonx-map.toml: inside the package, ships in the wheel매핑 매니페스트 pythonx/compose/pythonx-map.toml: 패키지 안에 있으며 wheel 에 포함implemented구현@Composable identity decorator@Composable 항등 데코레이터implemented구현pythonx as a real on-disk package re-exporting androidx.compose.* by one rule: names, keywords, signatures, Dp allowlistandroidx.compose.* 를 규칙 하나로 재노출하는 실제 디스크 패키지 pythonx: 이름, 키워드, 시그니처, Dp 허용 목록implemented구현Modifier chains with snake_case methods, overload dispatch, Dp as a numbersnake_case 메서드를 쓰는 Modifier 체인, 오버로드 디스패치, 숫자 Dpimplemented구현Column, Row, Spacer importable from material3 as well as layout (manifest [aliases])layout 뿐 아니라 material3 에서도 import 되는 Column, Row, Spacer (매니페스트 [aliases])implemented구현Type stubs (.pyi) generated from real Compose 1.11.1 by the runtime's naming rule, with py.typed, checked with mypy (#12); many types are still Any런타임과 같은 이름 규칙으로 실제 Compose 1.11.1 에서 생성한 타입 스텁(.pyi)과 py.typed, mypy 로 검사(#12); 아직 많은 타입이 Any 입니다implemented구현Snake-case names, keywords and signatures: modules and methods; the names and keywords on a proxy are the binder's since python-multiplatform #131 (an older binder spells them in Kotlin), and Kotlin's own spelling still works at run timesnake_case 이름·키워드·시그니처: 모듈과 메서드; 프록시의 이름과 키워드는 python-multiplatform #131 부터 바인더가 제공하고(오래된 바인더는 Kotlin 이름으로 씁니다), Kotlin 이름도 실행 시에는 그대로 통합니다partial부분The empty Modifier: Modifier.padding(16) from the class needs an app-supplied factory against real Compose빈 Modifier: 클래스에서 시작하는 Modifier.padding(16) 은 실제 Compose 에서 앱이 제공하는 팩토리가 필요implemented구현Alignment / Arrangement: Alignment.Center, Arrangement.End, Arrangement.spaced_by(8), and the grouped Alignment.Horizontal.End beside Alignment.EndAlignment / Arrangement: Alignment.Center, Arrangement.End, Arrangement.spaced_by(8), 그리고 Alignment.End 와 나란히 묶음 표기 Alignment.Horizontal.Endpartial부분Material 3 widgets: re-exported by rule; render proofs per widget pending (#9)Material 3 위젯: 규칙으로 재노출됨; 위젯별 렌더 증거 대기(#9)partial부분Distribution as pythonx-compose: the wheel carries the manifest, the re-export rule, the type stubs and py.typedpythonx-compose 배포: wheel 에 매니페스트, 재노출 규칙, 타입 스텁, py.typed 포함partial부분A declared app root (@app) and Pythonic state (state), with no update call: binder path tested against a fake host shaped after python-multiplatform #38; real Compose is the E2E module (#11, #19)갱신 호출 없는 선언형 앱 루트(@app)와 파이썬다운 상태(state): 바인더 경로는 python-multiplatform #38 모양의 가짜 호스트로 테스트됨, 실제 Compose 는 E2E 모듈(#11, #19)partial부분DefaultIcons is Icons.Default, written DefaultIcons.Add, tested against a fake host shaped after python-multiplatform #37/#38; the real draw is in python-multiplatform's render testDefaultIcons 는 Icons.Default 이며 DefaultIcons.Add 로 씁니다. python-multiplatform #37/#38 모양의 가짜 호스트로 테스트됨; 실제 그리기는 python-multiplatform 의 렌더 테스트에 있음partial부분TextField(state=…) with TextFieldState from pythonx.compose.foundation.text.input: tested against a fake host shaped after python-multiplatform #73; the input-method (IME) composing proof is python-multiplatform E2E #26 (#10)pythonx.compose.foundation.text.input 의 TextFieldState 를 쓰는 TextField(state=…): python-multiplatform #73 모양의 가짜 호스트로 테스트됨; 입력기(IME) 조합 증거는 python-multiplatform E2E #26 (#10)planned계획Colour schemes (36 colour parameters)색 스킴 (색 매개변수 36 개)planned계획remember_saveable, coroutine scopesremember_saveable, 코루틴 스코프 -

Test baseline: 140 passed, 112 skipped without a python-multiplatform checkout; 251 passed, 1 skipped with a current one. The one skip with a current checkout reads the maintainer's UI.ipynb.

테스트 기준선: python-multiplatform 체크아웃이 없으면 132 passed, 99 skipped, 최신 체크아웃이면 251 passed, 1 skipped. 최신 체크아웃에서 건너뛰는 1 개는 메인테이너의 UI.ipynb 를 읽는 테스트입니다.

+

Test baseline: 148 passed, 109 skipped without a python-multiplatform checkout; 256 passed, 1 skipped with a current one. The one skip with a current checkout reads the maintainer's UI.ipynb.

테스트 기준선: python-multiplatform 체크아웃이 없으면 132 passed, 99 skipped, 최신 체크아웃이면 256 passed, 1 skipped. 최신 체크아웃에서 건너뛰는 1 개는 메인테이너의 UI.ipynb 를 읽는 테스트입니다.

FAQ

diff --git a/pythonx/compose/ui/graphics/__init__.pyi b/pythonx/compose/ui/graphics/__init__.pyi index 7e1a297..6704f9a 100644 --- a/pythonx/compose/ui/graphics/__init__.pyi +++ b/pythonx/compose/ui/graphics/__init__.pyi @@ -739,7 +739,7 @@ class PathOperation: """Kotlin property: androidx.compose.ui.graphics.intersect (androidx.compose.ui.graphics.PathOperation.Companion.intersect: androidx.compose.ui.graphics.PathOperation)""" ... @property - def reverseDifference(self) -> _t.Any: + def reverse_difference(self) -> _t.Any: """Kotlin property: androidx.compose.ui.graphics.reverseDifference (androidx.compose.ui.graphics.PathOperation.Companion.reverseDifference: androidx.compose.ui.graphics.PathOperation)""" ... @property @@ -750,10 +750,6 @@ class PathOperation: def xor(self) -> _t.Any: """Kotlin property: androidx.compose.ui.graphics.xor (androidx.compose.ui.graphics.PathOperation.Companion.xor: androidx.compose.ui.graphics.PathOperation)""" ... - @property - def reverse_difference(self) -> _t.Any: - """Kotlin property: androidx.compose.ui.graphics.reverseDifference (androidx.compose.ui.graphics.PathOperation.Companion.reverseDifference: androidx.compose.ui.graphics.PathOperation)""" - ... Difference: _t.ClassVar[_t.Any] """Kotlin: androidx.compose.ui.graphics.PathOperation.Difference(): androidx.compose.ui.graphics.PathOperation""" Intersect: _t.ClassVar[_t.Any] diff --git a/scripts/gen_stubs.py b/scripts/gen_stubs.py index ab29ed9..1473016 100644 --- a/scripts/gen_stubs.py +++ b/scripts/gen_stubs.py @@ -569,6 +569,7 @@ def _add_object(self, body: list[ast.stmt], kotlin_package: str, name: str, impo if isinstance(node, ast.ClassDef): # A Kotlin type the object nests (`Alignment.Horizontal`): a class of the folded object. node.bases = [bases.visit(base) for base in node.bases] + self._convert_class(node) # its members follow the same rule, at any depth groups.append([node]) elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name): node.target.id = python_name(node.target.id) diff --git a/tests/test_stubs.py b/tests/test_stubs.py index 16fa412..3f28c57 100644 --- a/tests/test_stubs.py +++ b/tests/test_stubs.py @@ -412,6 +412,52 @@ def test_a_class_with_no_such_constants_gets_no_groups(self): self.assertEqual({}, _nested(cls)) +class TheNestedCompanion(unittest.TestCase): + """Issue #86: a member of a class nested in an object's class is renamed like any other member.""" + + OBJECT = ( + "import typing as _t\n\n" + "class Companion:\n" + " \"\"\"Kotlin: a.b.Op.Companion\"\"\"\n" + " @property\n" + " def reverseDifference(self) -> _t.Any:\n" + " \"\"\"Kotlin property: a.b.Op.Companion.reverseDifference\"\"\"\n" + " ...\n" + " @property\n" + " def reverse_difference(self) -> _t.Any:\n" + " \"\"\"Kotlin property: a.b.Op.Companion.reverseDifference\"\"\"\n" + " ...\n" + " @property\n" + " def xor(self) -> _t.Any:\n" + " \"\"\"Kotlin property: a.b.Op.Companion.xor\"\"\"\n" + " ...\n" + " @property\n" + " def Upper(self) -> _t.Any:\n" + " \"\"\"Kotlin property: a.b.Op.Companion.Upper\"\"\"\n" + " ...\n" + " def doIt(self, value: int) -> _t.Any: ...\n" + "\n" + "class Inner:\n" + " \"\"\"Kotlin: a.b.Op.Inner\"\"\"\n" + " class Deeper:\n" + " \"\"\"Kotlin: a.b.Op.Inner.Deeper\"\"\"\n" + " def deepName(self) -> _t.Any: ...\n" + ) + + def convert(self) -> ast.ClassDef: + stubs = gen_stubs.KotlinStubs(KOTLIN_STUBS_V2) + stubs.files["a/b/Op/__init__.pyi"] = self.OBJECT + return _classes(gen_stubs.Converter(stubs).convert("import typing as _t\n", "a.b"))["Op"] + + def test_a_nested_classs_members_are_renamed_at_any_depth(self): + op = self.convert() + companion = _nested(op)["Companion"] + names = [n.name for n in companion.body if isinstance(n, ast.FunctionDef)] + self.assertEqual(["reverse_difference", "xor", "Upper", "do_it"], names) + deeper = _nested(_nested(op)["Inner"])["Deeper"] + self.assertEqual(["deep_name"], [n.name for n in deeper.body if isinstance(n, ast.FunctionDef)]) + + class TheBindersOwnAliases(unittest.TestCase): """The format of python-multiplatform #131: the binder writes its snake_case aliases itself.