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
4 changes: 2 additions & 2 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 이름 없음)
Expand Down
8 changes: 4 additions & 4 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions docs/guide/getting-started.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ <h2><span data-lang="en">1. Get the source</span><span data-lang="ko">1. 소스
<section>
<h2><span data-lang="en">2. Run the tests</span><span data-lang="ko">2. 테스트 실행</span></h2>
<pre><code class="language-bash">uv run --with pytest --with mypy pytest tests -q</code></pre>
<p data-lang="en">Expect <strong>140 passed, 112 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>140 passed, 112 skipped</strong> 가 정상입니다. 건너뛴 테스트 하나는 클론에는 없는 메인테이너의 <code>UI.ipynb</code> 를 읽으며, mypy 는 스텁을 타입 검사합니다. 나머지 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> 체크아웃이 있거나 다음이 필요합니다:</p>
<p data-lang="en">Expect <strong>148 passed, 109 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>148 passed, 109 skipped</strong> 가 정상입니다. 건너뛴 테스트 하나는 클론에는 없는 메인테이너의 <code>UI.ipynb</code> 를 읽으며, mypy 는 스텁을 타입 검사합니다. 나머지 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> 체크아웃이 있거나 다음이 필요합니다:</p>
<pre><code class="language-bash">PYTHONMULTIPLATFORM_HOME=/path/to/python-multiplatform uv run --with pytest --with mypy pytest tests -q</code></pre>
<p data-lang="en">With a current one (<code>31c092f0</code> or later) <strong>251 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>251 개가 통과</strong>하고 1 개(<code>UI.ipynb</code> 테스트)를 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 <strong>건너뜁니다</strong>. 예상된 상태이며 기록되어 있습니다.</p>
<p data-lang="en">With a current one (<code>31c092f0</code> or later) <strong>256 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>256 개가 통과</strong>하고 1 개(<code>UI.ipynb</code> 테스트)를 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 <strong>건너뜁니다</strong>. 예상된 상태이며 기록되어 있습니다.</p>
</section>

<section>
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/status.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
<thead><tr><th><span data-lang="en">State</span><span data-lang="ko">상태</span></th><th><span data-lang="en">Item</span><span data-lang="ko">항목</span></th></tr></thead>
<tbody><tr><td><span class="chip ok"><span data-lang="en">implemented</span><span data-lang="ko">구현</span></span></td><td><span data-lang="en">Mapping manifest <code>pythonx/compose/pythonx-map.toml</code>: inside the package, ships in the wheel</span><span data-lang="ko">매핑 매니페스트 <code>pythonx/compose/pythonx-map.toml</code>: 패키지 안에 있으며 wheel 에 포함</span></td></tr><tr><td><span class="chip ok"><span data-lang="en">implemented</span><span data-lang="ko">구현</span></span></td><td><span data-lang="en"><code>@Composable</code> identity decorator</span><span data-lang="ko"><code>@Composable</code> 항등 데코레이터</span></td></tr><tr><td><span class="chip ok"><span data-lang="en">implemented</span><span data-lang="ko">구현</span></span></td><td><span data-lang="en"><code>pythonx</code> as a real on-disk package re-exporting <code>androidx.compose.*</code> by one rule: names, keywords, signatures, <code>Dp</code> allowlist</span><span data-lang="ko"><code>androidx.compose.*</code> 를 규칙 하나로 재노출하는 실제 디스크 패키지 <code>pythonx</code>: 이름, 키워드, 시그니처, <code>Dp</code> 허용 목록</span></td></tr><tr><td><span class="chip ok"><span data-lang="en">implemented</span><span data-lang="ko">구현</span></span></td><td><span data-lang="en"><code>Modifier</code> chains with snake_case methods, overload dispatch, <code>Dp</code> as a number</span><span data-lang="ko">snake_case 메서드를 쓰는 <code>Modifier</code> 체인, 오버로드 디스패치, 숫자 <code>Dp</code></span></td></tr><tr><td><span class="chip ok"><span data-lang="en">implemented</span><span data-lang="ko">구현</span></span></td><td><span data-lang="en"><code>Column</code>, <code>Row</code>, <code>Spacer</code> importable from <code>material3</code> as well as <code>layout</code> (manifest <code>[aliases]</code>)</span><span data-lang="ko"><code>layout</code> 뿐 아니라 <code>material3</code> 에서도 import 되는 <code>Column</code>, <code>Row</code>, <code>Spacer</code> (매니페스트 <code>[aliases]</code>)</span></td></tr><tr><td><span class="chip ok"><span data-lang="en">implemented</span><span data-lang="ko">구현</span></span></td><td><span data-lang="en">Type stubs (<code>.pyi</code>) generated from real Compose 1.11.1 by the runtime's naming rule, with <code>py.typed</code>, checked with mypy (#12); many types are still <code>Any</code></span><span data-lang="ko">런타임과 같은 이름 규칙으로 실제 Compose 1.11.1 에서 생성한 타입 스텁(<code>.pyi</code>)과 <code>py.typed</code>, mypy 로 검사(#12); 아직 많은 타입이 <code>Any</code> 입니다</span></td></tr><tr><td><span class="chip ok"><span data-lang="en">implemented</span><span data-lang="ko">구현</span></span></td><td><span data-lang="en">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 time</span><span data-lang="ko">snake_case 이름·키워드·시그니처: 모듈과 메서드; 프록시의 이름과 키워드는 python-multiplatform #131 부터 바인더가 제공하고(오래된 바인더는 Kotlin 이름으로 씁니다), Kotlin 이름도 실행 시에는 그대로 통합니다</span></td></tr><tr><td><span class="chip partial"><span data-lang="en">partial</span><span data-lang="ko">부분</span></span></td><td><span data-lang="en">The empty <code>Modifier</code>: <code>Modifier.padding(16)</code> from the class needs an app-supplied factory against real Compose</span><span data-lang="ko">빈 <code>Modifier</code>: 클래스에서 시작하는 <code>Modifier.padding(16)</code> 은 실제 Compose 에서 앱이 제공하는 팩토리가 필요</span></td></tr><tr><td><span class="chip ok"><span data-lang="en">implemented</span><span data-lang="ko">구현</span></span></td><td><span data-lang="en"><code>Alignment</code> / <code>Arrangement</code>: <code>Alignment.Center</code>, <code>Arrangement.End</code>, <code>Arrangement.spaced_by(8)</code>, and the grouped <code>Alignment.Horizontal.End</code> beside <code>Alignment.End</code></span><span data-lang="ko"><code>Alignment</code> / <code>Arrangement</code>: <code>Alignment.Center</code>, <code>Arrangement.End</code>, <code>Arrangement.spaced_by(8)</code>, 그리고 <code>Alignment.End</code> 와 나란히 묶음 표기 <code>Alignment.Horizontal.End</code></span></td></tr><tr><td><span class="chip partial"><span data-lang="en">partial</span><span data-lang="ko">부분</span></span></td><td><span data-lang="en">Material 3 widgets: re-exported by rule; render proofs per widget pending (#9)</span><span data-lang="ko">Material 3 위젯: 규칙으로 재노출됨; 위젯별 렌더 증거 대기(#9)</span></td></tr><tr><td><span class="chip partial"><span data-lang="en">partial</span><span data-lang="ko">부분</span></span></td><td><span data-lang="en">Distribution as <code>pythonx-compose</code>: the wheel carries the manifest, the re-export rule, the type stubs and <code>py.typed</code></span><span data-lang="ko"><code>pythonx-compose</code> 배포: wheel 에 매니페스트, 재노출 규칙, 타입 스텁, <code>py.typed</code> 포함</span></td></tr><tr><td><span class="chip partial"><span data-lang="en">partial</span><span data-lang="ko">부분</span></span></td><td><span data-lang="en">A declared app root (<code>@app</code>) and Pythonic state (<code>state</code>), with no update call: binder path tested against a fake host shaped after python-multiplatform #38; real Compose is the E2E module (#11, #19)</span><span data-lang="ko">갱신 호출 없는 선언형 앱 루트(<code>@app</code>)와 파이썬다운 상태(<code>state</code>): 바인더 경로는 python-multiplatform #38 모양의 가짜 호스트로 테스트됨, 실제 Compose 는 E2E 모듈(#11, #19)</span></td></tr><tr><td><span class="chip partial"><span data-lang="en">partial</span><span data-lang="ko">부분</span></span></td><td><span data-lang="en"><code>DefaultIcons</code> is <code>Icons.Default</code>, written <code>DefaultIcons.Add</code>, tested against a fake host shaped after python-multiplatform #37/#38; the real draw is in python-multiplatform's render test</span><span data-lang="ko"><code>DefaultIcons</code> 는 <code>Icons.Default</code> 이며 <code>DefaultIcons.Add</code> 로 씁니다. python-multiplatform #37/#38 모양의 가짜 호스트로 테스트됨; 실제 그리기는 python-multiplatform 의 렌더 테스트에 있음</span></td></tr><tr><td><span class="chip partial"><span data-lang="en">partial</span><span data-lang="ko">부분</span></span></td><td><span data-lang="en"><code>TextField(state=…)</code> with <code>TextFieldState</code> from <code>pythonx.compose.foundation.text.input</code>: tested against a fake host shaped after python-multiplatform #73; the input-method (IME) composing proof is python-multiplatform E2E #26 (#10)</span><span data-lang="ko"><code>pythonx.compose.foundation.text.input</code> 의 <code>TextFieldState</code> 를 쓰는 <code>TextField(state=…)</code>: python-multiplatform #73 모양의 가짜 호스트로 테스트됨; 입력기(IME) 조합 증거는 python-multiplatform E2E #26 (#10)</span></td></tr><tr><td><span class="chip planned"><span data-lang="en">planned</span><span data-lang="ko">계획</span></span></td><td><span data-lang="en">Colour schemes (36 colour parameters)</span><span data-lang="ko">색 스킴 (색 매개변수 36 개)</span></td></tr><tr><td><span class="chip planned"><span data-lang="en">planned</span><span data-lang="ko">계획</span></span></td><td><span data-lang="en"><code>remember_saveable</code>, coroutine scopes</span><span data-lang="ko"><code>remember_saveable</code>, 코루틴 스코프</span></td></tr></tbody>
</table></div>
<p data-lang="en" class="muted">Test baseline: <code>140 passed, 112 skipped</code> without a python-multiplatform checkout; <code>251 passed, 1 skipped</code> with a current one. The one skip with a current checkout reads the maintainer's <code>UI.ipynb</code>.</p><p data-lang="ko" class="muted">테스트 기준선: python-multiplatform 체크아웃이 없으면 <code>132 passed, 99 skipped</code>, 최신 체크아웃이면 <code>251 passed, 1 skipped</code>. 최신 체크아웃에서 건너뛰는 1 개는 메인테이너의 <code>UI.ipynb</code> 를 읽는 테스트입니다.</p>
<p data-lang="en" class="muted">Test baseline: <code>148 passed, 109 skipped</code> without a python-multiplatform checkout; <code>256 passed, 1 skipped</code> with a current one. The one skip with a current checkout reads the maintainer's <code>UI.ipynb</code>.</p><p data-lang="ko" class="muted">테스트 기준선: python-multiplatform 체크아웃이 없으면 <code>132 passed, 99 skipped</code>, 최신 체크아웃이면 <code>256 passed, 1 skipped</code>. 최신 체크아웃에서 건너뛰는 1 개는 메인테이너의 <code>UI.ipynb</code> 를 읽는 테스트입니다.</p>
</section>
<section>
<h2>FAQ</h2>
Expand Down
6 changes: 1 addition & 5 deletions pythonx/compose/ui/graphics/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down
1 change: 1 addition & 0 deletions scripts/gen_stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
46 changes: 46 additions & 0 deletions tests/test_stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading