diff --git a/PROJECT.md b/PROJECT.md index 4ed2a79..749892a 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -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 개 중 다수는 *부재*(옛 토큰·삭제된 파일이 없음)를 확인하는 것이라 기능 진척으로 세지 않는다. ### 마일스톤 diff --git a/docs/SPEC.md b/docs/SPEC.md index 65b092a..a07b154 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -20,8 +20,8 @@ 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. @@ -29,7 +29,7 @@ Against a binder older than `ba4c6f49`, the 5 that call a snake_case method on a 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. --- diff --git a/docs/guide/getting-started.html b/docs/guide/getting-started.html index b5a54f0..cdd7177 100644 --- a/docs/guide/getting-started.html +++ b/docs/guide/getting-started.html @@ -43,7 +43,7 @@

1. Get the source1. 소스

2. Run the tests2. 테스트 실행

python3 -m pytest tests -q
-

Expect 53 passed, 56 skipped. The skipped tests drive the binder's adaptation layer and need a python-multiplatform checkout beside this one, or:

53 passed, 56 skipped 가 정상입니다. 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 python-multiplatform 체크아웃이 있거나 다음이 필요합니다:

+

Expect 55 passed, 56 skipped. The skipped tests drive the binder's adaptation layer and need a python-multiplatform checkout beside this one, or:

55 passed, 56 skipped 가 정상입니다. 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 python-multiplatform 체크아웃이 있거나 다음이 필요합니다:

PYTHONMULTIPLATFORM_HOME=/path/to/python-multiplatform python3 -m pytest tests -q

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 개를 건너뜁니다. 예상된 상태이며 기록되어 있습니다.

diff --git a/docs/guide/status.html b/docs/guide/status.html index ef4e2e4..220a8c2 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])partial부분Snake-case names, keywords and signatures — done for modules and method names; method keywords are still Kotlin'ssnake_case 이름·키워드·시그니처 — 모듈과 메서드 이름 완료, 메서드 키워드는 아직 Kotlin 이름partial부분The empty Modifier — Modifier.padding(16) from the class needs an app-supplied factory against real Compose빈 Modifier — 클래스에서 시작하는 Modifier.padding(16) 은 실제 Compose 에서 앱이 제공하는 팩토리가 필요partial부분Alignment / Arrangement — Alignment.Center, Arrangement.End, Arrangement.spaced_by(8) work; grouped Alignment.Horizontal.End pendingAlignment / Arrangement — Alignment.Center, Arrangement.End, Arrangement.spaced_by(8) 동작; 묶음 표기 Alignment.Horizontal.End 대기partial부분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 and the re-export rule; no stubs from real Compose and no py.typed yet (#12)pythonx-compose 배포 — wheel 에 매니페스트와 재노출 규칙 포함; 실제 Compose 스텁과 py.typed 는 아직 없음(#12)planned계획A declared app root and Pythonic state, with no update call (#11)갱신 호출 없는 선언형 앱 루트와 파이썬다운 상태(#11)planned계획TextField input-method (IME) handling (#10)TextField 입력기(IME) 처리(#10)planned계획Icon, DefaultIcons (python-multiplatform #37), colour schemesIcon, DefaultIcons (python-multiplatform #37), 색 스킴planned계획remember_saveable, coroutine scopesremember_saveable, 코루틴 스코프 -

Test baseline: 53 passed, 56 skipped without a python-multiplatform checkout; 109 passed with a current one.

테스트 기준선: python-multiplatform 체크아웃이 없으면 53 passed, 56 skipped, 최신 체크아웃이면 109 passed.

+

Test baseline: 55 passed, 56 skipped without a python-multiplatform checkout; 111 passed with a current one.

테스트 기준선: python-multiplatform 체크아웃이 없으면 55 passed, 56 skipped, 최신 체크아웃이면 111 passed.

FAQ

diff --git a/pythonx/compose/_reexport.py b/pythonx/compose/_reexport.py index 552f6f2..834ff1c 100644 --- a/pythonx/compose/_reexport.py +++ b/pythonx/compose/_reexport.py @@ -39,6 +39,7 @@ from __future__ import annotations import importlib +import importlib.util import inspect import re import sys @@ -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) diff --git a/tests/test_chain.py b/tests/test_chain.py index 9ae5cfc..8fdbeb3 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -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."""