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
6 changes: 3 additions & 3 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **`

```
python3 -m pytest tests -q
PythonMultiplatform 체크아웃 없음 53 passed, 56 skipped
python-multiplatform develop ba4c6f49+ 109 passed
PythonMultiplatform 체크아웃 없음 55 passed, 56 skipped
python-multiplatform develop ba4c6f49+ 111 passed
그보다 오래된 체크아웃 같은 수, 그중 5 개 건너뜀 (member resolver 없음)
```

member resolver(python-multiplatform `ba4c6f49`)가 없는 바인더에서는 proxy 의 snake_case 메서드를 부르는
5 개가 건너뛴다. 건너뜀은 통과가 아니다.
체크아웃 없이 통과하는 53 개 중 다수는 *부재*(옛 토큰·삭제된 파일이 없음)를 확인하는 것이라 기능 진척으로 세지
체크아웃 없이 통과하는 55 개 중 다수는 *부재*(옛 토큰·삭제된 파일이 없음)를 확인하는 것이라 기능 진척으로 세지
않는다.

### 마일스톤
Expand Down
6 changes: 3 additions & 3 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ Run from a worktree with `python3 -m pytest tests -q` (pytest 8, CPython 3.13):

| Environment | Result |
|---|---|
| No `PythonMultiplatform` checkout found | **53 passed, 56 skipped**, 66 subtests passed |
| python-multiplatform `develop` at `ba4c6f49` or later (has `add_member_resolver`) | **109 passed**, 72 subtests passed |
| No `PythonMultiplatform` checkout found | **55 passed, 56 skipped**, 66 subtests passed |
| python-multiplatform `develop` at `ba4c6f49` or later (has `add_member_resolver`) | **111 passed**, 72 subtests passed |
| an older checkout, without `add_member_resolver` | the same, with 5 of them skipped |

Without a checkout, the 56 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 53 that pass without a checkout, most assert **absence** (a retired token, a deleted file, a docstring that
Of the 55 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.

---
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/getting-started.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ <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">python3 -m pytest tests -q</code></pre>
<p data-lang="en">Expect <strong>53 passed, 56 skipped</strong>. The 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>53 passed, 56 skipped</strong> 가 정상입니다. 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> 체크아웃이 있거나 다음이 필요합니다:</p>
<p data-lang="en">Expect <strong>55 passed, 56 skipped</strong>. The 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>55 passed, 56 skipped</strong> 가 정상입니다. 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> 체크아웃이 있거나 다음이 필요합니다:</p>
<pre><code class="language-bash">PYTHONMULTIPLATFORM_HOME=/path/to/python-multiplatform python3 -m pytest tests -q</code></pre>
<p data-lang="en">With a current one (<code>ba4c6f49</code> 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.</p><p data-lang="ko">최신 체크아웃(<code>ba4c6f49</code> 이후)이면 전부 통과하고, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 <strong>건너뜁니다</strong>. 예상된 상태이며 기록되어 있습니다.</p>
</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 partial"><span data-lang="en">partial</span><span data-lang="ko">부분</span></span></td><td><span data-lang="en">Snake-case names, keywords and signatures — done for modules and method names; method keywords are still Kotlin's</span><span data-lang="ko">snake_case 이름·키워드·시그니처 — 모듈과 메서드 이름 완료, 메서드 키워드는 아직 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 partial"><span data-lang="en">partial</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> work; grouped <code>Alignment.Horizontal.End</code> pending</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.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 and the re-export rule; no stubs from real Compose and no <code>py.typed</code> yet (#12)</span><span data-lang="ko"><code>pythonx-compose</code> 배포 — wheel 에 매니페스트와 재노출 규칙 포함; 실제 Compose 스텁과 <code>py.typed</code> 는 아직 없음(#12)</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">A declared app root and Pythonic state, with no update call (#11)</span><span data-lang="ko">갱신 호출 없는 선언형 앱 루트와 파이썬다운 상태(#11)</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>TextField</code> input-method (IME) handling (#10)</span><span data-lang="ko"><code>TextField</code> 입력기(IME) 처리(#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"><code>Icon</code>, <code>DefaultIcons</code> (python-multiplatform #37), colour schemes</span><span data-lang="ko"><code>Icon</code>, <code>DefaultIcons</code> (python-multiplatform #37), 색 스킴</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>53 passed, 56 skipped</code> without a python-multiplatform checkout; <code>109 passed</code> with a current one.</p><p data-lang="ko" class="muted">테스트 기준선: python-multiplatform 체크아웃이 없으면 <code>53 passed, 56 skipped</code>, 최신 체크아웃이면 <code>109 passed</code>.</p>
<p data-lang="en" class="muted">Test baseline: <code>55 passed, 56 skipped</code> without a python-multiplatform checkout; <code>111 passed</code> with a current one.</p><p data-lang="ko" class="muted">테스트 기준선: python-multiplatform 체크아웃이 없으면 <code>55 passed, 56 skipped</code>, 최신 체크아웃이면 <code>111 passed</code>.</p>
</section>
<section>
<h2>FAQ</h2>
Expand Down
6 changes: 6 additions & 0 deletions pythonx/compose/_reexport.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from __future__ import annotations

import importlib
import importlib.util
import inspect
import re
import sys
Expand Down Expand Up @@ -299,6 +300,11 @@ def reexport(module_name: str):
def __getattr__(name):
if name.startswith("__") and name.endswith("__"):
raise AttributeError(name)
if importlib.util.find_spec(module_name + "." + name) is not None:
# A submodule file of this package (`ui.modifier`). `from package import name` asks for
# the attribute before it imports a submodule, and only an AttributeError lets it
# fall back -- so this has to answer before anything that needs the binder.
return importlib.import_module(module_name + "." + name)
source = _aliases(module_name).get(name)
if source is not None:
value = getattr(importlib.import_module(source), name)
Expand Down
27 changes: 27 additions & 0 deletions tests/test_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,33 @@ def test_a_name_not_listed_is_not_borrowed(self):
material3.padding_values_of # noqa: B018


class SubmodulesWithoutABinder(unittest.TestCase):
"""`from pythonx.compose.ui import modifier` reaches the file even when no binder is installed.

`from package import name` asks the package for the attribute first and only imports a
submodule if that raises AttributeError. The re-export `__getattr__` raised RuntimeError ("the
binding layer is not installed") for every name, so the submodule was never tried.
"""

def setUp(self):
adapter_loader.uninstall()
self.addCleanup(adapter_loader.uninstall)
root = str(Path(__file__).resolve().parents[1])
if root not in sys.path:
sys.path.insert(0, root)

def test_from_import_of_a_submodule_needs_no_binder(self):
from pythonx.compose.ui import modifier

self.assertTrue(callable(modifier.install))

def test_a_kotlin_name_still_says_the_binder_is_missing(self):
import pythonx.compose.ui as ui

with self.assertRaises(RuntimeError):
ui.Modifier # noqa: B018


class Handles(AdapterCase):
"""The binder's proxy owns a handle; dropping it gives the handle back."""

Expand Down
Loading