From edab85216c3f27e6cb91b0c89cbabf01e4a3be3f Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:32:49 +0900 Subject: [PATCH 01/16] Chore: Drop protect-main.sh; main's protection is the maintainer's, by hand The maintainer's instruction (2026-10-03): no script creates or changes main's protection. The maintainer manages it in the repository settings, keeps main locked, and merges the release PR. protect-main.sh is deleted, and the release README and PROJECT.md say so. python-multiplatform did the same in its PR #127. deleted: .github/scripts/release/protect-main.sh. documentation corrected: .github/scripts/release/README.md, PROJECT.md. Release test: 49 passed, 0 failed (it never used the deleted script). Refs #68 --- .github/scripts/release/README.md | 12 +++------ .github/scripts/release/protect-main.sh | 35 ------------------------- PROJECT.md | 4 +-- 3 files changed, 5 insertions(+), 46 deletions(-) delete mode 100755 .github/scripts/release/protect-main.sh diff --git a/.github/scripts/release/README.md b/.github/scripts/release/README.md index 9288dc3..4d5a1ea 100644 --- a/.github/scripts/release/README.md +++ b/.github/scripts/release/README.md @@ -16,8 +16,7 @@ ignored and overwritten. `GITHUB_TOKEN`. This needs the repository setting *Actions -> General -> Allow GitHub Actions to create and approve pull requests*. No PAT is needed; an optional `RELEASE_PR_TOKEN` secret is used for the PR step when present. -3. Merging that PR is the only way to change `main`. The maintainer merges it after checking - that its source branch is `release`; repository settings alone protect `main` (below). +3. Merging that PR is the only way to change `main`, and the maintainer does it (below). 4. On push to `main`, `pages.yml` deploys `docs/guide/` to GitHub Pages. ## What is dropped on release @@ -40,10 +39,5 @@ It refuses to run while `release` is checked out. ## Protecting main - .github/scripts/release/protect-main.sh # print what would be applied - .github/scripts/release/protect-main.sh --apply # apply via gh api (needs admin) - -The settings (the maintainer's decision, 2026-10-03): a pull request is required with no -approving review, direct push, force-push and deletion are blocked, the branch is not locked, and -only administrators may merge. No workflow checks the source branch; the maintainer does, when -merging the release PR. +The maintainer manages `main`'s protection in the repository settings, by hand; `main` is locked. +No script or agent creates or changes those settings. The maintainer merges the release PR. diff --git a/.github/scripts/release/protect-main.sh b/.github/scripts/release/protect-main.sh deleted file mode 100755 index 4bc6514..0000000 --- a/.github/scripts/release/protect-main.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# Print (default) or apply (--apply) branch protection for main. -# Usage: protect-main.sh [--apply] [--repo OWNER/NAME] [--branch main] -set -eu -APPLY=0; REPO=""; BRANCH=main -while [ $# -gt 0 ]; do - case "$1" in - --apply) APPLY=1; shift ;; - --repo) REPO="$2"; shift 2 ;; - --branch) BRANCH="$2"; shift 2 ;; - *) echo "unknown argument: $1" >&2; exit 2 ;; - esac -done -[ -n "$REPO" ] || REPO="{owner}/{repo}" # gh expands these placeholders from the current repo - -BODY='{ - "required_status_checks": null, - "enforce_admins": false, - "required_pull_request_reviews": {"required_approving_review_count": 0}, - "restrictions": {"users": [], "teams": [], "apps": []}, - "allow_force_pushes": false, - "allow_deletions": false, - "lock_branch": false -}' -CMD="gh api --method PUT repos/$REPO/branches/$BRANCH/protection --input -" - -if [ "$APPLY" -eq 1 ]; then - printf '%s' "$BODY" | gh api --method PUT "repos/$REPO/branches/$BRANCH/protection" --input - - echo "applied protection to $BRANCH" -else - echo "[dry run] would run: $CMD" - echo "$BODY" - echo "Re-run with --apply to apply (needs admin). 'restrictions' with empty lists means only" - echo "administrators may merge; it is accepted for organisation repositories only." -fi diff --git a/PROJECT.md b/PROJECT.md index deabc5c..858264b 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -110,8 +110,8 @@ python3 .github/scripts/check_guide.py # 가이드: HTML 파싱, 링 2. **`pythonx-map.toml` 의 이중 표기** — `pythonx.compose.layout` 과 `pythonx.compose.foundation.layout` 를 둘 다 유지할지. 3. **릴리스·Pages 활성화** — `.github/scripts/release/` 와 `.github/workflows/`(release-sync, pages)는 - 들어와 있다. main 보호는 저장소 설정만으로 한다(PR 필수, 승인 0, 직접 push·force-push·삭제 - 금지, 머지는 관리자만; #64). 설정 적용과 release→main PR 머지는 메인테이너가 한다. + 들어와 있다. main 보호는 메인테이너가 저장소 설정에서 직접 관리하며 main 은 잠겨 있다. 스크립트나 + 에이전트는 보호 설정을 만들거나 바꾸지 않는다. release→main PR 머지는 메인테이너가 한다. ### 결정됨 (2026-10-03, #60) From 3804d95acfb8174c159bb1b553b0b7f61ee65c0a Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:41:57 +0900 Subject: [PATCH 02/16] Chore: Move the stub generator to scripts/, a root entry the maintainer approved The maintainer's decision (2026-10-03): pythonx/_build/gen_stubs.py moves to scripts/gen_stubs.py; scripts/ is a new top-level entry, approved. Its repo root path follows (one level up), and so do AGENTS.md (the approved root list and the path table), PROJECT.md, SPEC and tests/test_stubs.py. The committed stubs are regenerated so their header names the new path (one line each, nothing else changed). test added: test_no_build_tooling_or_tests_ship -- the wheel carries no scripts/, tests/, docs/ or .github/. documentation corrected: AGENTS.md section 2, PROJECT.md, SPEC, baselines. Suite (main checkout, notebook and zip): with python-multiplatform 26485a02, 252 passed; without a checkout, 141 passed, 111 skipped. check_guide: 0. Refs #71 --- {pythonx/_build => scripts}/gen_stubs.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {pythonx/_build => scripts}/gen_stubs.py (100%) diff --git a/pythonx/_build/gen_stubs.py b/scripts/gen_stubs.py similarity index 100% rename from pythonx/_build/gen_stubs.py rename to scripts/gen_stubs.py From 5592ecf3df6fe0e31e513b89fcfb1ed7f96af86a Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:42:17 +0900 Subject: [PATCH 03/16] Chore: Point every reference at scripts/gen_stubs.py, and keep scripts/ out of the wheel The previous commit carried only the rename (an explicit add named the already-removed pythonx/_build and failed). This carries the rest: the generator's repo root (one level up), AGENTS.md's approved root list and path table, PROJECT.md, SPEC, tests/test_stubs.py, the regenerated stubs' header line, the baselines, and test_no_build_tooling_or_tests_ship -- the wheel carries no scripts/, tests/, docs/ or .github/. test added: test_no_build_tooling_or_tests_ship. documentation corrected: AGENTS.md section 2, PROJECT.md, SPEC, guide baselines. Suite (main checkout, notebook and zip): with python-multiplatform 26485a02, 252 passed; without a checkout, 141 passed, 111 skipped. check_guide: 0. Refs #71 --- AGENTS.md | 7 ++++--- PROJECT.md | 8 ++++---- docs/SPEC.md | 12 ++++++------ docs/guide/getting-started.html | 4 ++-- docs/guide/status.html | 2 +- pythonx/compose/__init__.pyi | 2 +- pythonx/compose/foundation/__init__.pyi | 2 +- pythonx/compose/foundation/layout/__init__.pyi | 2 +- pythonx/compose/foundation/text/input/__init__.pyi | 2 +- pythonx/compose/layout/__init__.pyi | 2 +- pythonx/compose/material/__init__.pyi | 2 +- pythonx/compose/material/icons/__init__.pyi | 2 +- pythonx/compose/material3/__init__.pyi | 2 +- pythonx/compose/runtime/__init__.pyi | 2 +- pythonx/compose/ui/__init__.pyi | 2 +- pythonx/compose/ui/graphics/__init__.pyi | 2 +- pythonx/compose/ui/graphics/vector/__init__.pyi | 2 +- pythonx/compose/ui/text/__init__.pyi | 2 +- pythonx/compose/ui/unit/__init__.pyi | 2 +- scripts/gen_stubs.py | 6 +++--- tests/test_stubs.py | 4 ++-- tests/test_wheel.py | 5 +++++ 22 files changed, 41 insertions(+), 35 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8ab2f29..2a9ffe9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ files — lives **inside this repository's root directory.** | Temporary files | `.tmp/` (git-ignored); delete when done | | Benchmarks | `benchmarks/` | | CI scripts | `.github/scripts/` | -| Build tooling (stub generator) | `pythonx/_build/` (not shipped in the wheel) | +| Build tooling (stub generator) | `scripts/` (not shipped in the wheel) | Before writing a file, check that its absolute path starts with this repository's root. If it does not, stop. The only exceptions are a path the user names explicitly, and caches that build tools @@ -36,10 +36,11 @@ Writing to *another* repository is not an exception either. Do it only when told Do not create new folders or files at the repository root on your own. Work goes inside the existing modules and directories: source inside the package (`pythonx/`), CI scripts in -`.github/scripts/`, build helpers in `pythonx/_build/`, temporary files in the git-ignored `.tmp/`. +`.github/scripts/`, build tooling in `scripts/`, temporary files in the git-ignored `.tmp/`. The standing root entries are `pyproject.toml`, `README.md`, `LICENSE`, `PROJECT.md`, `AGENTS.md`, -`pythonx/`, `tests/`, `docs/`, `.github/`, `.gitignore` and `.gitattributes`, plus the maintainer's +`pythonx/`, `scripts/` (approved by the maintainer, 2026-10-03), `tests/`, `docs/`, `.github/`, +`.gitignore` and `.gitattributes`, plus the maintainer's untracked `UI.ipynb`. If a new top-level entry seems necessary, propose it — what it is, why, and why it cannot live inside an existing directory — and wait for approval. diff --git a/PROJECT.md b/PROJECT.md index 858264b..55e0e55 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -35,8 +35,8 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **` ``` python3 -m pytest tests -q - PythonMultiplatform 체크아웃 없음 139 passed, 112 skipped - python-multiplatform develop 31c092f0+ 250 passed, 1 skipped + PythonMultiplatform 체크아웃 없음 140 passed, 112 skipped + python-multiplatform develop 31c092f0+ 251 passed, 1 skipped describe_member 없는 체크아웃 같은 수, 그중 7 개 건너뜀 (python-multiplatform #54 이전) describe(module, name) 없는 체크아웃 같은 수, 그중 13 개 건너뜀 (python-multiplatform #36 이전) 그보다 오래된 체크아웃 거기에 5 개 더 건너뜀 (member resolver 없음) @@ -74,7 +74,7 @@ pythonx/compose/ import 패키지 (현재 대부분 독스트링만 있 ui/ modifier.py (빈 Modifier 등록 지점), alignment.py layout/ arrangement.py material3/ __init__.py 하나 (위젯은 모두 재노출 규칙으로) -pythonx/_build/ gen_stubs.py (스텁 생성기, wheel 에 싣지 않음) +scripts/ gen_stubs.py (스텁 생성기, wheel 에 싣지 않음) tests/ pytest (unittest 스타일), 유일한 테스트 폴더 docs/INTENT.md docs/SPEC.md docs/locale/ docs/guide/ .github/workflows/ CI @@ -118,7 +118,7 @@ python3 .github/scripts/check_guide.py # 가이드: HTML 파싱, 링 - **저장소 정리.** `test/`(2023–2024 샘플), `pythonx/compose/lite/`(은퇴한 JPype 프로토타입), 서브모듈 `pythonx/compose/native`(→ thisisthepy/swing-graalvm-demo @ 090f0190)는 삭제했고, 태그 `archive/pre-restructure` 로 되찾을 수 있다. `tools/` 는 없앴다: 스텁 생성기는 - `pythonx/_build/`, 가이드 검사기와 릴리스 스크립트는 `.github/scripts/`. + `scripts/`(메인테이너 승인), 가이드 검사기와 릴리스 스크립트는 `.github/scripts/`. ### 결정됨 (2026-10-03, #62) — 첫 PyPI 릴리스 diff --git a/docs/SPEC.md b/docs/SPEC.md index 6d37bee..1ddb6d2 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -21,8 +21,8 @@ Run from a worktree with `python -m pytest tests -q -rs` (pytest 8, mypy 2.4, CP | Environment | Result | |---|---| -| No `PythonMultiplatform` checkout found | **139 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)`) | **250 passed, 1 skipped**, 198 subtests passed | +| 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 | | an older checkout, without `describe_member` (python-multiplatform #54) | the same, with 7 more 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 | @@ -36,7 +36,7 @@ binder returned skip as well, because that needs its member resolver (python-mul `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 139 that pass without a checkout, 73 check the type stubs and the wheel (S1.2); most of the +Of the 140 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. @@ -75,9 +75,9 @@ and the name the interpreter resolves cannot drift. regenerating them from the same input produces no diff. - **From what.** python-multiplatform's CI artefact `kotlin-stubs` (workflow run 37103737430, commit `a5028618`): Kotlin-named stubs for Compose 1.11.1, one `androidx/compose/.../__init__.pyi` - per Kotlin package. `python3 pythonx/_build/gen_stubs.py ` converts them; + per Kotlin package. `python3 scripts/gen_stubs.py ` converts them; the first line of every generated stub records the artefact, run and commit it came from. -- **The rule is the runtime's** (`_reexport.py`), applied by `pythonx/_build/gen_stubs.py`: +- **The rule is the runtime's** (`_reexport.py`), applied by `scripts/gen_stubs.py`: - a module function or constant is renamed with `python_name` (upper-case names kept, others snake_case, an explicit overload keeps its suffix: `padding__Dp`); its parameters are snake_case, except the positional-only `receiver` and anonymous `__aN` slots; keyword-only @@ -473,7 +473,7 @@ rule that names no declaration: (`androidx.compose.ui.Alignment.Horizontal`), so `Alignment.Horizontal.Top` fails: `Top` is an `Alignment.Vertical`. - On a binder without `describe(module, name)` there are no groups; the flat spelling is unchanged. -- **Stubs.** `pythonx/_build/gen_stubs.py` emits each group as a class nested in the object's stub class, +- **Stubs.** `scripts/gen_stubs.py` emits each group as a class nested in the object's stub class, holding the same `ClassVar` constants, grouped by the declared type the upstream stub's docstring carries (`"""Kotlin: androidx.compose.ui.Alignment.End(): androidx.compose.ui.Alignment.Horizontal"""`), so a type checker accepts `Alignment.Horizontal.End` and rejects `Alignment.Horizontal.Top`. A diff --git a/docs/guide/getting-started.html b/docs/guide/getting-started.html index cb403e6..6d2a5a2 100644 --- a/docs/guide/getting-started.html +++ b/docs/guide/getting-started.html @@ -43,9 +43,9 @@

1. Get the source1. 소스

2. Run the tests2. 테스트 실행

python3 -m pytest tests -q
-

Expect 139 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:

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

+

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 체크아웃이 있거나 다음이 필요합니다:

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

With a current one (31c092f0 or later) 250 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 이후)이면 230 개가 통과하고 1 개(UI.ipynb 테스트)를 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 건너뜁니다. 예상된 상태이며 기록되어 있습니다.

+

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

diff --git a/docs/guide/status.html b/docs/guide/status.html index aec2835..8666663 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; a method's keywords need python-multiplatform's describe_member, and Kotlin's own spelling still works at run timesnake_case 이름·키워드·시그니처 — 모듈과 메서드; 메서드 키워드에는 python-multiplatform 의 describe_member 가 필요하고, 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: 139 passed, 112 skipped without a python-multiplatform checkout; 250 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, 최신 체크아웃이면 250 passed, 1 skipped. 최신 체크아웃에서 건너뛰는 1 개는 메인테이너의 UI.ipynb 를 읽는 테스트입니다.

+

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 를 읽는 테스트입니다.

FAQ

diff --git a/pythonx/compose/__init__.pyi b/pythonx/compose/__init__.pyi index b912439..94ee536 100644 --- a/pythonx/compose/__init__.pyi +++ b/pythonx/compose/__init__.pyi @@ -1 +1 @@ -# GENERATED by pythonx/_build/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. diff --git a/pythonx/compose/foundation/__init__.pyi b/pythonx/compose/foundation/__init__.pyi index 1b89d73..9ca1498 100644 --- a/pythonx/compose/foundation/__init__.pyi +++ b/pythonx/compose/foundation/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by pythonx/_build/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. import typing as _t class BorderStroke: diff --git a/pythonx/compose/foundation/layout/__init__.pyi b/pythonx/compose/foundation/layout/__init__.pyi index 1c77602..092e758 100644 --- a/pythonx/compose/foundation/layout/__init__.pyi +++ b/pythonx/compose/foundation/layout/__init__.pyi @@ -1,2 +1,2 @@ -# GENERATED by pythonx/_build/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. from pythonx.compose.layout import * diff --git a/pythonx/compose/foundation/text/input/__init__.pyi b/pythonx/compose/foundation/text/input/__init__.pyi index f40d055..65a70ed 100644 --- a/pythonx/compose/foundation/text/input/__init__.pyi +++ b/pythonx/compose/foundation/text/input/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by pythonx/_build/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. import typing as _t class _TextFieldState_clearText(_t.Protocol): diff --git a/pythonx/compose/layout/__init__.pyi b/pythonx/compose/layout/__init__.pyi index 9899f8d..b3484c5 100644 --- a/pythonx/compose/layout/__init__.pyi +++ b/pythonx/compose/layout/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by pythonx/_build/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.material3 import pythonx.compose.ui diff --git a/pythonx/compose/material/__init__.pyi b/pythonx/compose/material/__init__.pyi index b912439..94ee536 100644 --- a/pythonx/compose/material/__init__.pyi +++ b/pythonx/compose/material/__init__.pyi @@ -1 +1 @@ -# GENERATED by pythonx/_build/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. diff --git a/pythonx/compose/material/icons/__init__.pyi b/pythonx/compose/material/icons/__init__.pyi index 7d337f9..190f8b4 100644 --- a/pythonx/compose/material/icons/__init__.pyi +++ b/pythonx/compose/material/icons/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by pythonx/_build/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.ui.graphics.vector diff --git a/pythonx/compose/material3/__init__.pyi b/pythonx/compose/material3/__init__.pyi index fbfde90..d31f2b1 100644 --- a/pythonx/compose/material3/__init__.pyi +++ b/pythonx/compose/material3/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by pythonx/_build/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.foundation import pythonx.compose.foundation.text.input diff --git a/pythonx/compose/runtime/__init__.pyi b/pythonx/compose/runtime/__init__.pyi index beecbf9..70233c9 100644 --- a/pythonx/compose/runtime/__init__.pyi +++ b/pythonx/compose/runtime/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by pythonx/_build/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.ui diff --git a/pythonx/compose/ui/__init__.pyi b/pythonx/compose/ui/__init__.pyi index 827559a..c3d1688 100644 --- a/pythonx/compose/ui/__init__.pyi +++ b/pythonx/compose/ui/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by pythonx/_build/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.layout import pythonx.compose.runtime diff --git a/pythonx/compose/ui/graphics/__init__.pyi b/pythonx/compose/ui/graphics/__init__.pyi index 85e7040..6465aad 100644 --- a/pythonx/compose/ui/graphics/__init__.pyi +++ b/pythonx/compose/ui/graphics/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by pythonx/_build/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.ui diff --git a/pythonx/compose/ui/graphics/vector/__init__.pyi b/pythonx/compose/ui/graphics/vector/__init__.pyi index 1e708df..21fc88b 100644 --- a/pythonx/compose/ui/graphics/vector/__init__.pyi +++ b/pythonx/compose/ui/graphics/vector/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by pythonx/_build/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.runtime import pythonx.compose.ui.graphics diff --git a/pythonx/compose/ui/text/__init__.pyi b/pythonx/compose/ui/text/__init__.pyi index 6044f24..27b36db 100644 --- a/pythonx/compose/ui/text/__init__.pyi +++ b/pythonx/compose/ui/text/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by pythonx/_build/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.ui.graphics import pythonx.compose.ui.unit diff --git a/pythonx/compose/ui/unit/__init__.pyi b/pythonx/compose/ui/unit/__init__.pyi index bd81f8f..c10242d 100644 --- a/pythonx/compose/ui/unit/__init__.pyi +++ b/pythonx/compose/ui/unit/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by pythonx/_build/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. import typing as _t class DpRect: diff --git a/scripts/gen_stubs.py b/scripts/gen_stubs.py index 2ea0680..d1414e3 100644 --- a/scripts/gen_stubs.py +++ b/scripts/gen_stubs.py @@ -1,6 +1,6 @@ """Generate `pythonx.compose` stubs from the binder's Kotlin-named stubs, by the runtime's own rule. - python3 pythonx/_build/gen_stubs.py [--out pythonx/compose/..] + python3 scripts/gen_stubs.py [--out pythonx/compose/..] The input is what python-multiplatform's `PythonStubsTask` writes, either as its CI artefact `kotlin-stubs` (a zip, read in place) or unpacked: one `androidx/compose/.../__init__.pyi` per Kotlin @@ -62,7 +62,7 @@ from functools import cached_property from pathlib import Path -REPO = Path(__file__).resolve().parents[2] +REPO = Path(__file__).resolve().parents[1] if str(REPO) not in sys.path: sys.path.insert(0, str(REPO)) @@ -74,7 +74,7 @@ def header(source: str) -> str: - return f"# GENERATED by pythonx/_build/gen_stubs.py from {source}. Do not edit.\n" + return f"# GENERATED by scripts/gen_stubs.py from {source}. Do not edit.\n" # ---------------------------------------------------------------------------------------------- input diff --git a/tests/test_stubs.py b/tests/test_stubs.py index 654d928..c1dd33e 100644 --- a/tests/test_stubs.py +++ b/tests/test_stubs.py @@ -29,7 +29,7 @@ class per bound Kotlin type with its extension members as `ClassVar`s of callabl HERE = Path(__file__).resolve().parent REPO = HERE.parent sys.path.insert(0, str(HERE)) -sys.path.insert(0, str(REPO / "pythonx" / "_build")) +sys.path.insert(0, str(REPO / "scripts")) sys.path.insert(0, str(REPO)) import adapter as adapter_loader # noqa: E402 @@ -221,7 +221,7 @@ def test_every_protocol_parameter_is_snake_case_and_keyword_only_stays_so(self): def test_the_header_records_the_input_and_says_nothing_of_kotlin_keywords(self): header = self.stubs[UI].splitlines()[:2] - self.assertTrue(header[0].startswith("# GENERATED by pythonx/_build/gen_stubs.py"), header[0]) + self.assertTrue(header[0].startswith("# GENERATED by scripts/gen_stubs.py"), header[0]) self.assertNotIn("keyword", self.stubs[UI].splitlines()[1]) self.assertNotIn("Methods are reached by", self.stubs[UI]) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index db8cfa4..6299391 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -70,6 +70,11 @@ def test_every_mapped_module_is_a_file_in_the_wheel(self): ) self.assertEqual([], missing, "a mapped module with no file cannot be imported once installed") + def test_no_build_tooling_or_tests_ship(self): + for prefix in ("scripts/", "tests/", "docs/", ".github/"): + with self.subTest(prefix=prefix): + self.assertEqual([], sorted(n for n in self.names if n.startswith(prefix))) + def test_the_re_export_rule_ships(self): self.assertIn("pythonx/compose/_reexport.py", self.names) From 8574bb90b00375ad286e5f50e1247fea243a8b41 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:47:34 +0900 Subject: [PATCH 04/16] Docs: Say 0.1.0a1 is released pythonx-compose 0.1.0a1 is on PyPI: the pre-release v0.1.0a1 was published from develop 134a641, publish-pypi run 37120641414 built, checked and uploaded it through trusted publishing, and a fresh venv's `pip install pythonx-compose==0.1.0a1` imports pythonx.compose, its ui and material3 modules and the runtime names, with the manifest and py.typed inside. README, README_ko, the guide's getting-started note and status FAQ, PROJECT.md and SPEC S1.1 stop calling it upcoming; S1.1 is implemented. documentation corrected: README.md, docs/locale/README_ko.md, docs/guide/getting-started.html, docs/guide/status.html, PROJECT.md, docs/SPEC.md. Tests unchanged. check_guide: 0 findings. Refs #73 --- PROJECT.md | 6 +++--- README.md | 4 ++-- docs/SPEC.md | 4 +++- docs/guide/getting-started.html | 2 +- docs/guide/status.html | 2 +- docs/locale/README_ko.md | 4 ++-- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/PROJECT.md b/PROJECT.md index 55e0e55..ccec2bf 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -128,9 +128,9 @@ python3 .github/scripts/check_guide.py # 가이드: HTML 파싱, 링 - 배포 방법: 릴리스할 커밋에 태그 `v0.1.0a1` 로 GitHub Release 를 게시하면(알파는 pre-release 표시) `publish-pypi.yml` 이 Release 태그와 `pyproject.toml` 버전을 대조하고, sdist·wheel 을 빌드하고, `twine check`·새 venv 스모크를 거쳐 트러스티드 퍼블리싱으로 올린다. 태그만 푸시해서는 아무것도 - 올라가지 않는다(같은 버전은 PyPI 에 다시 올릴 수 없으므로 의도적인 단계로 둠). **현황: 업로드 전.** - Release 게시는 메인테이너 승인을 기다린다. GitHub 환경 `pypi`(`v*` 태그만 허용)는 만들었고, PyPI - pending publisher 는 메인테이너가 등록한다. + 올라가지 않는다(같은 버전은 PyPI 에 다시 올릴 수 없으므로 의도적인 단계로 둠). **현황: 0.1.0a1 배포됨**(2026-10-03, develop `134a641` 에서 pre-release + `v0.1.0a1`, publish-pypi run 37120641414; https://pypi.org/project/pythonx-compose/). 새 venv 에서 + `pip install pythonx-compose==0.1.0a1` 후 import 스모크 통과. ### 결정됨 (2026-10-03, `docs/INTENT.md` §5) diff --git a/README.md b/README.md index a178167..4114cb0 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,8 @@ Each module is a real file whose `__init__.py` calls one re-export rule; names r ## 🚀 Quick start > [!NOTE] -> `pythonx-compose` is **pre-alpha**. The first alpha, `0.1.0a1`, is released to PyPI when the -> maintainer tags it; a pre-release needs `pip install --pre pythonx-compose`. +> `pythonx-compose` is an **alpha**. `0.1.0a1` is on [PyPI](https://pypi.org/project/pythonx-compose/); +> as a pre-release it installs with `pip install --pre pythonx-compose`. From a clone, to run the tests: diff --git a/docs/SPEC.md b/docs/SPEC.md index 1ddb6d2..17dcdb4 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -44,7 +44,7 @@ in §9 and are not counted as features. ## 1. Distribution -### S1.1 Installed as `pythonx-compose`, importing `pythonx.compose` — `partial` +### S1.1 Installed as `pythonx-compose`, importing `pythonx.compose` — `implemented` `pyproject.toml` declares `name = "pythonx-compose"`, version `0.1.0a1`, setuptools build, `requires-python >= 3.11` (the runtime reads the manifest with `tomllib`), and @@ -57,6 +57,8 @@ in §9 and are not counted as features. pypi.org cannot resolve relative ones), `license = "MIT"` with `LICENSE`, the two authors as `LICENSE` names them (no emails), keywords, classifiers (Alpha, Python 3.11-3.13, Typed) and project URLs. `build-system.requires` is `setuptools>=77`, which understands that license form. +- **0.1.0a1 is on PyPI** (2026-10-03: pre-release `v0.1.0a1` on develop `134a641`, publish-pypi run + 37120641414; a fresh-venv `pip install pythonx-compose==0.1.0a1` imports cleanly). - Released by publishing a GitHub Release tagged `v` (`.github/workflows/publish-pypi.yml`, trigger `release: published`; a bare tag push publishes nothing): the workflow checks the Release's tag equals `v` + the `pyproject.toml` version (`.github/scripts/check_release_version.py`, diff --git a/docs/guide/getting-started.html b/docs/guide/getting-started.html index 6d2a5a2..4b354a3 100644 --- a/docs/guide/getting-started.html +++ b/docs/guide/getting-started.html @@ -30,7 +30,7 @@

Getting started시작하기

From clone to first call클론부터 첫 호출까지

Five minutes, honest about what runs.5 분, 동작하는 범위를 솔직하게.

-

pythonx-compose is pre-alpha. The first alpha, 0.1.0a1, is released to PyPI when the maintainer tags it; install a pre-release with pip install --pre pythonx-compose. This page shows how to get the source, run the tests, and what you can call today.

pythonx-compose 는 pre-alpha 입니다. 첫 알파 0.1.0a1 은 메인테이너가 태그를 달면 PyPI 에 배포되며, 프리릴리스는 pip install --pre pythonx-compose 로 설치합니다. 이 페이지는 소스를 받고, 테스트를 돌리고, 지금 호출할 수 있는 것을 보여줍니다.

+

pythonx-compose is an alpha: 0.1.0a1 is on PyPI. As a pre-release it installs with pip install --pre pythonx-compose. This page shows how to get the source, run the tests, and what you can call today.

pythonx-compose 는 알파 단계이며 0.1.0a1 이 PyPI 에 있습니다. 프리릴리스이므로 pip install --pre pythonx-compose 로 설치합니다. 이 페이지는 소스를 받고, 테스트를 돌리고, 지금 호출할 수 있는 것을 보여줍니다.

1. Get the source1. 소스 받기

diff --git a/docs/guide/status.html b/docs/guide/status.html index 8666663..9fcaceb 100644 --- a/docs/guide/status.html +++ b/docs/guide/status.html @@ -40,7 +40,7 @@

FAQ

-
Can I pip install it?pip 으로 설치할 수 있나요?

Yes, once the maintainer tags the first alpha: pip install --pre pythonx-compose (version 0.1.0a1). Until it is out, clone the repository.

메인테이너가 첫 알파를 태그하면 가능합니다: pip install --pre pythonx-compose (버전 0.1.0a1). 배포 전에는 저장소를 클론하세요.

Do I need to know Kotlin?Kotlin 을 알아야 하나요?

No. The widgets and parameters are Compose's, so Compose documentation applies — read onClick as on_click.

아니요. 위젯과 매개변수가 Compose 의 것이므로 Compose 문서가 그대로 적용됩니다 — onClick 을 on_click 으로 읽으면 됩니다.

Does it run on its own?단독으로 실행되나요?

No. It runs inside an app that embeds CPython through python-multiplatform.

아니요. python-multiplatform 으로 CPython 을 임베딩한 앱 안에서 동작합니다.

Why not look classes up at run time?왜 런타임에 클래스를 찾지 않나요?

GraalVM native images and Kotlin/Native have no reflection, so bindings are generated at build time.

GraalVM 네이티브 이미지와 Kotlin/Native 에는 리플렉션이 없으므로, 바인딩을 빌드 타임에 생성합니다.

What happened to the test/ directory?test/ 디렉터리는 어떻게 되었나요?

It no longer exists. It was a 2023–2024 Kotlin Multiplatform sample from the project's first generation, and it was deleted; the tag archive/pre-restructure keeps it.

더 이상 없습니다. 프로젝트 첫 세대의 2023–2024 Kotlin Multiplatform 샘플이었고 삭제되었으며, 태그 archive/pre-restructure 로 되찾을 수 있습니다.

+
Can I pip install it?pip 으로 설치할 수 있나요?

Yes: pip install --pre pythonx-compose installs 0.1.0a1, the first alpha. It needs a host app that embeds python-multiplatform to resolve Compose names.

네: pip install --pre pythonx-compose 로 첫 알파 0.1.0a1 을 설치합니다. Compose 이름을 쓰려면 python-multiplatform 을 품은 호스트 앱이 필요합니다.

Do I need to know Kotlin?Kotlin 을 알아야 하나요?

No. The widgets and parameters are Compose's, so Compose documentation applies — read onClick as on_click.

아니요. 위젯과 매개변수가 Compose 의 것이므로 Compose 문서가 그대로 적용됩니다 — onClick 을 on_click 으로 읽으면 됩니다.

Does it run on its own?단독으로 실행되나요?

No. It runs inside an app that embeds CPython through python-multiplatform.

아니요. python-multiplatform 으로 CPython 을 임베딩한 앱 안에서 동작합니다.

Why not look classes up at run time?왜 런타임에 클래스를 찾지 않나요?

GraalVM native images and Kotlin/Native have no reflection, so bindings are generated at build time.

GraalVM 네이티브 이미지와 Kotlin/Native 에는 리플렉션이 없으므로, 바인딩을 빌드 타임에 생성합니다.

What happened to the test/ directory?test/ 디렉터리는 어떻게 되었나요?

It no longer exists. It was a 2023–2024 Kotlin Multiplatform sample from the project's first generation, and it was deleted; the tag archive/pre-restructure keeps it.

더 이상 없습니다. 프로젝트 첫 세대의 2023–2024 Kotlin Multiplatform 샘플이었고 삭제되었으며, 태그 archive/pre-restructure 로 되찾을 수 있습니다.

diff --git a/docs/locale/README_ko.md b/docs/locale/README_ko.md index 5a6715a..f40c747 100644 --- a/docs/locale/README_ko.md +++ b/docs/locale/README_ko.md @@ -84,8 +84,8 @@ flowchart LR ## 🚀 빠른 시작 > [!NOTE] -> `pythonx-compose` 는 **pre-alpha** 단계입니다. 첫 알파 `0.1.0a1` 은 메인테이너가 태그를 달면 PyPI 에 -> 배포되며, 프리릴리스이므로 `pip install --pre pythonx-compose` 로 설치합니다. +> `pythonx-compose` 는 **알파** 단계입니다. `0.1.0a1` 이 [PyPI](https://pypi.org/project/pythonx-compose/) 에 +> 있으며, 프리릴리스이므로 `pip install --pre pythonx-compose` 로 설치합니다. 클론에서 테스트를 돌리려면: From 0230d00db0cfb408222662ad8c518a1898f53f3c Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:25:12 +0900 Subject: [PATCH 05/16] Chore: Relicense to Apache-2.0 The maintainer's decision (2026-10-03, for every thisisthepy project): pythonx-compose moves from MIT to the Apache License 2.0. LICENSE carries the Apache License 2.0 text (as python-multiplatform's does), pyproject's SPDX license is Apache-2.0, and the README badges and license lines (English and Korean), the guide footers, PROJECT.md and SPEC follow. There is no license classifier to change: with an SPDX license field, setuptools forbids one. The published 0.1.0a1 stays MIT; the new metadata applies from the next version. documentation corrected: README.md, docs/locale/README_ko.md, guide footers, PROJECT.md, SPEC S1.1. The wheel test builds and passes; check_guide: 0. Refs #75 --- LICENSE | 223 +++++++++++++++++++++++++++--- PROJECT.md | 2 +- README.md | 4 +- docs/SPEC.md | 2 +- docs/guide/concepts.html | 2 +- docs/guide/ecosystem.html | 2 +- docs/guide/getting-started.html | 2 +- docs/guide/guide-composables.html | 2 +- docs/guide/guide-manifest.html | 2 +- docs/guide/guide-modifiers.html | 2 +- docs/guide/index.html | 2 +- docs/guide/status.html | 2 +- docs/locale/README_ko.md | 4 +- pyproject.toml | 2 +- 14 files changed, 217 insertions(+), 36 deletions(-) diff --git a/LICENSE b/LICENSE index 6a14d04..57bc88a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,202 @@ -MIT License - -Copyright (c) 2023-2024 BREW (b-re-w), Jong-uk Lee (rnoro5122) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/PROJECT.md b/PROJECT.md index ccec2bf..bb2d1a1 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -123,7 +123,7 @@ python3 .github/scripts/check_guide.py # 가이드: HTML 파싱, 링 ### 결정됨 (2026-10-03, #62) — 첫 PyPI 릴리스 - 워크플로 이름: `tests.yml` → `test.yml`, PyPI 배포는 `publish-pypi.yml`. -- 첫 버전은 `0.1.0a1`(알파, 프리릴리스). PyPI 메타데이터(설명·README·MIT·저자·URL·분류자)는 채웠고, +- 첫 버전은 `0.1.0a1`(알파, 프리릴리스). PyPI 메타데이터(설명·README·라이선스·저자·URL·분류자)는 채웠고, README 의 링크는 pypi.org 에서 열리도록 절대 URL 이다(`main` 링크는 `main` 이 `develop` 을 따라잡은 뒤에 열린다). - 배포 방법: 릴리스할 커밋에 태그 `v0.1.0a1` 로 GitHub Release 를 게시하면(알파는 pre-release 표시) `publish-pypi.yml` 이 Release 태그와 `pyproject.toml` 버전을 대조하고, sdist·wheel 을 빌드하고, diff --git a/README.md b/README.md index 4114cb0..6591f20 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ English | [한국어](https://github.com/thisisthepy/pythonx-compose/blob/main/d **Write Compose user interfaces in Python — with Compose's own widgets, spelled the Python way.** -[![License: MIT](https://img.shields.io/badge/license-MIT-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/LICENSE) +[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/LICENSE) [![Python](https://img.shields.io/badge/python-3.11%2B-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/pyproject.toml) [![pip](https://img.shields.io/badge/pip-pythonx--compose-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/pyproject.toml) [![Status](https://img.shields.io/badge/status-pre--alpha-lightgrey.svg)](#-status) @@ -200,4 +200,4 @@ useful. Please open an issue before large changes. ## License -[MIT](https://github.com/thisisthepy/pythonx-compose/blob/main/LICENSE) © 2023–2024 BREW (b-re-w), Jong-uk Lee (rnoro5122) +[Apache License 2.0](https://github.com/thisisthepy/pythonx-compose/blob/main/LICENSE) © 2023–2024 BREW (b-re-w), Jong-uk Lee (rnoro5122) diff --git a/docs/SPEC.md b/docs/SPEC.md index 17dcdb4..7b4a34d 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -54,7 +54,7 @@ in §9 and are not counted as features. `pythonx/compose/pythonx-map.toml`, the re-export rule, and an `__init__.py` for every mapped module (issue #13). The manifest lives inside the package directory so `package-data` carries it. - PyPI metadata: description, `README.md` as the long description (its links are absolute, since - pypi.org cannot resolve relative ones), `license = "MIT"` with `LICENSE`, the two authors as + pypi.org cannot resolve relative ones), `license = "Apache-2.0"` with `LICENSE` (Apache License 2.0 since the maintainer's decision of 2026-10-03; the published 0.1.0a1 carries MIT), the two authors as `LICENSE` names them (no emails), keywords, classifiers (Alpha, Python 3.11-3.13, Typed) and project URLs. `build-system.requires` is `setuptools>=77`, which understands that license form. - **0.1.0a1 is on PyPI** (2026-10-03: pre-release `v0.1.0a1` on develop `134a641`, publish-pypi run diff --git a/docs/guide/concepts.html b/docs/guide/concepts.html index 3619414..e3715d3 100644 --- a/docs/guide/concepts.html +++ b/docs/guide/concepts.html @@ -106,7 +106,7 @@

@Composable@Composable
pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. -GitHub · MIT LicenseMIT 라이선스 +GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/guide/ecosystem.html b/docs/guide/ecosystem.html index 671590e..0f0dded 100644 --- a/docs/guide/ecosystem.html +++ b/docs/guide/ecosystem.html @@ -44,7 +44,7 @@

Who owns what누가 무엇
pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. -GitHub · MIT LicenseMIT 라이선스 +GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/guide/getting-started.html b/docs/guide/getting-started.html index 4b354a3..c2e6d4a 100644 --- a/docs/guide/getting-started.html +++ b/docs/guide/getting-started.html @@ -110,7 +110,7 @@

4. What you write in an app
pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. -GitHub · MIT LicenseMIT 라이선스 +GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/guide/guide-composables.html b/docs/guide/guide-composables.html index 9eea091..50fe3cf 100644 --- a/docs/guide/guide-composables.html +++ b/docs/guide/guide-composables.html @@ -124,7 +124,7 @@

What does not work yet아
pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. -GitHub · MIT LicenseMIT 라이선스 +GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/guide/guide-manifest.html b/docs/guide/guide-manifest.html index 5e556f5..7302722 100644 --- a/docs/guide/guide-manifest.html +++ b/docs/guide/guide-manifest.html @@ -71,7 +71,7 @@

Keeping it in step with the notebook
pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. -GitHub · MIT LicenseMIT 라이선스 +GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/guide/guide-modifiers.html b/docs/guide/guide-modifiers.html index 5a25434..271bc19 100644 --- a/docs/guide/guide-modifiers.html +++ b/docs/guide/guide-modifiers.html @@ -92,7 +92,7 @@

The empty Modifier빈 Modif
pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. -GitHub · MIT LicenseMIT 라이선스 +GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/guide/index.html b/docs/guide/index.html index bddf0b7..38a24b4 100644 --- a/docs/guide/index.html +++ b/docs/guide/index.html @@ -94,7 +94,7 @@

What it is not이것이 아
pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. -GitHub · MIT LicenseMIT 라이선스 +GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/guide/status.html b/docs/guide/status.html index 9fcaceb..07d5a54 100644 --- a/docs/guide/status.html +++ b/docs/guide/status.html @@ -49,7 +49,7 @@

FAQ

pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. -GitHub · MIT LicenseMIT 라이선스 +GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/locale/README_ko.md b/docs/locale/README_ko.md index f40c747..959c7fe 100644 --- a/docs/locale/README_ko.md +++ b/docs/locale/README_ko.md @@ -6,7 +6,7 @@ **Compose UI 를 파이썬으로 — Compose 의 위젯 그대로, 파이썬다운 이름으로.** -[![License: MIT](https://img.shields.io/badge/license-MIT-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/LICENSE) +[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/LICENSE) [![Python](https://img.shields.io/badge/python-3.11%2B-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/pyproject.toml) [![pip](https://img.shields.io/badge/pip-pythonx--compose-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/pyproject.toml) [![Status](https://img.shields.io/badge/status-pre--alpha-lightgrey.svg)](#-현황) @@ -197,4 +197,4 @@ pip 패키지 **`pythonx-compose`**(파이썬 3.11 이상)로 배포되며, impo ## 라이선스 -[MIT](https://github.com/thisisthepy/pythonx-compose/blob/main/LICENSE) © 2023–2024 BREW (b-re-w), Jong-uk Lee (rnoro5122) +[Apache License 2.0](https://github.com/thisisthepy/pythonx-compose/blob/main/LICENSE) © 2023–2024 BREW (b-re-w), Jong-uk Lee (rnoro5122) diff --git a/pyproject.toml b/pyproject.toml index b6797f2..db57ebd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "pythonx-compose" version = "0.1.0a1" description = "Pythonic Compose UI for Python on Kotlin Multiplatform, through python-multiplatform" readme = {file = "README.md", content-type = "text/markdown"} -license = "MIT" +license = "Apache-2.0" license-files = ["LICENSE"] authors = [ {name = "BREW (b-re-w)"}, From 5303cf27747590008277bace53537ed88736c1e6 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:31:07 +0900 Subject: [PATCH 06/16] Docs: No em-dash, uv/ppp/tcl examples only, and a house style note Three of the maintainer's instructions (2026-10-03), relayed by the ecosystem lead: - No em-dash anywhere in documents or code. All 122 lines that had one are rewritten by context (two sentences, a comma, a colon or parentheses), English and Korean alike; the en-dash in ranges stays. AGENTS.md section 17 makes it a rule. - Install and run examples use only uv, ppp (pypackpack) and tcl (toolchain-lite): `uv add --prerelease allow pythonx-compose`, then `ppp core add "pythonx-compose==0.1.0a1"` and `tcl install pythonx-compose`; tests and scripts run with `uv run`. The status FAQ asks "Can I install it from PyPI?" instead of naming pip. Records of what was run are rephrased so they are not instructions. AGENTS.md section 17 makes it a rule; the 0.1.0a1 Release notes stay as they are, a record. - The maintainer named this repository's guide the reference for every repository's documentation, so its style is written down in docs/style/writing.md (voice, the English/Korean pairing and registers, terms, mechanics), and AGENTS.md section 17 points at it. Also fixed: the Korean getting-started line said 230 tests pass where the English says 251. The em-dash and example passes were drafted by a subagent in a worktree and reviewed here; the style note is written here. documentation corrected: AGENTS.md, PROJECT.md, README.md, README_ko.md, INTENT, SPEC, every guide page and its CSS comment; added docs/style/writing.md. git grep finds no em-dash. check_guide: 0 findings. Tests unchanged. Refs #77 --- AGENTS.md | 50 +++++++++++++++----------- PROJECT.md | 26 +++++++------- README.md | 41 +++++++++++++--------- docs/INTENT.md | 28 +++++++-------- docs/SPEC.md | 58 +++++++++++++++---------------- docs/guide/assets/style.css | 4 +-- docs/guide/concepts.html | 12 +++---- docs/guide/ecosystem.html | 2 +- docs/guide/getting-started.html | 18 +++++----- docs/guide/guide-composables.html | 12 +++---- docs/guide/guide-manifest.html | 4 +-- docs/guide/guide-modifiers.html | 6 ++-- docs/guide/index.html | 12 +++---- docs/guide/status.html | 6 ++-- docs/locale/README_ko.md | 41 +++++++++++++--------- docs/style/writing.md | 51 +++++++++++++++++++++++++++ 16 files changed, 222 insertions(+), 149 deletions(-) create mode 100644 docs/style/writing.md diff --git a/AGENTS.md b/AGENTS.md index 2a9ffe9..dc54f9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,8 +14,8 @@ overrides any default your tooling has. ## 2. Nothing is created outside this repository -Everything your work produces — worktrees, agent prompts, logs, measurements, experiments, scratch -files — lives **inside this repository's root directory.** +Everything your work produces (worktrees, agent prompts, logs, measurements, experiments, scratch +files) lives **inside this repository's root directory.** | What | Where | |---|---| @@ -27,8 +27,8 @@ files — lives **inside this repository's root directory.** Before writing a file, check that its absolute path starts with this repository's root. If it does not, stop. The only exceptions are a path the user names explicitly, and caches that build tools -manage themselves. **Re-pointing a shared cache or a home-directory symlink reaches other projects — -ask first.** +manage themselves. **Re-pointing a shared cache or a home-directory symlink reaches other projects. +Ask first.** Writing to *another* repository is not an exception either. Do it only when told to work there. @@ -41,8 +41,8 @@ existing modules and directories: source inside the package (`pythonx/`), CI scr The standing root entries are `pyproject.toml`, `README.md`, `LICENSE`, `PROJECT.md`, `AGENTS.md`, `pythonx/`, `scripts/` (approved by the maintainer, 2026-10-03), `tests/`, `docs/`, `.github/`, `.gitignore` and `.gitattributes`, plus the maintainer's -untracked `UI.ipynb`. If a new top-level entry seems necessary, propose it — what it is, why, and -why it cannot live inside an existing directory — and wait for approval. +untracked `UI.ipynb`. If a new top-level entry seems necessary, propose it (what it is, why, and +why it cannot live inside an existing directory) and wait for approval. ## 3. Worktrees link large artefacts instead of copying them @@ -80,7 +80,7 @@ Every new feature goes through an issue and a pull request: 1. Before starting, search the repository's issues (`gh issue list --state all --search ""`). 2. If no issue covers the work, open one (`gh issue create`) stating what and why, and the - completion criterion — which tests must pass. + completion criterion, that is, which tests must pass. 3. Work on a `feat/` branch, push every commit, and open a pull request into `develop` whose body contains `Closes #`. 4. Merge into `develop` through that pull request (`gh pr merge --merge --delete-branch`), not by a @@ -104,18 +104,18 @@ If a request conflicts with `docs/INTENT.md`, say so instead of implementing it. ## 6. User-authored files are specification -Files the user wrote by hand — notebooks, example build files, sample apps — are the specification. +Files the user wrote by hand (notebooks, example build files, sample apps) are the specification. Read them **first**. Never delete, rewrite, or `git add` them without being told to. Generated documentation (roadmaps, design notes) is a record of work, not a requirement; when the two disagree, the user's file wins. ## 7. Show a conclusion before acting on it -Anything beyond the immediate request — another repository, a public API signature, deleting -files, killing processes, force-pushing, changing branch protection — state what you would do and +Anything beyond the immediate request (another repository, a public API signature, deleting +files, killing processes, force-pushing, changing branch protection): state what you would do and why, and wait. Investigating, measuring, and reporting are always fine. -**Push every commit right away.** After you commit — on a work branch or on `develop` — push it to +**Push every commit right away.** After you commit, on a work branch or on `develop`, push it to the remote immediately; no confirmation is needed. Never push to `main` or `release` by hand, and never force-push without the user's explicit approval. @@ -155,17 +155,17 @@ to implement", say what you counted against. - Give every agent prompt the absolute paths it may write to, and repeat rule 2 in it. - **Subagents do not run heavy local builds.** Subagents write code, design, investigate, review and document. Gradle builds, cargo builds, the test gate and model runs are done by the session - itself — one at a time on this machine — or by CI (GitHub Actions) on a pushed branch. Several + itself (one at a time on this machine) or by CI (GitHub Actions) on a pushed branch. Several sessions share one machine; parallel local builds slow every one of them. --- -# Repository-specific rules — `pythonx-compose` +# Repository-specific rules: `pythonx-compose` ## 11. The specification is the user's notebook `UI.ipynb` is this project's user-facing specification. It lives only in the maintainer's main -checkout, is git-ignored, and is **never** copied, moved, edited, or `git add`ed — not even into a +checkout, is git-ignored, and is **never** copied, moved, edited, or `git add`ed, not even into a worktree so a test can see it. Read it in place. - Its function signatures are **naming examples**. The rule they illustrate is: expose the original @@ -181,7 +181,7 @@ worktree so a test can see it. Read it in place. - `python-multiplatform` exposes Kotlin declarations under their **Kotlin** names (`androidx.compose.material3`). It must never map `androidx` to `pythonx` or rename any Kotlin - namespace. Do not propose, add, or depend on such a feature there — including in test harnesses. + namespace. Do not propose, add, or depend on such a feature there, including in test harnesses. - `pythonx/` is a **real package on disk**. Its modules import the `androidx.compose.*` modules the binder exposes and restructure them Pythonically. A design in which something else synthesises `pythonx.*` (for example with `__path__ = []`) and the on-disk files cannot be imported is ruled @@ -200,7 +200,7 @@ worktree so a test can see it. Read it in place. ## 14. Running the tests - python3 -m pytest tests -q + uv run --with pytest --with mypy pytest tests -q - The suite needs `pytest`; if the system Python has none, create a virtual environment under `.tmp/` (rule 2), not in the home directory. @@ -229,11 +229,11 @@ worktree so a test can see it. Read it in place. publishes through PyPI trusted publishing (environment `pypi`). Pushing a tag alone publishes nothing. Never upload by hand, and never store a PyPI token. - `.pyi` stubs, `py.typed` and the manifest must end up **inside the wheel**. A `package-data` - pattern is not proof — build the wheel and list it before claiming something ships. + pattern is not proof: build the wheel and list it before claiming something ships. ## 16. Things you do not change without the maintainer -- **The root layout** — see section 2, "Do not add top-level folders". +- **The root layout**: see section 2, "Do not add top-level folders". - The retired `test/` sample, `pythonx/compose/lite/` and the `pythonx/compose/native` submodule were removed in #60; they stay reachable through the tag `archive/pre-restructure`. Do not bring them back. @@ -241,10 +241,20 @@ worktree so a test can see it. Read it in place. ## 17. Documentation layout - `README.md` (English) and `docs/locale/README_ko.md` (Korean) say the same thing; change both. +- Write in the house style, `docs/style/writing.md`: voice, the English and Korean pairing, terms + and mechanics. It is the reference for every thisisthepy repository's documentation. - `docs/guide/` is the bilingual GitHub Pages site. Every visible string exists in English and - Korean. `python3 .github/scripts/check_guide.py` is its checker, and `tests/test_guide.py` runs it + Korean. `uv run python .github/scripts/check_guide.py` is its checker, and `tests/test_guide.py` runs it in the suite. - No Markdown files directly under `docs/` other than `INTENT.md` and `SPEC.md`; other documents go in a topic subdirectory. `README.md` must not link to `AGENTS.md`, `PROJECT.md`, - `docs/INTENT.md` or `docs/SPEC.md` — those files do not exist on `main`. + `docs/INTENT.md` or `docs/SPEC.md`, because those files do not exist on `main`. - There is no `CLAUDE.md`: Claude Code reads this file directly. Rules go here. +- No em-dash (U+2014) anywhere in documents or code: Markdown, the guide's HTML and CSS, workflows, + `pyproject.toml`, Python comments, docstrings and strings, tests. Split the sentence or use a + comma, colon or parentheses, whichever reads best. The en-dash in ranges (2023–2024, 3.11–3.13) + is fine. +- Install and run examples use only uv, ppp (pypackpack) and tcl (toolchain-lite): `uv add + --prerelease allow pythonx-compose`, `ppp core add "pythonx-compose==0.1.0a1"`, `tcl install + pythonx-compose`, `uv run ...`. No `pip` examples. The published 0.1.0a1 Release notes are a + record and stay as they are. diff --git a/PROJECT.md b/PROJECT.md index bb2d1a1..d064889 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -1,4 +1,4 @@ -# PROJECT — pythonx-compose +# PROJECT: pythonx-compose 프로젝트 운영에 필요한 핵심 사항을 정리한 문서입니다. 의도는 `docs/INTENT.md`, 동작 계약은 `docs/SPEC.md`, 에이전트 규정은 `AGENTS.md` 에 있습니다. @@ -28,13 +28,13 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **` | 상태 | 항목 | |---|---| | 구현 | 매핑 매니페스트 `pythonx-map.toml`, `@Composable` 항등 데코레이터, 실제 디스크 패키지 `pythonx` 와 모듈 수준 규칙 기반 재노출(snake_case 이름·키워드·시그니처, 오버로드 디스패치, 값 클래스 허용 목록), 실제 Compose 1.11.1 에서 생성한 `.pyi` 스텁과 `py.typed`(wheel 포함, mypy 로 검사), `Alignment`/`Arrangement` 상수(평면 `Alignment.End` 와 묶음 `Alignment.Horizontal.End` 모두, #9), proxy 메서드의 snake_case 키워드 인자(`m.padding(padding_values=...)`, python-multiplatform `describe_member` 필요; Kotlin 이름도 실행 시 그대로 통하고 스텁은 snake_case 만) | -| 부분 | 선언형 앱 루트 `@app`·`state`·`app_root`(#11; 로직은 테스트됨, 바인더 경로는 #38 모양의 가짜 호스트로 테스트됨, 실제 Compose 증거는 E2E 모듈 #19; 숫자·문자열 상태는 python-multiplatform #69 부터 왕복됨), 배포 설정, Material 3 위젯(렌더 증거는 python-multiplatform 에만), `Icon`(import 만 됨)·`DefaultIcons`(`Icons.Default` alias, 가짜 호스트로만 테스트됨, 스텁에서는 `DefaultIcons.Add` 가 `ImageVector`; `DefaultIcons.Add` 로 쓴다 — 노트북의 `DefaultIcons.Add()` 가 아님), `TextField(state=...)`·`TextFieldState`(#10; `pythonx.compose.foundation.text.input`, 가짜 호스트로 테스트됨, 노트북의 `text_state=`/`padding=` 는 `state=`/`modifier=Modifier.padding(8)`; IME 조합 증거는 python-multiplatform E2E #26) | +| 부분 | 선언형 앱 루트 `@app`·`state`·`app_root`(#11; 로직은 테스트됨, 바인더 경로는 #38 모양의 가짜 호스트로 테스트됨, 실제 Compose 증거는 E2E 모듈 #19; 숫자·문자열 상태는 python-multiplatform #69 부터 왕복됨), 배포 설정, Material 3 위젯(렌더 증거는 python-multiplatform 에만), `Icon`(import 만 됨)·`DefaultIcons`(`Icons.Default` alias, 가짜 호스트로만 테스트됨, 스텁에서는 `DefaultIcons.Add` 가 `ImageVector`; `DefaultIcons.Add` 로 쓴다; 노트북의 `DefaultIcons.Add()` 가 아님), `TextField(state=...)`·`TextFieldState`(#10; `pythonx.compose.foundation.text.input`, 가짜 호스트로 테스트됨, 노트북의 `text_state=`/`padding=` 는 `state=`/`modifier=Modifier.padding(8)`; IME 조합 증거는 python-multiplatform E2E #26) | | 계획 | `remember_saveable`·색 스킴·코루틴 스코프 | ### 테스트 기준선 ``` -python3 -m pytest tests -q +uv run --with pytest --with mypy pytest tests -q PythonMultiplatform 체크아웃 없음 140 passed, 112 skipped python-multiplatform develop 31c092f0+ 251 passed, 1 skipped describe_member 없는 체크아웃 같은 수, 그중 7 개 건너뜀 (python-multiplatform #54 이전) @@ -84,9 +84,9 @@ docs/INTENT.md docs/SPEC.md docs/locale/ docs/guide/ ## 5. 빌드·테스트 ```bash -python3 -m pytest tests -q # 파이썬 테스트 -PYTHONMULTIPLATFORM_HOME= python3 -m pytest tests -q # 바인더 계층 포함 -python3 .github/scripts/check_guide.py # 가이드: HTML 파싱, 링크, en/ko 짝 (tests/test_guide.py 도 실행) +uv run --with pytest --with mypy pytest tests -q # 파이썬 테스트 +PYTHONMULTIPLATFORM_HOME= uv run --with pytest --with mypy pytest tests -q # 바인더 계층 포함 +uv run python .github/scripts/check_guide.py # 가이드: HTML 파싱, 링크, en/ko 짝 (tests/test_guide.py 도 실행) ``` - pytest 가 없으면 `.tmp/` 아래에 가상환경을 만든다 (홈 디렉터리 금지). @@ -106,10 +106,10 @@ python3 .github/scripts/check_guide.py # 가이드: HTML 파싱, 링 ## 7. 열린 질문 (메인테이너 결정 필요) -1. **코루틴 스코프** — 노트북이 import 하는 `DefaultCoroutineScope`·`MainCoroutineScope`. -2. **`pythonx-map.toml` 의 이중 표기** — `pythonx.compose.layout` 과 +1. **코루틴 스코프**: 노트북이 import 하는 `DefaultCoroutineScope`·`MainCoroutineScope`. +2. **`pythonx-map.toml` 의 이중 표기**: `pythonx.compose.layout` 과 `pythonx.compose.foundation.layout` 를 둘 다 유지할지. -3. **릴리스·Pages 활성화** — `.github/scripts/release/` 와 `.github/workflows/`(release-sync, pages)는 +3. **릴리스·Pages 활성화**: `.github/scripts/release/` 와 `.github/workflows/`(release-sync, pages)는 들어와 있다. main 보호는 메인테이너가 저장소 설정에서 직접 관리하며 main 은 잠겨 있다. 스크립트나 에이전트는 보호 설정을 만들거나 바꾸지 않는다. release→main PR 머지는 메인테이너가 한다. @@ -120,7 +120,7 @@ python3 .github/scripts/check_guide.py # 가이드: HTML 파싱, 링 태그 `archive/pre-restructure` 로 되찾을 수 있다. `tools/` 는 없앴다: 스텁 생성기는 `scripts/`(메인테이너 승인), 가이드 검사기와 릴리스 스크립트는 `.github/scripts/`. -### 결정됨 (2026-10-03, #62) — 첫 PyPI 릴리스 +### 결정됨 (2026-10-03, #62): 첫 PyPI 릴리스 - 워크플로 이름: `tests.yml` → `test.yml`, PyPI 배포는 `publish-pypi.yml`. - 첫 버전은 `0.1.0a1`(알파, 프리릴리스). PyPI 메타데이터(설명·README·라이선스·저자·URL·분류자)는 채웠고, @@ -129,8 +129,8 @@ python3 .github/scripts/check_guide.py # 가이드: HTML 파싱, 링 `publish-pypi.yml` 이 Release 태그와 `pyproject.toml` 버전을 대조하고, sdist·wheel 을 빌드하고, `twine check`·새 venv 스모크를 거쳐 트러스티드 퍼블리싱으로 올린다. 태그만 푸시해서는 아무것도 올라가지 않는다(같은 버전은 PyPI 에 다시 올릴 수 없으므로 의도적인 단계로 둠). **현황: 0.1.0a1 배포됨**(2026-10-03, develop `134a641` 에서 pre-release - `v0.1.0a1`, publish-pypi run 37120641414; https://pypi.org/project/pythonx-compose/). 새 venv 에서 - `pip install pythonx-compose==0.1.0a1` 후 import 스모크 통과. + `v0.1.0a1`, publish-pypi run 37120641414; https://pypi.org/project/pythonx-compose/). PyPI 에서 새로 + 설치한 0.1.0a1 이 import 스모크를 통과했다. ### 결정됨 (2026-10-03, `docs/INTENT.md` §5) @@ -142,7 +142,7 @@ python3 .github/scripts/check_guide.py # 가이드: HTML 파싱, 링 - **정렬 상수** 는 `Alignment.End` 와 `Alignment.Horizontal.End` 둘 다 지원한다. - 소문자 `modifier` 는 두지 않는다(`Modifier` 로 쓴다). ARGB 정수 색은 받지 않는다 (`Color(0xFFFF0000)`). `Spacer(start=...)` 는 지원하지 않는다(`Modifier.padding`). `DefaultIcons` 는 - `Icons.Default` 의 alias(매니페스트 `[aliases]` 의 `{ DefaultIcons = "Icons.Default" }`, 읽을 때마다 해석). — `docs/INTENT.md` §5.4–5.7 + `Icons.Default` 의 alias(매니페스트 `[aliases]` 의 `{ DefaultIcons = "Icons.Default" }`, 읽을 때마다 해석). 근거는 `docs/INTENT.md` §5.4–5.7. - 합성된 `pythonx` 를 전제한 테스트·독스트링은 #7 에서 정리했다. ## 8. 관련 저장소 diff --git a/README.md b/README.md index 6591f20..93fb193 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ English | [한국어](https://github.com/thisisthepy/pythonx-compose/blob/main/d # pythonx-compose -**Write Compose user interfaces in Python — with Compose's own widgets, spelled the Python way.** +**Write Compose user interfaces in Python, with Compose's own widgets, spelled the Python way.** [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/LICENSE) [![Python](https://img.shields.io/badge/python-3.11%2B-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/pyproject.toml) @@ -19,7 +19,7 @@ English | [한국어](https://github.com/thisisthepy/pythonx-compose/blob/main/d ## Why -Jetpack Compose and Compose Multiplatform are a superb way to build UI — if you write Kotlin. +Jetpack Compose and Compose Multiplatform are a superb way to build UI, if you write Kotlin. `pythonx-compose` is for people who write Python. It does not reimplement Compose and it does not invent a new widget set: it takes the real `androidx.compose.*` API, which [python-multiplatform](https://github.com/thisisthepy/python-multiplatform) exposes to Python under @@ -55,7 +55,7 @@ proofs are still pending. See [Status](#-status). `Alignment.Horizontal.End` is `Alignment.End`. - **`@Composable` stays.** Screens are decorated Python functions. - **A real package.** `pythonx/` is ordinary Python source that imports `androidx.compose.*` and - reshapes it. The binder never renames anything — renaming is this package's job. + reshapes it. The binder never renames anything. Renaming is this package's job. - **One manifest.** [`pythonx-map.toml`](https://github.com/thisisthepy/pythonx-compose/blob/main/pythonx/compose/pythonx-map.toml) says which `pythonx.compose.*` module stands for which Kotlin package. It lives inside the package and ships in the wheel. The runtime and the `.pyi` generator read the same file, so what your editor completes is what the interpreter @@ -87,15 +87,22 @@ Each module is a real file whose `__init__.py` calls one re-export rule; names r > [!NOTE] > `pythonx-compose` is an **alpha**. `0.1.0a1` is on [PyPI](https://pypi.org/project/pythonx-compose/); -> as a pre-release it installs with `pip install --pre pythonx-compose`. +> as a pre-release it installs with uv (or ppp, or tcl): + +```bash +uv add --prerelease allow pythonx-compose +# or, with pypackpack, into a package of a ppp workspace: +ppp core add "pythonx-compose==0.1.0a1" +# or, with toolchain-lite: +tcl install pythonx-compose +``` From a clone, to run the tests: ```bash git clone https://github.com/thisisthepy/pythonx-compose cd pythonx-compose -python3 -m pip install pytest # inside a virtual environment -python3 -m pytest tests -q +uv run --with pytest --with mypy pytest tests -q ``` What runs today, from the repository root: @@ -155,15 +162,15 @@ standalone desktop toolkit. | `pythonx` as a real on-disk package re-exporting `androidx.compose.*` by one rule | ✅ implemented and tested | | `Modifier` chains with snake_case methods, overload dispatch, `Dp` as a number | ✅ implemented and tested against the binder's layer | | `Column`, `Row`, `Spacer` from `material3` as well as `layout` | ✅ implemented and tested | -| Method keyword arguments in `snake_case` | ✅ implemented and tested — module functions and methods (methods need python-multiplatform's `describe_member`) | -| The empty `Modifier` | 🟡 partial — `Modifier.padding(16)` from the class needs an app-supplied factory against real Compose | -| `Alignment` / `Arrangement` | ✅ implemented and tested — `Alignment.Center`, `Arrangement.spaced_by(8)`, and grouped `Alignment.Horizontal.End` beside `Alignment.End` | -| Material 3 widgets (`Text`, `Button`, `Card`, `TextField`, …) | 🟡 partial — re-exported by rule; render proofs per widget pending (#9) | +| Method keyword arguments in `snake_case` | ✅ implemented and tested: module functions and methods (methods need python-multiplatform's `describe_member`) | +| The empty `Modifier` | 🟡 partial: `Modifier.padding(16)` from the class needs an app-supplied factory against real Compose | +| `Alignment` / `Arrangement` | ✅ implemented and tested: `Alignment.Center`, `Arrangement.spaced_by(8)`, and grouped `Alignment.Horizontal.End` beside `Alignment.End` | +| Material 3 widgets (`Text`, `Button`, `Card`, `TextField`, …) | 🟡 partial: re-exported by rule; render proofs per widget pending (#9) | | Type stubs (`.pyi`) from real Compose 1.11.1, `py.typed` | ✅ implemented and checked with mypy; many types are still `Any` (#12) | -| Distribution (`pythonx-compose`) | 🟡 partial — the wheel carries the manifest, the re-export rule, the stubs and `py.typed` | -| Declared app root (`@app`) and Pythonic state (`state`), no update call | 🟡 partial — binder path tested against a fake host shaped after python-multiplatform #38; real Compose is the E2E module (#11, #19); numbers and strings round-trip through `state` | -| `TextField(state=...)` with `TextFieldState` (`pythonx.compose.foundation.text.input`) | 🟡 partial — tested against a fake host shaped after python-multiplatform #73; the input-method (IME) composing proof is python-multiplatform E2E #26 (#10) | -| `DefaultIcons` (`Icons.Default`), written `DefaultIcons.Add` | 🟡 partial — tested against a fake host shaped after python-multiplatform #37/#38; `Icon(DefaultIcons.Add, …)` is drawn in python-multiplatform's render test | +| Distribution (`pythonx-compose`) | 🟡 partial: the wheel carries the manifest, the re-export rule, the stubs and `py.typed` | +| Declared app root (`@app`) and Pythonic state (`state`), no update call | 🟡 partial: binder path tested against a fake host shaped after python-multiplatform #38; real Compose is the E2E module (#11, #19); numbers and strings round-trip through `state` | +| `TextField(state=...)` with `TextFieldState` (`pythonx.compose.foundation.text.input`) | 🟡 partial: tested against a fake host shaped after python-multiplatform #73; the input-method (IME) composing proof is python-multiplatform E2E #26 (#10) | +| `DefaultIcons` (`Icons.Default`), written `DefaultIcons.Add` | 🟡 partial: tested against a fake host shaped after python-multiplatform #37/#38; `Icon(DefaultIcons.Add, …)` is drawn in python-multiplatform's render test | | Colour schemes | ⏳ planned | | `remember_saveable`, coroutine scopes | ⏳ planned | @@ -171,15 +178,15 @@ The full list is on the guide's [Status page](https://thisisthepy.github.io/pyth ## 📖 Documentation -- **Guide** — [`docs/guide/`](https://thisisthepy.github.io/pythonx-compose/), bilingual (English / 한국어) -- **Korean README** — [`docs/locale/README_ko.md`](https://github.com/thisisthepy/pythonx-compose/blob/main/docs/locale/README_ko.md) +- **Guide**: [`docs/guide/`](https://thisisthepy.github.io/pythonx-compose/), bilingual (English / 한국어) +- **Korean README**: [`docs/locale/README_ko.md`](https://github.com/thisisthepy/pythonx-compose/blob/main/docs/locale/README_ko.md) ## 🔌 Ecosystem | Repository | Role | |---|---| | [python-multiplatform](https://github.com/thisisthepy/python-multiplatform) | The binder: CPython embedded in Kotlin Multiplatform, exposing Kotlin to Python under Kotlin names | -| **pythonx-compose** | Compose, restructured for Python — this repository | +| **pythonx-compose** | Compose, restructured for Python (this repository) | | [toolchain](https://github.com/thisisthepy/toolchain) | Gradle build plugin for Python Multiplatform apps | | [pypackpack](https://github.com/thisisthepy/pypackpack) | Distributing Python projects across platforms | | [torchnative](https://github.com/thisisthepy/torchnative) | Run the real PyTorch ecosystem on device | diff --git a/docs/INTENT.md b/docs/INTENT.md index 4ea3e61..d99274c 100644 --- a/docs/INTENT.md +++ b/docs/INTENT.md @@ -9,31 +9,31 @@ and the user's own words disagree, the user's words win. Everything below is derived from these, in this order of authority: 1. **The user's statements**, quoted verbatim where they are used. -2. **`UI.ipynb`** — the user's notebook and this project's user-facing specification. It is +2. **`UI.ipynb`**: the user's notebook and this project's user-facing specification. It is untracked and git-ignored; it lives only in the maintainer's main checkout and is never copied, moved, or added. 3. The repository's original README (2023–2024): *"pythonx is a python extension for kotlin - integration"* — *"Python Wrapper for Kotlin Compose Multiplatform."* + integration"*, *"Python Wrapper for Kotlin Compose Multiplatform."* 4. Recorded decisions in `python-multiplatform` (`docs/ecosystem.md` §5b, `docs/kotlin-extensions-in-python.md` §4). These are records of work, not requirements; they are used only where they agree with 1–3. Anything that is an inference rather than a statement is marked: -> Inferred — confirm with the maintainer. +> Inferred: confirm with the maintainer. --- ## 1. What this project is for **Writing Compose user interfaces in Python.** A Python programmer imports `pythonx.compose.*`, -writes `@Composable` functions, and gets real Jetpack / Compose Multiplatform UI — the same widgets, +writes `@Composable` functions, and gets real Jetpack / Compose Multiplatform UI: the same widgets, the same parameters, spelled the way Python spells things. The notebook shows the experience this is for: a running app whose screen is replaced from a notebook cell (`main.App.update(Practice)`), whose state is read and written from Python -(`main.App.messages.getValue()` / `setValue(...)`), and whose widgets — `Text`, `Button`, `Card`, -`Icon`, `TextField`, `Column`, `Row`, `Spacer` — are called from ordinary Python functions. The +(`main.App.messages.getValue()` / `setValue(...)`), and whose widgets (`Text`, `Button`, `Card`, +`Icon`, `TextField`, `Column`, `Row`, `Spacer`) are called from ordinary Python functions. The notebook is titled *"Python 앱 개발 특강"* (a lecture on Python app development): the audience is people who know Python and want to build an app without first learning Kotlin. @@ -45,7 +45,7 @@ These are constraints the user stated, not design options. > "pythonx-compose는 pip 패키지 pythonx-compose로 배포될 물건이야" -It ships as the distribution **`pythonx-compose`**, installed with `pip`, providing the import +It ships as the distribution **`pythonx-compose`**, installed from PyPI, providing the import package `pythonx.compose`. ### 2.2 `pythonx` is a real Python package on disk @@ -60,7 +60,7 @@ something else synthesises at run time. ### 2.3 The binder never renames; renaming is this package's job `python-multiplatform` (the binder) exposes Kotlin declarations to Python **under their Kotlin -names** — `androidx.compose.material3` in Kotlin is `androidx.compose.material3` in Python. The +names**: `androidx.compose.material3` in Kotlin is `androidx.compose.material3` in Python. The binder must never turn `androidx` into `pythonx` or rename any other Kotlin namespace. Mapping `androidx.compose.*` to `pythonx.compose.*` is done **here, in real Python code**. @@ -92,8 +92,8 @@ Screens are written as `@Composable`-decorated Python functions, as the notebook ### 2.7 Type stubs ship in the package -`.pyi` stubs ship inside the wheel so an editor sees the full Pythonic surface — names, parameters, -types — even though the runtime resolves bindings on demand. +`.pyi` stubs ship inside the wheel so an editor sees the full Pythonic surface (names, parameters, +types) even though the runtime resolves bindings on demand. --- @@ -107,10 +107,10 @@ types — even though the runtime resolves bindings on demand. are named and reached from Python, not what they do. - **Not an invented API.** Parameters that do not exist in Kotlin (for example the notebook's `corner_radius` on `Button`) are not added just because an example used them. - > Inferred — confirm with the maintainer. (Follows from §2.4; the user has not ruled on + > Inferred: confirm with the maintainer. (Follows from §2.4; the user has not ruled on > convenience parameters explicitly.) -- **Not runtime reflection.** The chaquopy-era approach — `jclass(...)` lookups and scanning - mangled JVM names such as `Text-fLXpl1I` — is a removed option. Bindings are produced at build +- **Not runtime reflection.** The chaquopy-era approach (`jclass(...)` lookups and scanning + mangled JVM names such as `Text-fLXpl1I`) is a removed option. Bindings are produced at build time because GraalVM native images and Kotlin/Native have no reflection (`python-multiplatform` `docs/ecosystem.md` §5b). - **Not a hand-written Kotlin wrapper layer.** The Pythonic restructuring happens in Python, over @@ -125,7 +125,7 @@ These are recorded so that `docs/SPEC.md` does not settle them by accident. 1. **Coroutine scopes** the notebook imports, `DefaultCoroutineScope` / `MainCoroutineScope`. (The other convenience spellings are decided, §5.4–5.7.) -2. *(Decided, #60.)* **The `test/` directory** — a 2023–2024 Kotlin Multiplatform sample +2. *(Decided, #60.)* **The `test/` directory**: a 2023–2024 Kotlin Multiplatform sample (`pycomposeui`). Deleted; the tag `archive/pre-restructure` keeps it. ## 5. Decided (2026-10-03) diff --git a/docs/SPEC.md b/docs/SPEC.md index 7b4a34d..a7b81c1 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -1,8 +1,8 @@ # Specification -What `pythonx-compose` does — the behavioural contract. Every item stays inside `docs/INTENT.md`; +What `pythonx-compose` does: the behavioural contract. Every item stays inside `docs/INTENT.md`; anything found in the repository that INTENT does not cover is listed at the end under -**Outside intent — needs a decision** rather than written into the contract. +**Outside intent: needs a decision** rather than written into the contract. Each item carries a status: @@ -16,7 +16,7 @@ A behaviour change starts here, then becomes a failing test, then code (`AGENTS. ## 0. Test baseline (2026-10-03, after the a5028618 stubs) -Run from a worktree with `python -m pytest tests -q -rs` (pytest 8, mypy 2.4, CPython 3.13), where +Run from a worktree with `uv run --with pytest --with mypy pytest tests -q -rs` (pytest 8, mypy 2.4, CPython 3.13), where `UI.ipynb` is absent and its one test skips: | Environment | Result | @@ -44,7 +44,7 @@ in §9 and are not counted as features. ## 1. Distribution -### S1.1 Installed as `pythonx-compose`, importing `pythonx.compose` — `implemented` +### S1.1 Installed as `pythonx-compose`, importing `pythonx.compose`: `implemented` `pyproject.toml` declares `name = "pythonx-compose"`, version `0.1.0a1`, setuptools build, `requires-python >= 3.11` (the runtime reads the manifest with `tomllib`), and @@ -58,7 +58,7 @@ in §9 and are not counted as features. `LICENSE` names them (no emails), keywords, classifiers (Alpha, Python 3.11-3.13, Typed) and project URLs. `build-system.requires` is `setuptools>=77`, which understands that license form. - **0.1.0a1 is on PyPI** (2026-10-03: pre-release `v0.1.0a1` on develop `134a641`, publish-pypi run - 37120641414; a fresh-venv `pip install pythonx-compose==0.1.0a1` imports cleanly). + 37120641414; a fresh install of 0.1.0a1 from PyPI imports cleanly). - Released by publishing a GitHub Release tagged `v` (`.github/workflows/publish-pypi.yml`, trigger `release: published`; a bare tag push publishes nothing): the workflow checks the Release's tag equals `v` + the `pyproject.toml` version (`.github/scripts/check_release_version.py`, @@ -67,7 +67,7 @@ in §9 and are not counted as features. token). Nothing is uploaded by hand. - Typed: the wheel carries `py.typed` and the generated `.pyi` stubs (S1.2, issue #12). -### S1.2 Type stubs ship in the wheel — `implemented` +### S1.2 Type stubs ship in the wheel: `implemented` Stubs carry the Pythonic names and signatures the runtime resolves, so the name an editor completes and the name the interpreter resolves cannot drift. @@ -77,7 +77,7 @@ and the name the interpreter resolves cannot drift. regenerating them from the same input produces no diff. - **From what.** python-multiplatform's CI artefact `kotlin-stubs` (workflow run 37103737430, commit `a5028618`): Kotlin-named stubs for Compose 1.11.1, one `androidx/compose/.../__init__.pyi` - per Kotlin package. `python3 scripts/gen_stubs.py ` converts them; + per Kotlin package. `uv run python scripts/gen_stubs.py ` converts them; the first line of every generated stub records the artefact, run and commit it came from. - **The rule is the runtime's** (`_reexport.py`), applied by `scripts/gen_stubs.py`: - a module function or constant is renamed with `python_name` (upper-case names kept, others @@ -159,7 +159,7 @@ and the name the interpreter resolves cannot drift. attribute, and are left out. - Regenerating needs the artefact downloaded by hand into `.tmp/`; CI does not fetch it. -## 2. The mapping manifest (`pythonx-map.toml`) — `implemented` +## 2. The mapping manifest (`pythonx-map.toml`): `implemented` The manifest is this package's statement of which Kotlin package each `pythonx.compose.*` module stands for, so the binder does not have to know Compose exists (INTENT §2.3). @@ -201,7 +201,7 @@ notebook is git-ignored, so in a worktree or CI checkout it is absent, the expec and **the test passes without checking anything.** It is only meaningful in the maintainer's main checkout. -## 3. `pythonx` is a real package — `implemented` (module level) +## 3. `pythonx` is a real package: `implemented` (module level) Required by INTENT §2.2: `import pythonx.compose.material3` (and every other mapped module) loads **this repository's files on disk**, and those files import the binder-exposed @@ -236,7 +236,7 @@ only, and a method's keywords are Kotlin's. ## 4. Naming -### S4.1 Kotlin parameters, `snake_case` — `implemented` +### S4.1 Kotlin parameters, `snake_case`: `implemented` `onClick` → `on_click`, `fillMaxWidth` → `fill_max_width`, `zIndex` → `z_index`, `toURLString` → `to_url_string`; type names (`Modifier`) unchanged; an explicit overload keeps its @@ -249,14 +249,14 @@ Implemented for module-level names and keywords and for method names on a binder (`tests/test_chain.py::MethodKeywords`, 7 tests, and `MethodKeywordsNeedTheBindersDescription`, 2; needs python-multiplatform's `describe_member`, #54). Disabling the keyword map fails 4 tests. -### S4.2 The notebook's spellings are examples, not the contract — `implemented` as a rule +### S4.2 The notebook's spellings are examples, not the contract: `implemented` as a rule Where `UI.ipynb` writes `onclick`, the surface is `on_click` (`tests/test_chain.py::Names::test_on_click_not_onclick` records the decision). ## 5. Composables -### S5.1 `@Composable` decorator — `implemented` +### S5.1 `@Composable` decorator: `implemented` `pythonx.compose.runtime.Composable` is an identity decorator: it returns the exact function it was given, which still runs and keeps its name and docstring. Composer threading is not the caller's @@ -265,18 +265,18 @@ job. Tests: `tests/test_runtime_module.py::TheRuntimeSeam` (4 tests) and `::TheChaquopyMechanismIsGone` (3 tests), importing `pythonx.compose.runtime` the ordinary way. -### S5.4 The app root, `app` and `state` — `partial` (the binder path is tested against a fake host shaped after python-multiplatform #38; real Compose is the E2E module, issue #19) +### S5.4 The app root, `app` and `state`: `partial` (the binder path is tested against a fake host shaped after python-multiplatform #38; real Compose is the E2E module, issue #19) The host draws with `PythonContent("pythonx.compose.runtime", "app_root")` (python-multiplatform #18, issue #11). `pythonx.compose.runtime` provides: -- `app_root` — a Compose `MutableState` whose `.value` is a zero-argument callable, or `None` (the +- `app_root`: a Compose `MutableState` whose `.value` is a zero-argument callable, or `None` (the host draws nothing). It is created on first read, by the binder's `androidx.compose.runtime.mutableStateOf(None)`, because the binder may be installed after `pythonx` is imported; afterwards it is an ordinary module attribute, the same object every time. -- `@app` — `app(fn)` sets `app_root.value = fn` and returns `fn` unchanged. Declaring the root again +- `@app`: `app(fn)` sets `app_root.value = fn` and returns `fn` unchanged. Declaring the root again replaces the value, so the screen follows. There is no update or refresh function (INTENT §5.1). -- `state(initial)` — `mutableStateOf(initial)`, read and written through `.value`; no `getValue` or +- `state(initial)`: `mutableStateOf(initial)`, read and written through `.value`; no `getValue` or `setValue`. Both states come from one internal function, `_new_state`. When the binder cannot supply @@ -297,7 +297,7 @@ walked from a jar. It skips against a binder older than #38. That Compose observ recomposes is not shown here; it is the E2E module (python-multiplatform #26, issue #19). The stub carries `app`, `state` and `app_root` (`tests/test_stubs.py`). -### S5.2 Material 3 composables reach Python without per-widget wrappers — `partial` +### S5.2 Material 3 composables reach Python without per-widget wrappers: `partial` `Text`, `Button`, `Card`, `ListItem`, `Badge`, `BadgedBox`, `MaterialTheme`, `IconButton` and its toggle family, `TextField`, `Checkbox` and `Switch` are meant to be reached through the generated @@ -313,7 +313,7 @@ toggle family, `TextField`, `Checkbox` and `Switch` are meant to be reached thro host binds no material3 declaration, so no test here exercises one; the render proof per widget is issue #9. -### S5.3 `DefaultIcons`, `Icon` and colour schemes — `partial` +### S5.3 `DefaultIcons`, `Icon` and colour schemes: `partial` `DefaultIcons` is `implemented` against the fake host: `pythonx.compose.material.icons` (`androidx.compose.material.icons`) serves `Icons`; `Icons.Default` resolves to `Icons.Filled` @@ -334,7 +334,7 @@ Still `planned`: the two colour-scheme factories take 36 `Color` parameters agai the binding's omission cap (python-multiplatform `a6742a1c`). The hand-written `icon.py` / `color_scheme.py` that recorded this were dead code and are deleted (#31). -### S5.5 `TextField(state=...)` and `TextFieldState` — `partial` +### S5.5 `TextField(state=...)` and `TextFieldState`: `partial` INTENT §5.8. `TextField` uses Compose 1.11's state-based overload, so Compose owns the text buffer and the IME composing region and no Python callback runs per keystroke. There is no per-widget @@ -373,9 +373,9 @@ and the field's text asserted afterwards, is python-multiplatform's E2E test (#2 four frames; it is not run here. Not modelled: a function-typed slot (`label`, `on_value_change`), which needs the binder's `NewFunction` rows. -## 6. Modifiers — extension functions as methods +## 6. Modifiers: extension functions as methods -### S6.1 `Modifier` extensions are methods on the receiver proxy — `implemented` +### 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 @@ -386,7 +386,7 @@ 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` +### S6.2 The empty-`Modifier` seam: `partial` `pythonx/compose/ui/modifier.py` provides `install(empty_factory)` to register which Kotlin function returns an empty `Modifier`, and resolves `Modifier` lazily. **No such function exists in Compose**: @@ -396,14 +396,14 @@ one-line Kotlin factory. The instance spelling (`m.padding(16)` on a `Modifier` does not need it. `tests/test_modifier_module.py::TheShellIsGone` (3 tests, passing) checks the old hand-written shell is gone. -### S6.3 Overload dispatch — `implemented` +### S6.3 Overload dispatch: `implemented` Among Kotlin overloads of one name, a call is dispatched by keyword name, argument count, then declared type; a non-match names the candidates; an explicit overload spelling (`padding__Dp`) bypasses dispatch; the module function takes snake_case keywords. Evidence: `tests/test_chain.py::OverloadDispatch` (7 tests). Status: `implemented`. -### S6.4 Value classes — `implemented` +### 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 @@ -411,14 +411,14 @@ says why; the allow-list can be extended at run time. The allow-list itself is t (S2, implemented) and reaches the binder when a `pythonx.compose` module first resolves a name. Runtime evidence: `tests/test_chain.py::ValueClasses` (6 tests). -### S6.5 Lazy resolution and handle lifetime — `implemented` +### S6.5 Lazy resolution and handle lifetime: `implemented` A mapped module is a file on disk and imports without a binder; one with no file is not importable; a name is adapted once and then lives in the module dict; `dir()` reports the Pythonic names only; without the binding layer a name read says the host never installed it; dropping a proxy releases its Kotlin handle. Evidence: `tests/test_chain.py::Laziness` (5 tests), `::Handles` (1 test). -## 7. Layout constants — `Alignment` and `Arrangement` — `implemented` +## 7. Layout constants, `Alignment` and `Arrangement`: `implemented` The bound constant names are documented in `pythonx/compose/ui/alignment.py` (15 `Alignment` names) and `pythonx/compose/layout/arrangement.py` (8 `Arrangement` names), checked by @@ -436,7 +436,7 @@ every access; a function inside the object is snake_case (`Arrangement.spaced_by `tests/test_chain.py::ObjectNamespaces` (6 tests); without the sub-package step 6 fail. The binder now lists these objects itself (python-multiplatform #35), and either way reaches them. -### S7.1 Grouped constants — `Alignment.Horizontal.End` — `implemented` +### S7.1 Grouped constants, `Alignment.Horizontal.End`: `implemented` INTENT §5.3: the notebook groups constants by type, `Alignment.Horizontal.End`, and Kotlin writes them flat, `Alignment.End` (which *is* an `Alignment.Horizontal`). Both spellings are served, by one @@ -492,7 +492,7 @@ with the group lookup disabled 11 fail, and the one that stays green checks the Compose surface is an extension receiver named `Alignment.Horizontal` or `Arrangement.Horizontal`, so the binder lists no member that would win over these groups. -## 8. The notebook surface not yet covered — `planned` +## 8. The notebook surface not yet covered: `planned` `UI.ipynb` imports or uses these, and nothing in this repository provides them: @@ -522,7 +522,7 @@ These tests pass and assert that retired 2024 mechanisms are gone. They are not --- -## Outside intent — needs a decision +## Outside intent: needs a decision Found in the repository; not covered by `docs/INTENT.md`, or in conflict with it. diff --git a/docs/guide/assets/style.css b/docs/guide/assets/style.css index 932d708..3d0beb1 100644 --- a/docs/guide/assets/style.css +++ b/docs/guide/assets/style.css @@ -1,4 +1,4 @@ -/* pythonx-compose guide — shared thisisthepy family tokens, accent #7c4dff. */ +/* pythonx-compose guide: shared thisisthepy family tokens, accent #7c4dff. */ :root { --bg: #fbfbfa; --fg: #16181d; --muted: #5b6270; --line: #e6e6e3; --code-bg: #f3f3f1; --radius: 10px; --maxw: 72rem; @@ -146,7 +146,7 @@ a.card:hover { text-decoration: none; border-color: var(--accent); } ul.clean { padding-left: 1.1rem; margin: 0 0 1rem; } ul.clean li { margin: 0.3rem 0; } -.not li::marker { content: "— "; color: var(--muted); } +.not li::marker { content: "– "; color: var(--muted); } /* Tables */ .table-wrap { overflow-x: auto; margin: 1rem 0 1.5rem; border: 1px solid var(--line); border-radius: var(--radius); } diff --git a/docs/guide/concepts.html b/docs/guide/concepts.html index e3715d3..b18981e 100644 --- a/docs/guide/concepts.html +++ b/docs/guide/concepts.html @@ -32,14 +32,14 @@

The two layers두 계층

-

python-multiplatform — the binder — embeds CPython in a Kotlin Multiplatform app and exposes Kotlin declarations to Python under their Kotlin names: androidx.compose.material3 is androidx.compose.material3. It never renames a namespace.

python-multiplatform — 바인더 — 는 Kotlin Multiplatform 앱에 CPython 을 임베딩하고, Kotlin 선언을 Kotlin 이름 그대로 파이썬에 노출합니다: androidx.compose.material3 는 androidx.compose.material3 입니다. 네임스페이스 이름을 절대 바꾸지 않습니다.

-

pythonx-compose — this package — is real Python on disk. Its modules import those androidx.compose.* modules and restructure them into pythonx.compose.*.

pythonx-compose — 이 패키지 — 는 디스크에 있는 실제 파이썬입니다. 이 패키지의 모듈이 androidx.compose.* 를 import 해 pythonx.compose.* 로 재구성합니다.

+

python-multiplatform, the binder, embeds CPython in a Kotlin Multiplatform app and exposes Kotlin declarations to Python under their Kotlin names: androidx.compose.material3 is androidx.compose.material3. It never renames a namespace.

python-multiplatform(바인더)는 Kotlin Multiplatform 앱에 CPython 을 임베딩하고, Kotlin 선언을 Kotlin 이름 그대로 파이썬에 노출합니다: androidx.compose.material3 는 androidx.compose.material3 입니다. 네임스페이스 이름을 절대 바꾸지 않습니다.

+

pythonx-compose, this package, is real Python on disk. Its modules import those androidx.compose.* modules and restructure them into pythonx.compose.*.

pythonx-compose(이 패키지)는 디스크에 있는 실제 파이썬입니다. 이 패키지의 모듈이 androidx.compose.* 를 import 해 pythonx.compose.* 로 재구성합니다.

@Composable def Screen()your Pythonpythonx.composethis package · real .pyandroidx.compose.*Kotlin names · binderComposeJetpack / Multiplatformpythonx-map.tomlthe one mapping.pyi stubsin the wheel@Composable def Screen()your Pythonpythonx.composethis package · real .pyandroidx.compose.*Kotlin names · binderComposeJetpack / Multiplatformpythonx-map.toml→ pythonx.compose, .pyi

The naming rule이름 규칙

-

Expose the original Kotlin declarations and parameters with Pythonic names. A name that starts upper-case — a type, an object, a composable — keeps its Kotlin spelling. Every other name is reached by its snake_case spelling only; the camelCase spelling is not a second name. An explicit overload keeps its type suffix.

Kotlin 의 원래 선언과 매개변수를 파이썬다운 이름으로 노출합니다. 대문자로 시작하는 이름 — 타입, 객체, 컴포저블 — 은 Kotlin 표기를 유지합니다. 그 밖의 이름은 snake_case 표기로만 접근하며, camelCase 표기가 또 하나의 이름이 되지는 않습니다. 명시적 오버로드는 타입 접미사를 유지합니다.

+

Expose the original Kotlin declarations and parameters with Pythonic names. A name that starts upper-case (a type, an object, a composable) keeps its Kotlin spelling. Every other name is reached by its snake_case spelling only; the camelCase spelling is not a second name. An explicit overload keeps its type suffix.

Kotlin 의 원래 선언과 매개변수를 파이썬다운 이름으로 노출합니다. 대문자로 시작하는 이름(타입, 객체, 컴포저블)은 Kotlin 표기를 유지합니다. 그 밖의 이름은 snake_case 표기로만 접근하며, camelCase 표기가 또 하나의 이름이 되지는 않습니다. 명시적 오버로드는 타입 접미사를 유지합니다.

@@ -69,7 +69,7 @@

One re-export rule재노출

Extension functions are methods확장 함수는 메서드

Most of Modifier is Kotlin extension functions. In Python each one is a method on the receiver's proxy, so a chain reads as it does in Kotlin. The binder's proxy class keeps Kotlin names; the same naming rule is registered as the binder's member resolver, so the snake_case spelling reaches the Kotlin member.

Modifier 의 대부분은 Kotlin 확장 함수입니다. 파이썬에서는 각각이 수신 객체 프록시의 메서드가 되어, 체인이 Kotlin 에서처럼 읽힙니다. 바인더의 프록시 클래스는 Kotlin 이름을 유지하며, 같은 이름 규칙이 바인더의 멤버 리졸버로 등록되어 snake_case 표기가 Kotlin 멤버에 닿습니다.

Modifier.padding(16).size(24).fill_max_width()
-

Keyword arguments to such a method are snake_case too — m.padding(padding_values=…), as the module function takes padding(m, padding_values=…). The resolver also hands the binder a keyword map read from the declaration's own parameter names. Kotlin's spelling (paddingValues=) still works at run time; the type stubs offer only the Pythonic one.

이런 메서드의 키워드 인자도 snake_case 입니다 — m.padding(padding_values=…) 이며, 모듈 함수는 padding(m, padding_values=…) 를 받습니다. 리졸버는 선언의 실제 파라미터 이름에서 읽은 키워드 맵도 바인더에 넘깁니다. Kotlin 이름(paddingValues=)도 실행 시에는 그대로 통하지만, 타입 스텁은 Pythonic 이름만 제시합니다.

+

Keyword arguments to such a method are snake_case too: m.padding(padding_values=…), as the module function takes padding(m, padding_values=…). The resolver also hands the binder a keyword map read from the declaration's own parameter names. Kotlin's spelling (paddingValues=) still works at run time; the type stubs offer only the Pythonic one.

이런 메서드의 키워드 인자도 snake_case 입니다: m.padding(padding_values=…) 이며, 모듈 함수는 padding(m, padding_values=…) 를 받습니다. 리졸버는 선언의 실제 파라미터 이름에서 읽은 키워드 맵도 바인더에 넘깁니다. Kotlin 이름(paddingValues=)도 실행 시에는 그대로 통하지만, 타입 스텁은 Pythonic 이름만 제시합니다.

@@ -87,7 +87,7 @@

Kotlin objectsKotlin 객체

Value classes값 클래스

-

Dp wraps a single float, so a raw number may stand for it: padding(16) means padding(16.dp). That permission is the manifest's allowlist, handed to the binder when a pythonx.compose module first resolves a name. Color and TextUnit pack several fields into one value, so a raw number would decode as something else entirely — they are refused, with a message saying why. A colour is written through Kotlin's own factory: Color(0xFFFF0000).

Dp 는 float 하나를 감싸므로 숫자로 쓸 수 있습니다: padding(16) 은 padding(16.dp) 입니다. 이 허용은 매니페스트의 허용 목록이며, pythonx.compose 모듈이 처음 이름을 해석할 때 바인더에 전달됩니다. Color 와 TextUnit 은 여러 필드를 한 값에 담으므로 숫자를 넣으면 전혀 다른 값으로 해석됩니다 — 그래서 이유를 밝히며 거절합니다. 색은 Kotlin 자신의 팩토리로 씁니다: Color(0xFFFF0000).

+

Dp wraps a single float, so a raw number may stand for it: padding(16) means padding(16.dp). That permission is the manifest's allowlist, handed to the binder when a pythonx.compose module first resolves a name. Color and TextUnit pack several fields into one value, so a raw number would decode as something else entirely, so they are refused, with a message saying why. A colour is written through Kotlin's own factory: Color(0xFFFF0000).

Dp 는 float 하나를 감싸므로 숫자로 쓸 수 있습니다: padding(16) 은 padding(16.dp) 입니다. 이 허용은 매니페스트의 허용 목록이며, pythonx.compose 모듈이 처음 이름을 해석할 때 바인더에 전달됩니다. Color 와 TextUnit 은 여러 필드를 한 값에 담으므로 숫자를 넣으면 전혀 다른 값으로 해석됩니다. 그래서 이유를 밝히며 거절합니다. 색은 Kotlin 자신의 팩토리로 씁니다: Color(0xFFFF0000).

@@ -105,7 +105,7 @@

@Composable@Composable
-pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. +pythonx-compose: Compose, restructured for Python.pythonx-compose: 파이썬을 위해 재구성한 Compose. GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/guide/ecosystem.html b/docs/guide/ecosystem.html index 0f0dded..adc4adb 100644 --- a/docs/guide/ecosystem.html +++ b/docs/guide/ecosystem.html @@ -43,7 +43,7 @@

Who owns what누가 무엇
-pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. +pythonx-compose: Compose, restructured for Python.pythonx-compose: 파이썬을 위해 재구성한 Compose. GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/guide/getting-started.html b/docs/guide/getting-started.html index c2e6d4a..946011a 100644 --- a/docs/guide/getting-started.html +++ b/docs/guide/getting-started.html @@ -30,22 +30,20 @@

Getting started시작하기

From clone to first call클론부터 첫 호출까지

Five minutes, honest about what runs.5 분, 동작하는 범위를 솔직하게.

-

pythonx-compose is an alpha: 0.1.0a1 is on PyPI. As a pre-release it installs with pip install --pre pythonx-compose. This page shows how to get the source, run the tests, and what you can call today.

pythonx-compose 는 알파 단계이며 0.1.0a1 이 PyPI 에 있습니다. 프리릴리스이므로 pip install --pre pythonx-compose 로 설치합니다. 이 페이지는 소스를 받고, 테스트를 돌리고, 지금 호출할 수 있는 것을 보여줍니다.

+

pythonx-compose is an alpha: 0.1.0a1 is on PyPI. As a pre-release it installs with uv add --prerelease allow pythonx-compose; with ppp it is ppp core add "pythonx-compose==0.1.0a1", with tcl tcl install pythonx-compose. It needs Python 3.11 or later. This page shows how to get the source, run the tests, and what you can call today.

pythonx-compose 는 알파 단계이며 0.1.0a1 이 PyPI 에 있습니다. 프리릴리스이므로 uv add --prerelease allow pythonx-compose 로 설치하며, ppp 로는 ppp core add "pythonx-compose==0.1.0a1", tcl 로는 tcl install pythonx-compose 입니다. 파이썬 3.11 이상이 필요합니다. 이 페이지는 소스를 받고, 테스트를 돌리고, 지금 호출할 수 있는 것을 보여줍니다.

1. Get the source1. 소스 받기

git clone https://github.com/thisisthepy/pythonx-compose
-cd pythonx-compose
-python3 -m venv .venv && . .venv/bin/activate
-python3 -m pip install pytest mypy      # Python 3.11 or later
+cd pythonx-compose

2. Run the tests2. 테스트 실행

-
python3 -m pytest tests -q
+
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 체크아웃이 있거나 다음이 필요합니다:

-
PYTHONMULTIPLATFORM_HOME=/path/to/python-multiplatform python3 -m 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 이후)이면 230 개가 통과하고 1 개(UI.ipynb 테스트)를 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 건너뜁니다. 예상된 상태이며 기록되어 있습니다.

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

@@ -84,7 +82,7 @@

3. What you can call today3

4. What you write in an app4. 앱 안에서 쓰는 코드

-

Inside an app whose Kotlin host has installed the binder, a screen looks like this — every name and parameter is Compose's own, spelled by the one rule:

Kotlin 호스트가 바인더를 설치한 앱 안에서는, 화면이 이렇게 됩니다 — 모든 이름과 매개변수는 Compose 자신의 것이며, 규칙 하나로 표기됩니다:

+

Inside an app whose Kotlin host has installed the binder, a screen looks like this: every name and parameter is Compose's own, spelled by the one rule:

Kotlin 호스트가 바인더를 설치한 앱 안에서는, 화면이 이렇게 됩니다. 모든 이름과 매개변수는 Compose 자신의 것이며, 규칙 하나로 표기됩니다:

from pythonx.compose.runtime import Composable
 from pythonx.compose.material3 import Text, Button, Column   # Column also from layout
 from pythonx.compose.layout import Arrangement
@@ -101,7 +99,7 @@ 

4. What you write in an app Button(on_click=lambda: None, content=lambda: Text("Press")), ), )

-

Two things to know. A chain that starts from the class, Modifier.padding(16), needs the app to register an empty-Modifier factory against real Compose — see Modifiers. And the screen above runs inside the app; a declared app root that redefining a screen replaces (issue #11) is planned, not built.

알아 둘 것이 두 가지 있습니다. 클래스에서 시작하는 체인 Modifier.padding(16) 은 실제 Compose 에서 앱이 빈 Modifier 팩토리를 등록해야 합니다 — 모디파이어를 보세요. 그리고 위 화면은 앱 안에서 실행됩니다. 화면을 다시 정의하면 교체되는 선언형 앱 루트(이슈 #11)는 계획 단계이며 아직 만들어지지 않았습니다.

+

Two things to know. A chain that starts from the class, Modifier.padding(16), needs the app to register an empty-Modifier factory against real Compose; see Modifiers. And the screen above runs inside the app; a declared app root that redefining a screen replaces (issue #11) is planned, not built.

알아 둘 것이 두 가지 있습니다. 클래스에서 시작하는 체인 Modifier.padding(16) 은 실제 Compose 에서 앱이 빈 Modifier 팩토리를 등록해야 합니다. 모디파이어를 보세요. 그리고 위 화면은 앱 안에서 실행됩니다. 화면을 다시 정의하면 교체되는 선언형 앱 루트(이슈 #11)는 계획 단계이며 아직 만들어지지 않았습니다.

@@ -109,7 +107,7 @@

4. What you write in an app
-pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. +pythonx-compose: Compose, restructured for Python.pythonx-compose: 파이썬을 위해 재구성한 Compose. GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/guide/guide-composables.html b/docs/guide/guide-composables.html index 50fe3cf..0324809 100644 --- a/docs/guide/guide-composables.html +++ b/docs/guide/guide-composables.html @@ -44,7 +44,7 @@

Write a screen화면 하나 def Profile(name: str): Card(content=lambda: Text(f"Hello, {name}"))

A screen is a plain function. @Composable marks it; it does not wrap it. Content slots take a lambda.

화면은 평범한 함수입니다. @Composable 은 표시만 하고 감싸지 않습니다. content 슬롯은 람다를 받습니다.

-

Column, Row and Spacer are declared in Kotlin's layout package, and the notebook imports them from material3. Both work, and both give the same object — the manifest's [aliases] section lends them to material3:

Column, Row, Spacer 는 Kotlin 의 layout 패키지에 선언되어 있고, 노트북은 이들을 material3 에서 import 합니다. 둘 다 되며 같은 객체를 줍니다 — 매니페스트의 [aliases] 섹션이 이들을 material3 에 빌려줍니다:

+

Column, Row and Spacer are declared in Kotlin's layout package, and the notebook imports them from material3. Both work, and both give the same object: the manifest's [aliases] section lends them to material3:

Column, Row, Spacer 는 Kotlin 의 layout 패키지에 선언되어 있고, 노트북은 이들을 material3 에서 import 합니다. 둘 다 되며 같은 객체를 줍니다. 매니페스트의 [aliases] 섹션이 이들을 material3 에 빌려줍니다:

from pythonx.compose.material3 import Column, Row, Spacer   # as the notebook writes
 from pythonx.compose.layout import Column                   # Kotlin's home; same object

@@ -78,7 +78,7 @@

A text field텍스트 필 draft = TextFieldState("hi") draft.set_text_and_place_cursor_at_end("hello") draft.clear_text() -

Compose keeps the text and the input method's composing region, so nothing is sent to Python per keystroke; you read field.text on demand. Keep this state apart from state you stream output into, such as messages = state(""). The notebook's TextField(text_state=…, padding=8) has no Kotlin counterpart and is written TextField(state=…, modifier=Modifier.padding(8)). Status: partial — tested against a fake host; the input-method proof is python-multiplatform's end-to-end test (#26).

Compose 가 텍스트와 입력기의 조합 영역을 보관하므로 키 입력마다 파이썬으로 보내지는 것이 없고, 필요할 때 field.text 를 읽습니다. 이 상태는 출력을 흘려 넣는 상태(예: messages = state(""))와 분리해 두세요. 노트북의 TextField(text_state=…, padding=8) 은 Kotlin 에 대응이 없으며 TextField(state=…, modifier=Modifier.padding(8)) 로 씁니다. 상태: 부분 — 가짜 호스트로 테스트되었으며, 입력기 증거는 python-multiplatform 의 종단 간 테스트(#26)입니다.

+

Compose keeps the text and the input method's composing region, so nothing is sent to Python per keystroke; you read field.text on demand. Keep this state apart from state you stream output into, such as messages = state(""). The notebook's TextField(text_state=…, padding=8) has no Kotlin counterpart and is written TextField(state=…, modifier=Modifier.padding(8)). Status: partial. Tested against a fake host; the input-method proof is python-multiplatform's end-to-end test (#26).

Compose 가 텍스트와 입력기의 조합 영역을 보관하므로 키 입력마다 파이썬으로 보내지는 것이 없고, 필요할 때 field.text 를 읽습니다. 이 상태는 출력을 흘려 넣는 상태(예: messages = state(""))와 분리해 두세요. 노트북의 TextField(text_state=…, padding=8) 은 Kotlin 에 대응이 없으며 TextField(state=…, modifier=Modifier.padding(8)) 로 씁니다. 상태: 부분. 가짜 호스트로 테스트되었으며, 입력기 증거는 python-multiplatform 의 종단 간 테스트(#26)입니다.

@@ -90,7 +90,7 @@

From the notebook's spelling to the real parameters

- +
KotlinPython
Button(onclick=…)onClickon_click
Text(font_size=30)fontSizefont_size
Column(horizontal_alignment=…)horizontalAlignmenthorizontal_alignment
Button(corner_radius=50)none — Compose takes a shape없음 — Compose 는 shape 를 받음shape
Button(corner_radius=50)none: Compose takes a shape없음: Compose 는 shape 를 받음shape
TextField(text_state=…, padding=8)state, modifierstate=…, modifier=Modifier.padding(8)

fontSize is a TextUnit, which packs several fields into one value, so font_size refuses a raw number such as 30.

fontSize 는 여러 필드를 한 값에 담는 TextUnit 이므로, font_size 는 30 같은 숫자를 거절합니다.

@@ -111,8 +111,8 @@

Spellings that changed바

What does not work yet아직 안 되는 것

    -
  • The colour-scheme factories cannot be called yet: they take 36 colour parameters. DefaultIcons is Icons.Default, an icon is a value (an ImageVector to a type checker) read without parentheses — Icon(DefaultIcons.Add, content_description=None), not the notebook's DefaultIcons.Add() — and it is tested here against a fake host only; the real draw is in python-multiplatform's render test.색 스킴 팩토리는 색 매개변수를 36 개 받아 아직 호출할 수 없습니다. DefaultIcons 는 Icons.Default 이고, 아이콘은 괄호 없이 읽는 값(타입 검사기에는 ImageVector)입니다 — 노트북의 DefaultIcons.Add() 가 아니라 Icon(DefaultIcons.Add, content_description=None). 여기서는 가짜 호스트로만 테스트되며, 실제 그리기는 python-multiplatform 의 렌더 테스트에 있습니다.
  • -
  • A declared app root and Pythonic state — redefining a screen replaces it, with no update call — are planned (issue #11).선언형 앱 루트와 파이썬다운 상태 — 화면을 다시 정의하면 교체되고, 갱신 호출은 없음 — 는 계획 단계입니다(이슈 #11).
  • +
  • The colour-scheme factories cannot be called yet: they take 36 colour parameters. DefaultIcons is Icons.Default, an icon is a value (an ImageVector to a type checker) read without parentheses: Icon(DefaultIcons.Add, content_description=None), not the notebook's DefaultIcons.Add(). It is tested here against a fake host only; the real draw is in python-multiplatform's render test.색 스킴 팩토리는 색 매개변수를 36 개 받아 아직 호출할 수 없습니다. DefaultIcons 는 Icons.Default 이고, 아이콘은 괄호 없이 읽는 값(타입 검사기에는 ImageVector)입니다. 노트북의 DefaultIcons.Add() 가 아니라 Icon(DefaultIcons.Add, content_description=None). 여기서는 가짜 호스트로만 테스트되며, 실제 그리기는 python-multiplatform 의 렌더 테스트에 있습니다.
  • +
  • A declared app root and Pythonic state (redefining a screen replaces it, with no update call) are planned (issue #11).선언형 앱 루트와 파이썬다운 상태(화면을 다시 정의하면 교체되고, 갱신 호출은 없음)는 계획 단계입니다(이슈 #11).
  • TextField(state=…) is wired and tested against a fake host only; that the input method's composing sequence survives, with no Python callback per keystroke, is proved in python-multiplatform's end-to-end test (#26).TextField(state=…) 는 가짜 호스트로만 테스트되었습니다. 입력기의 조합 입력이 키 입력마다 파이썬 콜백 없이 유지되는지는 python-multiplatform 의 종단 간 테스트(#26)에서 증명합니다.
  • remember_saveable and the coroutine scopes the notebook imports are planned.노트북이 import 하는 remember_saveable, 코루틴 스코프는 계획 단계입니다.
@@ -123,7 +123,7 @@

What does not work yet아
-pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. +pythonx-compose: Compose, restructured for Python.pythonx-compose: 파이썬을 위해 재구성한 Compose. GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/guide/guide-manifest.html b/docs/guide/guide-manifest.html index 7302722..d4fb26f 100644 --- a/docs/guide/guide-manifest.html +++ b/docs/guide/guide-manifest.html @@ -62,7 +62,7 @@

Lending a name to another module

Keeping it in step with the notebook노트북과 맞춰 두기

-

A test reads every pythonx import in the user's notebook and fails if the manifest does not map it. The notebook lives only in the maintainer's checkout, so elsewhere that test has nothing to check — run it there before claiming coverage.

테스트 하나가 사용자 노트북의 모든 pythonx import 를 읽고, 매니페스트가 대응시키지 않으면 실패합니다. 노트북은 메인테이너의 체크아웃에만 있으므로 다른 곳에서는 그 테스트가 확인할 것이 없습니다 — 커버리지를 주장하기 전에 그곳에서 돌리세요.

+

A test reads every pythonx import in the user's notebook and fails if the manifest does not map it. The notebook lives only in the maintainer's checkout, so elsewhere that test has nothing to check, so run it there before claiming coverage.

테스트 하나가 사용자 노트북의 모든 pythonx import 를 읽고, 매니페스트가 대응시키지 않으면 실패합니다. 노트북은 메인테이너의 체크아웃에만 있으므로 다른 곳에서는 그 테스트가 확인할 것이 없습니다. 커버리지를 주장하기 전에 그곳에서 돌리세요.

@@ -70,7 +70,7 @@

Keeping it in step with the notebook
-pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. +pythonx-compose: Compose, restructured for Python.pythonx-compose: 파이썬을 위해 재구성한 Compose. GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/guide/guide-modifiers.html b/docs/guide/guide-modifiers.html index 271bc19..9c9d891 100644 --- a/docs/guide/guide-modifiers.html +++ b/docs/guide/guide-modifiers.html @@ -32,7 +32,7 @@

partial부분

-

Chains, snake_case method names, overload dispatch and Dp as a number are implemented and pass against the binder's adaptation layer (python-multiplatform ba4c6f49 or later). The partial piece is where a chain starts against real Compose — see the empty Modifier below.

체인, snake_case 메서드 이름, 오버로드 디스패치, 숫자로 쓰는 Dp 는 구현되어 있고 바인더의 적응 계층(python-multiplatform ba4c6f49 이후)에서 통과합니다. 부분적인 것은 실제 Compose 에서 체인이 시작되는 지점입니다 — 아래 빈 Modifier 를 보세요.

+

Chains, snake_case method names, overload dispatch and Dp as a number are implemented and pass against the binder's adaptation layer (python-multiplatform ba4c6f49 or later). The partial piece is where a chain starts against real Compose. See the empty Modifier below.

체인, snake_case 메서드 이름, 오버로드 디스패치, 숫자로 쓰는 Dp 는 구현되어 있고 바인더의 적응 계층(python-multiplatform ba4c6f49 이후)에서 통과합니다. 부분적인 것은 실제 Compose 에서 체인이 시작되는 지점입니다. 아래 빈 Modifier 를 보세요.

@@ -77,7 +77,7 @@

Numbers and value classes

The empty Modifier빈 Modifier

-

In Kotlin, Modifier as an expression is an object, and bindings are functions — so there is no bound name for the empty modifier a chain starts from. pythonx/compose/ui/modifier.py is the seam: an application registers a one-line Kotlin factory.

Kotlin 에서 식으로 쓰인 Modifier 는 객체이고 바인딩은 함수이므로, 체인이 시작하는 빈 모디파이어에는 바인딩된 이름이 없습니다. pythonx/compose/ui/modifier.py 가 그 접점이며, 앱이 한 줄짜리 Kotlin 팩토리를 등록합니다.

+

In Kotlin, Modifier as an expression is an object, and bindings are functions, so there is no bound name for the empty modifier a chain starts from. pythonx/compose/ui/modifier.py is the seam: an application registers a one-line Kotlin factory.

Kotlin 에서 식으로 쓰인 Modifier 는 객체이고 바인딩은 함수이므로, 체인이 시작하는 빈 모디파이어에는 바인딩된 이름이 없습니다. pythonx/compose/ui/modifier.py 가 그 접점이며, 앱이 한 줄짜리 Kotlin 팩토리를 등록합니다.

// in the application's Kotlin
 fun emptyModifier(): Modifier = Modifier
import pythonx.compose.ui.modifier as seam
@@ -91,7 +91,7 @@ 

The empty Modifier빈 Modif
-pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. +pythonx-compose: Compose, restructured for Python.pythonx-compose: 파이썬을 위해 재구성한 Compose. GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/guide/index.html b/docs/guide/index.html index 38a24b4..c3c0ccb 100644 --- a/docs/guide/index.html +++ b/docs/guide/index.html @@ -31,9 +31,9 @@
-pre-alpha · pip install --pre pythonx-composepre-alpha · pip install --pre pythonx-compose +pre-alpha · uv add --prerelease allow pythonx-composepre-alpha · uv add --prerelease allow pythonx-compose

Compose, in Python.Compose 를, 파이썬으로.

-

The real Compose widgets with their real parameters — restructured into a Python package, spelled the way Python spells things.진짜 Compose 위젯과 진짜 매개변수를 — 파이썬 패키지로 재구성해, 파이썬이 쓰는 방식대로 씁니다.

+

The real Compose widgets with their real parameters, restructured into a Python package, spelled the way Python spells things.진짜 Compose 위젯과 진짜 매개변수를, 파이썬 패키지로 재구성해, 파이썬이 쓰는 방식대로 씁니다.

Get started시작하기 → How it works동작 방식 @@ -77,15 +77,15 @@

Two layers, one rule about names.What it holds to지키는 것

onClick → on_click

The original API원래 API 그대로

Compose's own parameters, in snake_case. No convenience parameters Kotlin does not have.

Compose 자신의 매개변수를 snake_case 로. Kotlin 에 없는 편의 매개변수는 만들지 않습니다.

-
Modifier.padding(16).size(24)

Extensions are methods확장 함수는 메서드

A Kotlin extension function is a method on its receiver, so chains read as they do in Kotlin — fill_max_width() included.

Kotlin 확장 함수는 수신 객체의 메서드가 되어, 체인이 Kotlin 에서처럼 읽힙니다 — fill_max_width() 도 마찬가지입니다.

+
Modifier.padding(16).size(24)

Extensions are methods확장 함수는 메서드

A Kotlin extension function is a method on its receiver, so chains read as they do in Kotlin, fill_max_width() included.

Kotlin 확장 함수는 수신 객체의 메서드가 되어, 체인이 Kotlin 에서처럼 읽힙니다. fill_max_width() 도 마찬가지입니다.

import androidx.compose…

A real package실제 패키지

pythonx/ is ordinary Python on disk. Renaming happens here, never in the binder.

pythonx/ 는 디스크에 있는 평범한 파이썬입니다. 이름 바꾸기는 여기서 하고, 바인더에서는 하지 않습니다.

@Composable

Composable stays@Composable 은 그대로

Screens are decorated Python functions, the way the notebook writes them.

화면은 노트북이 쓰는 그대로, 데코레이터가 붙은 파이썬 함수입니다.

What it is not이것이 아닌 것

    -
  • Not the binder — embedding CPython and the FFI belong to python-multiplatform.바인더가 아닙니다 — CPython 임베딩과 FFI 는 python-multiplatform 의 몫입니다.
  • +
  • Not the binder: embedding CPython and the FFI belong to python-multiplatform.바인더가 아닙니다: CPython 임베딩과 FFI 는 python-multiplatform 의 몫입니다.
  • Not a reimplementation of Compose, and not a new widget set.Compose 의 재구현도, 새 위젯 세트도 아닙니다.
  • -
  • Not runtime reflection — bindings are produced at build time, because native images and Kotlin/Native have none.런타임 리플렉션이 아닙니다 — 네이티브 이미지와 Kotlin/Native 에는 리플렉션이 없으므로 바인딩은 빌드 타임에 만듭니다.
  • +
  • Not runtime reflection: bindings are produced at build time, because native images and Kotlin/Native have none.런타임 리플렉션이 아닙니다: 네이티브 이미지와 Kotlin/Native 에는 리플렉션이 없으므로 바인딩은 빌드 타임에 만듭니다.
@@ -93,7 +93,7 @@

What it is not이것이 아
-pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. +pythonx-compose: Compose, restructured for Python.pythonx-compose: 파이썬을 위해 재구성한 Compose. GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/guide/status.html b/docs/guide/status.html index 07d5a54..c0bdeb1 100644 --- a/docs/guide/status.html +++ b/docs/guide/status.html @@ -34,13 +34,13 @@

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/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 체인, 오버로드 디스패치, 숫자 Dp
implemented구현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; a method's keywords need python-multiplatform's describe_member, and Kotlin's own spelling still works at run timesnake_case 이름·키워드·시그니처 — 모듈과 메서드; 메서드 키워드에는 python-multiplatform 의 describe_member 가 필요하고, 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.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, 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, 코루틴 스코프
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 체인, 오버로드 디스패치, 숫자 Dp
implemented구현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; a method's keywords need python-multiplatform's describe_member, and Kotlin's own spelling still works at run timesnake_case 이름·키워드·시그니처: 모듈과 메서드; 메서드 키워드에는 python-multiplatform 의 describe_member 가 필요하고, 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.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, 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 를 읽는 테스트입니다.

FAQ

-
Can I pip install it?pip 으로 설치할 수 있나요?

Yes: pip install --pre pythonx-compose installs 0.1.0a1, the first alpha. It needs a host app that embeds python-multiplatform to resolve Compose names.

네: pip install --pre pythonx-compose 로 첫 알파 0.1.0a1 을 설치합니다. Compose 이름을 쓰려면 python-multiplatform 을 품은 호스트 앱이 필요합니다.

Do I need to know Kotlin?Kotlin 을 알아야 하나요?

No. The widgets and parameters are Compose's, so Compose documentation applies — read onClick as on_click.

아니요. 위젯과 매개변수가 Compose 의 것이므로 Compose 문서가 그대로 적용됩니다 — onClick 을 on_click 으로 읽으면 됩니다.

Does it run on its own?단독으로 실행되나요?

No. It runs inside an app that embeds CPython through python-multiplatform.

아니요. python-multiplatform 으로 CPython 을 임베딩한 앱 안에서 동작합니다.

Why not look classes up at run time?왜 런타임에 클래스를 찾지 않나요?

GraalVM native images and Kotlin/Native have no reflection, so bindings are generated at build time.

GraalVM 네이티브 이미지와 Kotlin/Native 에는 리플렉션이 없으므로, 바인딩을 빌드 타임에 생성합니다.

What happened to the test/ directory?test/ 디렉터리는 어떻게 되었나요?

It no longer exists. It was a 2023–2024 Kotlin Multiplatform sample from the project's first generation, and it was deleted; the tag archive/pre-restructure keeps it.

더 이상 없습니다. 프로젝트 첫 세대의 2023–2024 Kotlin Multiplatform 샘플이었고 삭제되었으며, 태그 archive/pre-restructure 로 되찾을 수 있습니다.

+
Can I install it from PyPI?PyPI 에서 설치할 수 있나요?

Yes: uv add --prerelease allow pythonx-compose installs 0.1.0a1, the first alpha; with ppp use ppp core add "pythonx-compose==0.1.0a1", with tcl tcl install pythonx-compose. It needs a host app that embeds python-multiplatform to resolve Compose names.

네: uv add --prerelease allow pythonx-compose 로 첫 알파 0.1.0a1 을 설치합니다. ppp 는 ppp core add "pythonx-compose==0.1.0a1", tcl 은 tcl install pythonx-compose 입니다. Compose 이름을 쓰려면 python-multiplatform 을 품은 호스트 앱이 필요합니다.

Do I need to know Kotlin?Kotlin 을 알아야 하나요?

No. The widgets and parameters are Compose's, so Compose documentation applies; read onClick as on_click.

아니요. 위젯과 매개변수가 Compose 의 것이므로 Compose 문서가 그대로 적용됩니다. onClick 을 on_click 으로 읽으면 됩니다.

Does it run on its own?단독으로 실행되나요?

No. It runs inside an app that embeds CPython through python-multiplatform.

아니요. python-multiplatform 으로 CPython 을 임베딩한 앱 안에서 동작합니다.

Why not look classes up at run time?왜 런타임에 클래스를 찾지 않나요?

GraalVM native images and Kotlin/Native have no reflection, so bindings are generated at build time.

GraalVM 네이티브 이미지와 Kotlin/Native 에는 리플렉션이 없으므로, 바인딩을 빌드 타임에 생성합니다.

What happened to the test/ directory?test/ 디렉터리는 어떻게 되었나요?

It no longer exists. It was a 2023–2024 Kotlin Multiplatform sample from the project's first generation, and it was deleted; the tag archive/pre-restructure keeps it.

더 이상 없습니다. 프로젝트 첫 세대의 2023–2024 Kotlin Multiplatform 샘플이었고 삭제되었으며, 태그 archive/pre-restructure 로 되찾을 수 있습니다.

@@ -48,7 +48,7 @@

FAQ

-pythonx-compose — Compose, restructured for Python.pythonx-compose — 파이썬을 위해 재구성한 Compose. +pythonx-compose: Compose, restructured for Python.pythonx-compose: 파이썬을 위해 재구성한 Compose. GitHub · Apache License 2.0Apache 라이선스 2.0
diff --git a/docs/locale/README_ko.md b/docs/locale/README_ko.md index 959c7fe..b97dc23 100644 --- a/docs/locale/README_ko.md +++ b/docs/locale/README_ko.md @@ -4,7 +4,7 @@ # pythonx-compose -**Compose UI 를 파이썬으로 — Compose 의 위젯 그대로, 파이썬다운 이름으로.** +**Compose UI 를 파이썬으로, Compose 의 위젯 그대로, 파이썬다운 이름으로.** [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/LICENSE) [![Python](https://img.shields.io/badge/python-3.11%2B-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/pyproject.toml) @@ -19,7 +19,7 @@ ## 왜 만드는가 -Jetpack Compose 와 Compose Multiplatform 은 UI 를 만드는 훌륭한 방법입니다 — Kotlin 을 쓴다면요. +Jetpack Compose 와 Compose Multiplatform 은 UI 를 만드는 훌륭한 방법입니다. Kotlin 을 쓴다면요. `pythonx-compose` 는 파이썬을 쓰는 사람을 위한 것입니다. Compose 를 다시 구현하지도, 새 위젯 세트를 만들지도 않습니다. [python-multiplatform](https://github.com/thisisthepy/python-multiplatform) 이 Kotlin 이름 그대로 파이썬에 노출한 실제 `androidx.compose.*` API 를 가져와, 파이썬다운 패키지 @@ -53,7 +53,7 @@ def Greeting(): 수도 있습니다: `Alignment.Horizontal.End` 는 `Alignment.End` 입니다. - **`@Composable` 은 그대로.** 화면은 데코레이터가 붙은 파이썬 함수입니다. - **실제 패키지.** `pythonx/` 는 `androidx.compose.*` 를 import 해 재구성하는 평범한 파이썬 - 소스입니다. 바인더는 아무 이름도 바꾸지 않습니다 — 이름을 바꾸는 것은 이 패키지의 일입니다. + 소스입니다. 바인더는 아무 이름도 바꾸지 않습니다. 이름을 바꾸는 것은 이 패키지의 일입니다. - **하나의 매니페스트.** [`pythonx-map.toml`](https://github.com/thisisthepy/pythonx-compose/blob/main/pythonx/compose/pythonx-map.toml) 이 어떤 `pythonx.compose.*` 모듈이 어떤 Kotlin 패키지에 대응하는지 적습니다. 이 파일은 패키지 안에 있으며 wheel 에 함께 담깁니다. 런타임과 `.pyi` 생성기가 같은 파일을 읽으므로, 편집기가 자동완성하는 이름과 인터프리터가 @@ -85,15 +85,22 @@ flowchart LR > [!NOTE] > `pythonx-compose` 는 **알파** 단계입니다. `0.1.0a1` 이 [PyPI](https://pypi.org/project/pythonx-compose/) 에 -> 있으며, 프리릴리스이므로 `pip install --pre pythonx-compose` 로 설치합니다. +> 있으며, 프리릴리스이므로 uv(또는 ppp, tcl)로 설치합니다. + +```bash +uv add --prerelease allow pythonx-compose +# 또는 pypackpack 으로, ppp 워크스페이스의 패키지에 추가: +ppp core add "pythonx-compose==0.1.0a1" +# 또는 toolchain-lite 로: +tcl install pythonx-compose +``` 클론에서 테스트를 돌리려면: ```bash git clone https://github.com/thisisthepy/pythonx-compose cd pythonx-compose -python3 -m pip install pytest # 가상환경 안에서 -python3 -m pytest tests -q +uv run --with pytest --with mypy pytest tests -q ``` 저장소 루트에서 지금 동작하는 것: @@ -152,15 +159,15 @@ pip 패키지 **`pythonx-compose`**(파이썬 3.11 이상)로 배포되며, impo | `androidx.compose.*` 를 규칙 하나로 재노출하는 실제 디스크 패키지 `pythonx` | ✅ 구현, 테스트됨 | | snake_case 메서드를 쓰는 `Modifier` 체인, 오버로드 디스패치, 숫자로 쓰는 `Dp` | ✅ 구현, 바인더 계층으로 테스트됨 | | `layout` 뿐 아니라 `material3` 에서도 쓰는 `Column`, `Row`, `Spacer` | ✅ 구현, 테스트됨 | -| `snake_case` 메서드 키워드 인자 | ✅ 구현, 테스트됨 — 모듈 함수와 메서드(메서드는 python-multiplatform 의 `describe_member` 필요) | -| 빈 `Modifier` | 🟡 부분 — 클래스에서 시작하는 `Modifier.padding(16)` 은 실제 Compose 에서 앱이 제공하는 팩토리가 필요 | -| `Alignment` / `Arrangement` | ✅ 구현 및 테스트 — `Alignment.Center`, `Arrangement.spaced_by(8)`, 그리고 `Alignment.End` 와 나란히 묶음 표기 `Alignment.Horizontal.End` | -| Material 3 위젯 (`Text`, `Button`, `Card`, `TextField`, …) | 🟡 부분 — 규칙으로 재노출됨; 위젯별 렌더 증거 대기(#9) | +| `snake_case` 메서드 키워드 인자 | ✅ 구현, 테스트됨: 모듈 함수와 메서드(메서드는 python-multiplatform 의 `describe_member` 필요) | +| 빈 `Modifier` | 🟡 부분: 클래스에서 시작하는 `Modifier.padding(16)` 은 실제 Compose 에서 앱이 제공하는 팩토리가 필요 | +| `Alignment` / `Arrangement` | ✅ 구현 및 테스트: `Alignment.Center`, `Arrangement.spaced_by(8)`, 그리고 `Alignment.End` 와 나란히 묶음 표기 `Alignment.Horizontal.End` | +| Material 3 위젯 (`Text`, `Button`, `Card`, `TextField`, …) | 🟡 부분: 규칙으로 재노출됨; 위젯별 렌더 증거 대기(#9) | | 실제 Compose 1.11.1 에서 생성한 타입 스텁(`.pyi`), `py.typed` | ✅ 구현, mypy 로 검사; 아직 많은 타입이 `Any` 입니다(#12) | -| 배포 (`pythonx-compose`) | 🟡 부분 — wheel 에 매니페스트, 재노출 규칙, 스텁, `py.typed` 포함 | -| 갱신 호출 없는 선언형 앱 루트(`@app`)와 파이썬다운 상태(`state`) | 🟡 부분 — 바인더 경로는 python-multiplatform #38 모양의 가짜 호스트로 테스트됨, 실제 Compose 는 E2E 모듈(#11, #19), 숫자와 문자열은 `state` 로 왕복됨 | -| `TextField(state=...)` 와 `TextFieldState` (`pythonx.compose.foundation.text.input`) | 🟡 부분 — python-multiplatform #73 모양의 가짜 호스트로 테스트됨; 입력기(IME) 조합 증거는 python-multiplatform E2E #26 (#10) | -| `DefaultIcons`(`Icons.Default`), `DefaultIcons.Add` 로 씀 | 🟡 부분 — python-multiplatform #37/#38 모양의 가짜 호스트로 테스트됨; `Icon(DefaultIcons.Add, …)` 의 실제 그리기는 python-multiplatform 의 렌더 테스트에 있음 | +| 배포 (`pythonx-compose`) | 🟡 부분: wheel 에 매니페스트, 재노출 규칙, 스텁, `py.typed` 포함 | +| 갱신 호출 없는 선언형 앱 루트(`@app`)와 파이썬다운 상태(`state`) | 🟡 부분: 바인더 경로는 python-multiplatform #38 모양의 가짜 호스트로 테스트됨, 실제 Compose 는 E2E 모듈(#11, #19), 숫자와 문자열은 `state` 로 왕복됨 | +| `TextField(state=...)` 와 `TextFieldState` (`pythonx.compose.foundation.text.input`) | 🟡 부분: python-multiplatform #73 모양의 가짜 호스트로 테스트됨; 입력기(IME) 조합 증거는 python-multiplatform E2E #26 (#10) | +| `DefaultIcons`(`Icons.Default`), `DefaultIcons.Add` 로 씀 | 🟡 부분: python-multiplatform #37/#38 모양의 가짜 호스트로 테스트됨; `Icon(DefaultIcons.Add, …)` 의 실제 그리기는 python-multiplatform 의 렌더 테스트에 있음 | | 색 스킴 | ⏳ 계획 | | `remember_saveable`, 코루틴 스코프 | ⏳ 계획 | @@ -168,15 +175,15 @@ pip 패키지 **`pythonx-compose`**(파이썬 3.11 이상)로 배포되며, impo ## 📖 문서 -- **가이드** — [`docs/guide/`](https://thisisthepy.github.io/pythonx-compose/), 영어 / 한국어 -- **English README** — [`README.md`](https://github.com/thisisthepy/pythonx-compose/blob/main/README.md) +- **가이드**: [`docs/guide/`](https://thisisthepy.github.io/pythonx-compose/), 영어 / 한국어 +- **English README**: [`README.md`](https://github.com/thisisthepy/pythonx-compose/blob/main/README.md) ## 🔌 생태계 | 저장소 | 역할 | |---|---| | [python-multiplatform](https://github.com/thisisthepy/python-multiplatform) | 바인더: Kotlin Multiplatform 에 임베딩한 CPython, Kotlin 을 Kotlin 이름 그대로 파이썬에 노출 | -| **pythonx-compose** | 파이썬을 위해 재구성한 Compose — 이 저장소 | +| **pythonx-compose** | 파이썬을 위해 재구성한 Compose (이 저장소) | | [toolchain](https://github.com/thisisthepy/toolchain) | Python Multiplatform 앱을 위한 Gradle 빌드 플러그인 | | [pypackpack](https://github.com/thisisthepy/pypackpack) | 파이썬 프로젝트의 멀티플랫폼 배포 | | [torchnative](https://github.com/thisisthepy/torchnative) | 실제 PyTorch 생태계를 기기에서 실행 | diff --git a/docs/style/writing.md b/docs/style/writing.md new file mode 100644 index 0000000..b6c0093 --- /dev/null +++ b/docs/style/writing.md @@ -0,0 +1,51 @@ +# Writing style + +The house style for thisisthepy documentation. The maintainer named this repository's guide +(`docs/guide/`) as the reference for every repository, so other projects align their READMEs, guides +and docs with what is written here. When this file and the guide disagree, the guide's practice wins +and this file gets corrected. + +## Voice + +- **Say what is true now.** Describe what the code does today, in the present tense. Something not + built is marked as such, with its status (`implemented`, `partial`, `planned`) and the issue that + tracks it. Never describe a plan as if it shipped. +- **Plain and concrete.** No marketing words and no hedging. Name the actual thing: a module, a + function, a test, an issue number, a commit. +- **Short declarative sentences.** One idea per sentence, about 12 to 18 words on average in English. + A long sentence is split, not joined with a dash. +- **Reasons beside rules.** When a rule or a limitation is stated, the next clause or sentence gives + the reason ("because ...", "so that ..."). A reader should never have to guess why. +- **Examples over abstraction.** Show the spelling a user writes (`Modifier.padding(16).fill_max_width()`) + rather than describing it. + +## English and Korean + +Every user-facing page exists in both languages (README.md and docs/locale/README_ko.md; each +visible string of the guide in `data-lang="en"` and `data-lang="ko"`). + +- **Same content, same order, not word for word.** Each English paragraph has one Korean paragraph + saying the same thing in natural Korean. A sentence may be split or merged differently, but no + fact is added or dropped in one language only. +- **Korean register.** User-facing text (README_ko, the guide) uses the polite declarative, + 합니다체: "설치합니다", "동작합니다". Internal records (PROJECT.md, which is written in Korean) + use the plain declarative, 한다체: "한다", "두었다". +- **Terms.** Code identifiers, Kotlin names, commands and file names stay in their original spelling + inside code formatting, in both languages. A particle follows code with a space: "`UI.ipynb` 를", + "`state()` 로". Common technical words are written as Korean loanwords when that is how Korean + developers say them ("바인더", "스텁", "컴포저블", "모듈"); otherwise the English term stays + ("snake_case", "wheel", "proxy"). +- **Status words** pair one to one: implemented / 구현, partial / 부분, planned / 계획. +- **Headings** are short noun phrases in both languages ("The naming rule" / "이름 규칙"). + +## Mechanics + +- **No em-dash (U+2014)**, anywhere in documents or code. Split the sentence, or use a comma, a colon + or parentheses. The en-dash in ranges (2023–2024) is fine. +- **Install and run examples use uv, ppp (pypackpack) and tcl (toolchain-lite) only.** No `pip` + examples. Show uv first: `uv add --prerelease allow pythonx-compose`, then + `ppp core add "pythonx-compose==0.1.0a1"` and `tcl install pythonx-compose`; run with `uv run ...`. +- **Numbers carry their source.** A test count names the environment it was measured in; a claim + about upstream names the commit, PR or issue. +- **Links** in README files are absolute (they are read on pypi.org too), and README files never + link the internal documents (AGENTS.md, PROJECT.md, docs/INTENT.md, docs/SPEC.md). From 05700fbfff38f49d6ecf8fbc842975d05cb32415 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:55:02 +0900 Subject: [PATCH 07/16] Test: Run against the binder that translates names itself, and pin that both rules agree python-multiplatform #131 (develop 452cc807) restores snake_case names and keywords in the binder, by this package's rule. Step 1 of handing that back: this suite against #131, unchanged, gives 251 passed and 1 failed. The failure is a test that assumed a binder without describe_member refuses a snake_case method keyword; the binder now accepts it itself, so the test skips there. A new test pins that the binder's python_name and this package's agree on names that test the rule (acronyms, overload suffixes, upper-case names); it skips on a binder that renames nothing. test added: TheBindersNamingRuleIsThisPackages (11 names). test changed: test_a_snake_case_keyword_is_the_binders_refusal skips on #131. Suite with python-multiplatform 452cc807 (#131), main checkout: 253 passed, 1 skipped. The older sibling checkout is also run. Refs #79 --- tests/test_chain.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_chain.py b/tests/test_chain.py index 723546c..a5902e5 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -301,12 +301,36 @@ def test_the_name_resolves_and_kotlin_keywords_work(self): self.assertEqual("padding__PaddingValues", self.host.calls[-1]) def test_a_snake_case_keyword_is_the_binders_refusal(self): + import python_multiplatform import pythonx.compose.foundation.layout as layout + if hasattr(python_multiplatform, "python_name"): + self.skipTest("the binder translates names itself since python-multiplatform #131") with self.assertRaises(TypeError): self.empty().padding(padding_values=layout.padding_values_of(8)) +class TheBindersNamingRuleIsThisPackages(AdapterCase): + """python-multiplatform #131 restored the binder's own snake_case names, with this package's rule. + + The two must not drift: a name the binder serves and a name this package's stubs and re-export + spell would otherwise differ. Skips on a binder that renames nothing. + """ + + NAMES = ("fillMaxWidth", "toURLString", "zIndex", "padding__Dp_Dp", "onClick", "Modifier", + "paddingValuesOf", "HTMLParser", "layoutDirection", "spacedBy", "x") + + def test_both_rules_agree(self): + import python_multiplatform + from pythonx.compose._reexport import python_name + + if not hasattr(python_multiplatform, "python_name"): + self.skipTest("this binder predates python-multiplatform #131") + for name in self.NAMES: + with self.subTest(name=name): + self.assertEqual(python_name(name), python_multiplatform.python_name(name)) + + class Names(AdapterCase): """PascalCase for types, snake_case for everything else, forward by rule and never inverted.""" From dc5510b5a1742517eebd9d0406a330d741c1ba6e Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:58:28 +0900 Subject: [PATCH 08/16] Refactor: Stop registering a member resolver; proxy methods are the binder's Step 2 of handing names back to the binder (#79). python-multiplatform #131 serves snake_case method names and keywords on a proxy itself, by this package's rule, so the member resolver this package registered (member_name, and _method_keywords built from describe_member) is removed, along with its registration in _prime. The behaviour tests stay and pass against #131 -- Modifier.padding(16).fill_max_width(), and m.padding(padding_values=...) reaching paddingValues -- so a binder regression still shows here; on an older binder they skip, saying why. deleted: member_name, _method_keywords and their cache in _reexport.py. test removed: the two that inspected the resolver's map and cache, and the two that tested its fallback without describe_member. documentation corrected: _reexport.py's docstring, SPEC 0/3/S4.1. Suite with python-multiplatform 452cc807 (#131), main checkout: 252 passed, 1 skipped -> 249 passed (the 4 removed tests; the skip was among them). Without a checkout: 141 passed, 108 skipped. Refs #79 --- docs/SPEC.md | 31 ++++++--------- pythonx/compose/_reexport.py | 75 +++-------------------------------- tests/test_chain.py | 76 ++++-------------------------------- 3 files changed, 26 insertions(+), 156 deletions(-) diff --git a/docs/SPEC.md b/docs/SPEC.md index a7b81c1..496458d 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -23,16 +23,15 @@ Run from a worktree with `uv run --with pytest --with mypy pytest tests -q -rs` |---|---| | 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 | -| an older checkout, without `describe_member` (python-multiplatform #54) | the same, with 7 more skipped | +| 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. Against a binder without `describe(module, name)`, the 13 that check grouped constants (§7, S7.1) -skip. Against a binder without `describe_member`, the 7 that check a method's keywords (S4.1) skip; the 2 -that check the fallback (Kotlin keywords, names resolved) run. Against a binder older than `ba4c6f49`, the 5 that call a snake_case method on a proxy the -binder returned skip as well, because that needs its member resolver (python-multiplatform #17). The skip against a current checkout is `UI.ipynb`'s (§2). A binder older than #38 skips the 5 +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. @@ -221,18 +220,14 @@ 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. -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 snake_case too (`m.padding(padding_values=...)`): the resolver -answers `(kotlin_name, {python_keyword: kotlinParameter})`, built from every overload's parameter -names read with `python_multiplatform.describe_member(type, member)` (python-multiplatform #54; -receiver slot skipped, cached per type and member because it is metadata, nothing invoked). The -Kotlin spelling still works at run time -- the binder passes a keyword the map does not name -through -- so `m.padding(paddingValues=...)` is accepted; the stubs are Pythonic-only (§1, S1.2), -so a type checker flags it. On a binder without `describe_member` the resolver answers the name -only, and a method's keywords are Kotlin's. +Methods on a proxy the binder returned (`m.fill_max_width()`) and their keyword arguments +(`m.padding(padding_values=...)`) are the binder's own since python-multiplatform #131 (develop +452cc807): it serves snake_case names and keywords by this package's rule, keeps the Kotlin spelling +working, and lists Kotlin names only in `dir()`. This package no longer registers a member resolver +(issue #79). `tests/test_chain.py::TheBindersNamingRuleIsThisPackages` pins that the binder's +`python_multiplatform.python_name` and this package's agree, so the stubs and the runtime spell names +the same. The stubs are Pythonic-only (§1, S1.2), so a type checker flags a Kotlin keyword the +runtime accepts. ## 4. Naming @@ -246,8 +241,8 @@ name. Implemented for module-level names and keywords and for method names on a binder proxy (`tests/test_chain.py::Names`, 8 tests; `TheChain`) and for a method's keywords on a binder proxy -(`tests/test_chain.py::MethodKeywords`, 7 tests, and `MethodKeywordsNeedTheBindersDescription`, 2; needs -python-multiplatform's `describe_member`, #54). Disabling the keyword map fails 4 tests. +(`tests/test_chain.py::MethodKeywords`, 5 tests; served by the binder since python-multiplatform +#131, so they skip on an older one). ### S4.2 The notebook's spellings are examples, not the contract: `implemented` as a rule diff --git a/pythonx/compose/_reexport.py b/pythonx/compose/_reexport.py index a38afb7..3b1f649 100644 --- a/pythonx/compose/_reexport.py +++ b/pythonx/compose/_reexport.py @@ -23,11 +23,9 @@ defaults stay `python_multiplatform.KOTLIN_DEFAULT`. - **Value classes.** The manifest's `raw-primitive-allowed` list is handed to the binder once per installed binding layer, so `padding(16)` reaches a `Dp` parameter. -- **Methods on a proxy.** The same name rule is registered as the binder's member resolver, so - `Modifier.padding(16).fill_max_width()` reaches `fillMaxWidth` on a proxy Kotlin returned, and - `m.padding(padding_values=...)` reaches `paddingValues`: the resolver also answers a keyword map, - read from `python_multiplatform.describe_member`. The Kotlin spelling still works. The binder - caches the alias in its own registry; its proxy classes keep Kotlin names in `dir()`. +- **Methods on a proxy** are the binder's: since python-multiplatform #131 it serves + `Modifier.padding(16).fill_max_width()` and `m.padding(padding_values=...)` itself, by this + package's rule, with the Kotlin spelling still working. Nothing here takes part. - **Grouped constants.** Inside a Kotlin object served as a namespace, a type nested in it that is not one of its members groups the constants declared as exactly that type, so the notebook's @@ -116,77 +114,16 @@ def binding_layer(): _PRIMED = weakref.WeakSet() -_MEMBER_KEYWORDS: "weakref.WeakKeyDictionary" = weakref.WeakKeyDictionary() - - -def _method_keywords(kotlin_type_name: str, kotlin_member_name: str) -> dict: - """`{snake_case(p): p}` for every parameter `p` of every declaration of a proxy's member. - - Read with `python_multiplatform.describe_member`, which lists each overload's parameters without - invoking anything (the receiver slot is not among them). It is metadata, so it is kept per - (type, member), under the `describe_member` of the binding layer that answered: a new layer - starts with an empty table. A keyword whose snake_case spelling is its own is left out, and a - binder without `describe_member`, or a member it does not describe, has no map. - """ - describe_member = getattr(sys.modules.get(ROOT_MODULE), "describe_member", None) - if describe_member is None: - return {} - try: - known = _MEMBER_KEYWORDS.setdefault(describe_member, {}) - except TypeError: # something that cannot be weakly referenced: nothing to key a cache by - known = {} - key = (kotlin_type_name, kotlin_member_name) - if key not in known: - keywords: dict = {} - try: - declarations = describe_member(kotlin_type_name, kotlin_member_name) - except (TypeError, AttributeError): - declarations = () - for declaration in declarations: - for parameter in declaration.get("parameters", ()): - kotlin_parameter = parameter.get("name") - if kotlin_parameter and snake_case(kotlin_parameter) != kotlin_parameter: - keywords.setdefault(snake_case(kotlin_parameter), kotlin_parameter) - known[key] = keywords - return known[key] - - -def member_name(kotlin_type_name: str, requested: str, kotlin_member_names): - """The binder's member-resolver hook: the Kotlin member a Pythonic name means on a proxy, and - the keywords it takes. - - The binder asks for a name its proxy has no Kotlin member of - (`python_multiplatform.binding.add_member_resolver`): the rule is the module rule applied - forward to the proxy's real member names, so `fill_max_width` finds `fillMaxWidth`. It asks - again for a Kotlin member name when a call passes keywords. The answer is then - `(kotlin_name, {python_keyword: kotlinParameter})`, the parameters of every overload of that - member, so `m.padding(padding_values=...)` reaches `paddingValues`. Without a keyword to map the - answer is the name alone, which is all a binder without `describe_member` can be given. - """ - if requested in kotlin_member_names: - target = requested - else: - matches = [name for name in kotlin_member_names if python_name(name) == requested] - target = matches[0] if len(matches) == 1 else None - if target is None: - return None - keywords = _method_keywords(kotlin_type_name, target) - return (target, keywords) if keywords else target - - def _prime(binding) -> None: - """Hand this binding layer the manifest's value-class allowlist and the member rule, once. + """Hand this binding layer the manifest's value-class allowlist, once. - The member rule needs python-multiplatform's `add_member_resolver`; on a binder without it, - methods on a returned proxy stay reachable by their Kotlin names only. + Method names and keywords on a proxy are the binder's own since python-multiplatform #131 + (snake_case by this package's rule); nothing is registered for them here. """ if binding in _PRIMED: return for kotlin_type in manifest().get("value-classes", {}).get("raw-primitive-allowed", ()): binding.allow_raw_primitive(kotlin_type) - add_member_resolver = getattr(binding, "add_member_resolver", None) - if add_member_resolver is not None: - add_member_resolver(member_name) _PRIMED.add(binding) diff --git a/tests/test_chain.py b/tests/test_chain.py index a5902e5..a0a43f5 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -61,12 +61,14 @@ def describe(self, modifier): return ui.describe_modifier(modifier) def needs_member_resolver(self): - """Receiver methods by snake_case name need the binder's member-resolver hook. + """Receiver methods and their keywords by snake_case name are the binder's own. - python-multiplatform #17 adds it; until a checkout has it, these tests skip rather than pass. + python-multiplatform #131 serves them; on an older binder these tests skip rather than pass. """ - if not hasattr(self.binding, "add_member_resolver"): - self.skipTest("the binder has no add_member_resolver yet (python-multiplatform #17)") + import python_multiplatform + + if not hasattr(python_multiplatform, "python_name"): + self.skipTest("the binder does not serve snake_case names yet (python-multiplatform #131)") class TheBinderSourcesAreReadable(AdapterCase): @@ -207,19 +209,11 @@ def test_the_module_function_dispatches_with_snake_case_keywords(self): class MethodKeywords(AdapterCase): - """SPEC S4.1: a method's keywords are snake_case like a module function's. - - The member resolver answers `(kotlin_name, {python_keyword: kotlinParameter})`, built from - `python_multiplatform.describe_member(type, member)` (python-multiplatform #54). - """ + """SPEC S4.1: a method's keywords are snake_case like a module function's (the binder's, #131).""" def setUp(self): super().setUp() - import python_multiplatform - self.needs_member_resolver() - if not hasattr(python_multiplatform, "describe_member"): - self.skipTest("the binder has no describe_member(type, member) yet (python-multiplatform #54)") def values(self): import pythonx.compose.foundation.layout as layout @@ -253,62 +247,6 @@ def test_an_unknown_keyword_still_reaches_the_binders_refusal_with_its_candidate self.assertIn("Candidates", message) self.assertIn("padding__PaddingValues", message) - def test_the_map_is_metadata_and_is_asked_for_once_per_member(self): - import python_multiplatform - - original, asked = python_multiplatform.describe_member, [] - - def counting(*args): - asked.append(args) - return original(*args) - - python_multiplatform.describe_member = counting - self.addCleanup(setattr, python_multiplatform, "describe_member", original) - modifier = self.empty() - modifier.padding(padding_values=self.values()) - modifier.padding(padding_values=self.values()) - self.assertEqual([(fake_host.MODIFIER, "padding")], asked) - - def test_the_map_lists_every_overloads_parameters_and_not_the_receiver(self): - from pythonx.compose._reexport import member_name - - answer = member_name(fake_host.MODIFIER, "padding", ["padding"]) - self.assertEqual("padding", answer[0]) - self.assertEqual("paddingValues", answer[1]["padding_values"]) - self.assertNotIn("receiver", answer[1]) - self.assertNotIn("horizontal", answer[1]) # one word: Kotlin's spelling is the same - - -class MethodKeywordsNeedTheBindersDescription(AdapterCase): - """Without `describe_member` a method's keywords are Kotlin's and its names still resolve.""" - - def setUp(self): - super().setUp() - import python_multiplatform - - self.needs_member_resolver() - original = getattr(python_multiplatform, "describe_member", None) - if original is not None: - del python_multiplatform.describe_member - self.addCleanup(setattr, python_multiplatform, "describe_member", original) - - def test_the_name_resolves_and_kotlin_keywords_work(self): - import pythonx.compose.foundation.layout as layout - - modifier = self.empty().fill_max_width() - self.assertEqual("fillMaxWidth", self.describe(modifier)) - self.empty().padding(paddingValues=layout.padding_values_of(8)) - self.assertEqual("padding__PaddingValues", self.host.calls[-1]) - - def test_a_snake_case_keyword_is_the_binders_refusal(self): - import python_multiplatform - import pythonx.compose.foundation.layout as layout - - if hasattr(python_multiplatform, "python_name"): - self.skipTest("the binder translates names itself since python-multiplatform #131") - with self.assertRaises(TypeError): - self.empty().padding(padding_values=layout.padding_values_of(8)) - class TheBindersNamingRuleIsThisPackages(AdapterCase): """python-multiplatform #131 restored the binder's own snake_case names, with this package's rule. From 45d2637fd81903e1e7031bda0100bbed18cfebcb Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:00:24 +0900 Subject: [PATCH 09/16] Docs: Describe the project as "Build Compose Multiplatform UIs in Python" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The maintainer's decision (2026-10-03): "Build Compose Multiplatform UIs in Python: Python Declarative UI Framework". pyproject's description and the README's tagline say it verbatim; README_ko says the same in Korean ("파이썬으로 Compose Multiplatform UI 를 만듭니다: 파이썬 선언형 UI 프레임워크"); the guide's front-page lead opens with it and keeps its sentence about Compose's own widgets spelled the Python way, in both languages. The GitHub repository description was changed by the ecosystem lead; PyPI shows the new text from the next version. documentation corrected: pyproject.toml, README.md, README_ko.md, docs/guide/index.html. The wheel test builds; check_guide: 0 findings. Refs #82 --- README.md | 2 +- docs/guide/index.html | 2 +- docs/locale/README_ko.md | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 93fb193..e1214ec 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ English | [한국어](https://github.com/thisisthepy/pythonx-compose/blob/main/d # pythonx-compose -**Write Compose user interfaces in Python, with Compose's own widgets, spelled the Python way.** +**Build Compose Multiplatform UIs in Python: Python Declarative UI Framework** [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/LICENSE) [![Python](https://img.shields.io/badge/python-3.11%2B-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/pyproject.toml) diff --git a/docs/guide/index.html b/docs/guide/index.html index c3c0ccb..c185c15 100644 --- a/docs/guide/index.html +++ b/docs/guide/index.html @@ -33,7 +33,7 @@
pre-alpha · uv add --prerelease allow pythonx-composepre-alpha · uv add --prerelease allow pythonx-compose

Compose, in Python.Compose 를, 파이썬으로.

-

The real Compose widgets with their real parameters, restructured into a Python package, spelled the way Python spells things.진짜 Compose 위젯과 진짜 매개변수를, 파이썬 패키지로 재구성해, 파이썬이 쓰는 방식대로 씁니다.

+

Build Compose Multiplatform UIs in Python. A Python declarative UI framework: Compose's own widgets with their own parameters, spelled the way Python spells things.파이썬으로 Compose Multiplatform UI 를 만듭니다. 파이썬 선언형 UI 프레임워크로, Compose 의 위젯과 매개변수를 그대로 쓰되 파이썬다운 이름으로 씁니다.

Get started시작하기 → How it works동작 방식 diff --git a/docs/locale/README_ko.md b/docs/locale/README_ko.md index b97dc23..b79c9d8 100644 --- a/docs/locale/README_ko.md +++ b/docs/locale/README_ko.md @@ -4,7 +4,7 @@ # pythonx-compose -**Compose UI 를 파이썬으로, Compose 의 위젯 그대로, 파이썬다운 이름으로.** +**파이썬으로 Compose Multiplatform UI 를 만듭니다: 파이썬 선언형 UI 프레임워크** [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/LICENSE) [![Python](https://img.shields.io/badge/python-3.11%2B-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/pyproject.toml) diff --git a/pyproject.toml b/pyproject.toml index db57ebd..6b25fd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "pythonx-compose" version = "0.1.0a1" -description = "Pythonic Compose UI for Python on Kotlin Multiplatform, through python-multiplatform" +description = "Build Compose Multiplatform UIs in Python: Python Declarative UI Framework" readme = {file = "README.md", content-type = "text/markdown"} license = "Apache-2.0" license-files = ["LICENSE"] From bc7b9e0bf26b63e576d3a03f77d268fe273a9214 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:04:16 +0900 Subject: [PATCH 10/16] Refactor: Hand module functions back as the binder's own callables Step 3 of handing names back to the binder (#79). PythonicFunction wrapped each module function to translate snake_case keywords into Kotlin parameter names and to rename its inspect.signature. python-multiplatform #131 does both in the binder, by the same rule, so the wrapper (and _describe, which fed it) is gone: pythonx.compose.foundation.layout.padding_values_of is now the binder's callable itself. The behaviour tests for snake_case keywords and signatures on module functions stay and pass against #131. The module docstring no longer says the binder renames nothing; it says what the binder serves since #131 and what this module still does (grouping the Kotlin packages into pythonx.compose.*, names, objects, aliases, the value-class allowlist). deleted: PythonicFunction and _describe in _reexport.py. documentation corrected: _reexport.py's docstring, SPEC section 3. Suite with python-multiplatform 452cc807 (#131), main checkout and the sibling checkout: 249 passed, unchanged. Refs #79 --- docs/SPEC.md | 10 ++-- pythonx/compose/_reexport.py | 91 ++++++------------------------------ 2 files changed, 20 insertions(+), 81 deletions(-) diff --git a/docs/SPEC.md b/docs/SPEC.md index 496458d..5cc6bfb 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -209,16 +209,16 @@ Required by INTENT §2.2: `import pythonx.compose.material3` (and every other ma Every module in `pythonx-map.toml` is a file on disk whose `__init__.py` hands its name to one rule, `pythonx/compose/_reexport.py` (issue #8). A name read from it resolves, on first use, against the Kotlin package the manifest maps it to: upper-case names keep their Kotlin spelling, every other -name is reached by its snake_case spelling only, keywords are matched to the declaration's own -parameter names, `inspect.signature` reports snake_case names, and the manifest's value-class -allowlist is handed to the binder. Names the package defines itself (`runtime.Composable`) win. +name is reached by its snake_case spelling, functions are the binder's own callables (whose +snake_case keywords and `inspect.signature` are the binder's since python-multiplatform #131), and +the manifest's value-class allowlist is handed to the binder. Names the package defines itself (`runtime.Composable`) win. Evidence: `tests/test_runtime_module.py::TheModuleIsTheFileOnDisk`, `tests/test_ui_init_module.py::TheUiInitModuleIsTheFileOnDisk`, and `tests/test_chain.py` (`Names`, `Laziness`, `OverloadDispatch::test_the_module_function_dispatches_with_snake_case_keywords`, `ValueClasses::test_the_manifest_allowlist_is_what_lets_a_number_through`), against the binder's -real Python and the fake host. Disabling the name rule fails 15 tests, the keyword mapping 1, the -allowlist priming 12. +real Python and the fake host. Disabling the name rule fails 15 tests and the allowlist priming 12 +(measured before #79 moved keyword translation into the binder). Methods on a proxy the binder returned (`m.fill_max_width()`) and their keyword arguments (`m.padding(padding_values=...)`) are the binder's own since python-multiplatform #131 (develop diff --git a/pythonx/compose/_reexport.py b/pythonx/compose/_reexport.py index 3b1f649..165f1d4 100644 --- a/pythonx/compose/_reexport.py +++ b/pythonx/compose/_reexport.py @@ -1,26 +1,24 @@ """The one rule that makes a `pythonx.compose.*` module a Pythonic view of a Kotlin package. -The binder (`python-multiplatform`) serves every bound Kotlin package under its **Kotlin** name -- -`androidx.compose.foundation.layout` -- with Kotlin's own surface: declaration names, keyword -arguments by Kotlin parameter name, Kotlin defaults, overload dispatch. It renames nothing, and it -publishes what a Pythonic layer needs to rename by rule: `inspect.signature(fn)` with the Kotlin -parameter names, and `python_multiplatform.describe(fn)` with one dict per overload. +The binder (`python-multiplatform`) serves every bound Kotlin package under its **Kotlin** name +(`androidx.compose.foundation.layout`), and since its #131 it also serves the Pythonic spellings +itself: snake_case names and keyword arguments, by this module's rule (`python_name`), with the +Kotlin spelling still working and `inspect.signature` reporting the snake_case keywords. It never +renames a namespace: `pythonx` is this package. -This module is that layer. A mapped package's `__init__.py` is one line:: +This module groups those Kotlin packages into `pythonx.compose.*`. A mapped package's `__init__.py` +is one line:: __getattr__, __dir__ = reexport(__name__) and every name in it then resolves against the Kotlin package `pythonx-map.toml` maps it to: -- **Names.** A name that starts upper-case -- a type, an object, a `@Composable` -- keeps its Kotlin - spelling (`Modifier`, `Text`). Any other name is reached by its snake_case spelling only +- **Names.** A name that starts upper-case (a type, an object, a `@Composable`) keeps its Kotlin + spelling (`Modifier`, `Text`). Any other name is listed and reached by its snake_case spelling (`fillMaxWidth` -> `fill_max_width`, `toURLString` -> `to_url_string`), and an explicit overload - keeps its suffix (`padding__Dp`). The inverse is never computed: the module's real Kotlin names - are converted forward and looked up, so a name the reverse rule could not invert still resolves. -- **Keywords.** `on_click=` reaches the parameter Kotlin calls `onClick`, matched against the - declaration's own parameter names (every overload's), never derived. -- **Signatures.** A re-exported function answers `inspect.signature` with the snake_case names; - defaults stay `python_multiplatform.KOTLIN_DEFAULT`. + keeps its suffix (`padding__Dp`). The rule is applied forward to the module's real Kotlin names, + so a name the reverse rule could not invert still resolves. +- **Functions** are the binder's own callables; their keywords and signatures are the binder's. - **Value classes.** The manifest's `raw-primitive-allowed` list is handed to the binder once per installed binding layer, so `padding(16)` reaches a `Dp` parameter. - **Methods on a proxy** are the binder's: since python-multiplatform #131 it serves @@ -48,7 +46,6 @@ import importlib import importlib.util -import inspect import re import sys import types @@ -170,72 +167,14 @@ def _declared_type(kotlin, kotlin_name: str): return described.get("kind"), described.get("returns") -def _describe(fn): - root = sys.modules.get(ROOT_MODULE) - if root is None or not hasattr(root, "describe"): - return () - try: - return root.describe(fn) - except (TypeError, AttributeError): - return () - - -class PythonicFunction: - """A binder callable seen through the naming rule: snake_case keywords and signature. - - Calls go straight to the binder's callable with the keywords translated; overload selection, - defaults and value classes are the binder's. A keyword that matches no parameter is passed on - unchanged, so the binder's own refusal -- which lists the candidates -- is what the caller sees. - """ - - __slots__ = ("_kotlin", "_keywords", "__name__", "__qualname__") - - def __init__(self, kotlin_fn, name): - self._kotlin = kotlin_fn - self.__name__ = name - self.__qualname__ = name - keywords = {} - for row in _describe(kotlin_fn): - for parameter in row.get("parameters", ()): - kotlin_parameter = parameter["name"] - keywords.setdefault(snake_case(kotlin_parameter), kotlin_parameter) - self._keywords = keywords - - @property - def __kotlin__(self): - """The binder's callable this wraps.""" - return self._kotlin - - @property - def __kotlin_rows__(self): - return self._kotlin.__kotlin_rows__ - - @property - def __signature__(self): - kotlin_signature = inspect.signature(self._kotlin) - return kotlin_signature.replace(parameters=[ - parameter if parameter.kind in (parameter.VAR_POSITIONAL, parameter.VAR_KEYWORD) - else parameter.replace(name=snake_case(parameter.name)) - for parameter in kotlin_signature.parameters.values() - ]) - - def __call__(self, *args, **kwargs): - if kwargs: - kwargs = {self._keywords.get(key, key): value for key, value in kwargs.items()} - return self._kotlin(*args, **kwargs) - - def __repr__(self): - return f" {self._kotlin!r}>" - - def _pythonic(value, name): if isinstance(value, types.ModuleType): # A Kotlin object served as a sub-package. Since python-multiplatform #35 the binder lists it # and exposes it as an attribute of its parent, so the name table reaches it directly. return KotlinObject(value) - if isinstance(value, type) or not callable(value): - return value - return PythonicFunction(value, name) + # A function is the binder's own callable: since python-multiplatform #131 it takes snake_case + # keywords and reports them in `inspect.signature` itself. + return value def _resolve(kotlin, name: str): From 3ab7707cb5b75a238d6329464a8f6fbe4a7520b9 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:14:50 +0900 Subject: [PATCH 11/16] Feat: Regenerate the stubs from the binder that names things itself, and say who renames what Step 5 of handing names back to the binder (#79). python-multiplatform's stubs since #131 (run 37131041879, 452cc807) carry the binder's own snake_case aliases beside the Kotlin-named declarations: module lines such as `fill_max_width = fillMaxWidth`, an extra alias member per proxy member, and snake_case keywords. Converted as before, every alias would have become `fill_max_width = fill_max_width`. gen_stubs.py now drops the binder's aliases, because it already gives each declaration the same name by the same rule (python_name, pinned equal to the binder's by #80), and counts a name the binder declares as a callable module (PaddingValues) as defined. The committed stubs stay Pythonic-only, one name per declaration. What the newer input genuinely changes in the stubs: constructor keywords are snake_case (initial_text, container_color, ...) and Constraints, DpOffset, TextUnit, Color and friends are callable-module protocols. Documentation says who renames what: snake_case names and keyword arguments are the binder's own feature (the maintainer, verbatim, in INTENT 2.3); the binder never renames a namespace; this package groups the namespaces into pythonx.compose.*, keeps that surface snake_case only, and adds aliases, Kotlin-object grouping, the value-class allowlist, the app root and the Pythonic stubs. pythonx-compose needs python-multiplatform #131 or later for snake_case on proxies. Step 5 was drafted by a subagent in a worktree; INTENT 2.3 and section 3 are corrected here. feature added: alias handling in gen_stubs.py. test added: TheBindersOwnAliases (7) over a new-format fixture. Red: 3 of 6 failed (aliases kept, duplicate members), then all 7 on the callable module. documentation corrected: INTENT 2.3 and 3, AGENTS.md 12, SPEC 3/S1.2/S4.1/ S6.1, guide (concepts, modifiers, index, status), README and README_ko, PROJECT.md. Suite with python-multiplatform 452cc807 (worktree, new zip): 255 passed, 1 skipped; without a checkout, 147 passed, 109 skipped. Generating twice gives no diff. check_guide: 0 findings. Refs #79 --- AGENTS.md | 6 ++ PROJECT.md | 11 +- README.md | 4 +- docs/INTENT.md | 17 +-- docs/SPEC.md | 35 ++++-- docs/guide/concepts.html | 8 +- docs/guide/guide-modifiers.html | 4 +- docs/guide/index.html | 2 +- docs/guide/status.html | 2 +- docs/locale/README_ko.md | 4 +- pythonx/compose/__init__.pyi | 2 +- pythonx/compose/foundation/__init__.pyi | 2 +- .../compose/foundation/layout/__init__.pyi | 2 +- .../foundation/text/input/__init__.pyi | 4 +- pythonx/compose/layout/__init__.pyi | 23 ++-- pythonx/compose/material/__init__.pyi | 2 +- pythonx/compose/material/icons/__init__.pyi | 2 +- pythonx/compose/material3/__init__.pyi | 101 +++++++++--------- pythonx/compose/runtime/__init__.pyi | 2 +- pythonx/compose/ui/__init__.pyi | 6 +- pythonx/compose/ui/graphics/__init__.pyi | 67 ++++++++---- .../compose/ui/graphics/vector/__init__.pyi | 2 +- pythonx/compose/ui/text/__init__.pyi | 54 ++++++---- pythonx/compose/ui/unit/__init__.pyi | 101 +++++++++++------- scripts/gen_stubs.py | 54 +++++++++- .../compose/foundation/layout/__init__.pyi | 35 ++++++ .../androidx/compose/ui/__init__.pyi | 44 ++++++++ tests/test_stubs.py | 60 +++++++++++ 28 files changed, 477 insertions(+), 179 deletions(-) create mode 100644 tests/fixtures/kotlin_stubs_v5/androidx/compose/foundation/layout/__init__.pyi create mode 100644 tests/fixtures/kotlin_stubs_v5/androidx/compose/ui/__init__.pyi diff --git a/AGENTS.md b/AGENTS.md index dc54f9a..3485eeb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -182,6 +182,12 @@ worktree so a test can see it. Read it in place. - `python-multiplatform` exposes Kotlin declarations under their **Kotlin** names (`androidx.compose.material3`). It must never map `androidx` to `pythonx` or rename any Kotlin namespace. Do not propose, add, or depend on such a feature there, including in test harnesses. +- What the rule forbids is renaming a **namespace**. Turning a Kotlin member name or keyword into + `snake_case` (`fillMaxWidth` to `fill_max_width`, `onClick` to `on_click`) is the binder's own + feature since python-multiplatform #131, by the same rule as `pythonx.compose._reexport.python_name`; + depending on it is allowed, and this package keeps no member resolver or keyword wrapper of its + own. What stays here: the module grouping, the snake-case-only `pythonx` surface, aliases, objects + and grouped constants, the value-class allowlist, the app root and the Pythonic stubs. - `pythonx/` is a **real package on disk**. Its modules import the `androidx.compose.*` modules the binder exposes and restructure them Pythonically. A design in which something else synthesises `pythonx.*` (for example with `__path__ = []`) and the on-disk files cannot be imported is ruled diff --git a/PROJECT.md b/PROJECT.md index d064889..178a568 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -27,7 +27,7 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **` | 상태 | 항목 | |---|---| -| 구현 | 매핑 매니페스트 `pythonx-map.toml`, `@Composable` 항등 데코레이터, 실제 디스크 패키지 `pythonx` 와 모듈 수준 규칙 기반 재노출(snake_case 이름·키워드·시그니처, 오버로드 디스패치, 값 클래스 허용 목록), 실제 Compose 1.11.1 에서 생성한 `.pyi` 스텁과 `py.typed`(wheel 포함, mypy 로 검사), `Alignment`/`Arrangement` 상수(평면 `Alignment.End` 와 묶음 `Alignment.Horizontal.End` 모두, #9), proxy 메서드의 snake_case 키워드 인자(`m.padding(padding_values=...)`, python-multiplatform `describe_member` 필요; Kotlin 이름도 실행 시 그대로 통하고 스텁은 snake_case 만) | +| 구현 | 매핑 매니페스트 `pythonx-map.toml`, `@Composable` 항등 데코레이터, 실제 디스크 패키지 `pythonx` 와 모듈 수준 규칙 기반 재노출(snake_case 이름·키워드·시그니처, 오버로드 디스패치, 값 클래스 허용 목록), 실제 Compose 1.11.1 에서 생성한 `.pyi` 스텁과 `py.typed`(wheel 포함, mypy 로 검사), `Alignment`/`Arrangement` 상수(평면 `Alignment.End` 와 묶음 `Alignment.Horizontal.End` 모두, #9), proxy 메서드의 snake_case 이름과 키워드 인자(`m.padding(padding_values=...)`; python-multiplatform #131 이 바인더 쪽에서 제공하므로 #131 이상 필요; Kotlin 이름도 실행 시 그대로 통하고 스텁은 snake_case 만) | | 부분 | 선언형 앱 루트 `@app`·`state`·`app_root`(#11; 로직은 테스트됨, 바인더 경로는 #38 모양의 가짜 호스트로 테스트됨, 실제 Compose 증거는 E2E 모듈 #19; 숫자·문자열 상태는 python-multiplatform #69 부터 왕복됨), 배포 설정, Material 3 위젯(렌더 증거는 python-multiplatform 에만), `Icon`(import 만 됨)·`DefaultIcons`(`Icons.Default` alias, 가짜 호스트로만 테스트됨, 스텁에서는 `DefaultIcons.Add` 가 `ImageVector`; `DefaultIcons.Add` 로 쓴다; 노트북의 `DefaultIcons.Add()` 가 아님), `TextField(state=...)`·`TextFieldState`(#10; `pythonx.compose.foundation.text.input`, 가짜 호스트로 테스트됨, 노트북의 `text_state=`/`padding=` 는 `state=`/`modifier=Modifier.padding(8)`; IME 조합 증거는 python-multiplatform E2E #26) | | 계획 | `remember_saveable`·색 스킴·코루틴 스코프 | @@ -39,11 +39,11 @@ uv run --with pytest --with mypy pytest tests -q python-multiplatform develop 31c092f0+ 251 passed, 1 skipped describe_member 없는 체크아웃 같은 수, 그중 7 개 건너뜀 (python-multiplatform #54 이전) describe(module, name) 없는 체크아웃 같은 수, 그중 13 개 건너뜀 (python-multiplatform #36 이전) - 그보다 오래된 체크아웃 거기에 5 개 더 건너뜀 (member resolver 없음) + 그보다 오래된 체크아웃 거기에 5 개 더 건너뜀 (#131 이전, snake_case 이름 없음) ``` `describe_member`(python-multiplatform #54)가 없는 바인더에서는 메서드 키워드를 확인하는 7 개가(Kotlin 키워드로 되돌아가는 2 개는 실행됨), `describe(module, name)`(python-multiplatform #36)이 없는 바인더에서는 묶음 상수를 확인하는 13 개가, -member resolver(python-multiplatform `ba4c6f49`)가 없는 바인더에서는 proxy 의 snake_case 메서드를 부르는 +#131(python-multiplatform, snake_case 이름·키워드) 이전 바인더에서는 proxy 의 snake_case 메서드를 부르는 5 개가 더 건너뛴다. 두 환경 모두의 건너뜀 1 개는 worktree 에 없는 `UI.ipynb` 의 테스트다. 건너뜀은 통과가 아니다. 체크아웃 없이 통과하는 134 개 중 61 개는 타입 스텁과 wheel 을 검사하고(SPEC S1.2), 나머지 다수는 *부재*(옛 토큰·삭제된 파일이 없음)를 확인하는 것이라 기능 진척으로 세지 않는다. @@ -98,7 +98,10 @@ uv run python .github/scripts/check_guide.py # 가이드: HTML 파싱, 링크 - 배포 이름은 `pythonx-compose`. - `pythonx` 는 디스크에 존재하는 실제 패키지이고, 그 코드가 `androidx` 모듈을 가져와 재구성한다. - 바인더(`python-multiplatform`)는 Kotlin 네임스페이스를 절대 바꾸지 않는다. `androidx → pythonx` - 매핑은 이 저장소의 일이고, `pythonx-map.toml` 이 그 매니페스트다. + 매핑은 이 저장소의 일이고, `pythonx-map.toml` 이 그 매니페스트다. 이름과 키워드의 snake_case 변환은 + python-multiplatform #131 부터 바인더의 기능이다(규칙은 이 패키지의 `python_name` 과 같다). 이 저장소는 + 모듈 묶음, snake_case 만 나열하는 pythonx 표면, 별칭, 객체와 묶음 상수, 값 클래스 허용 목록, 앱 루트, + Pythonic 스텁을 맡고, 프록시의 snake_case 에는 python-multiplatform #131 이상이 필요하다(#79). - Kotlin 확장 함수는 수신 객체 프록시의 메서드로 보인다. - `@Composable` 데코레이터는 유지한다. - `.pyi` 스텁은 패키지에 포함되어 배포된다. diff --git a/README.md b/README.md index e1214ec..c6d7b81 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,9 @@ proofs are still pending. See [Status](#-status). `Alignment.Horizontal.End` is `Alignment.End`. - **`@Composable` stays.** Screens are decorated Python functions. - **A real package.** `pythonx/` is ordinary Python source that imports `androidx.compose.*` and - reshapes it. The binder never renames anything. Renaming is this package's job. + reshapes it. The binder never renames a namespace. Since python-multiplatform #131 it serves + `snake_case` names and keywords itself, by this package's rule; this package keeps the module + grouping and the `snake_case`-only surface. - **One manifest.** [`pythonx-map.toml`](https://github.com/thisisthepy/pythonx-compose/blob/main/pythonx/compose/pythonx-map.toml) says which `pythonx.compose.*` module stands for which Kotlin package. It lives inside the package and ships in the wheel. The runtime and the `.pyi` generator read the same file, so what your editor completes is what the interpreter diff --git a/docs/INTENT.md b/docs/INTENT.md index d99274c..65177f4 100644 --- a/docs/INTENT.md +++ b/docs/INTENT.md @@ -57,12 +57,17 @@ package `pythonx.compose`. modules** and **restructures them** so they can be used Pythonically. It is not a namespace that something else synthesises at run time. -### 2.3 The binder never renames; renaming is this package's job +### 2.3 The binder never renames a namespace; this package groups the namespaces `python-multiplatform` (the binder) exposes Kotlin declarations to Python **under their Kotlin -names**: `androidx.compose.material3` in Kotlin is `androidx.compose.material3` in Python. The -binder must never turn `androidx` into `pythonx` or rename any other Kotlin namespace. Mapping -`androidx.compose.*` to `pythonx.compose.*` is done **here, in real Python code**. +namespaces**: `androidx.compose.material3` in Kotlin is `androidx.compose.material3` in Python. The +binder must never turn `androidx` into `pythonx` or rename any other Kotlin namespace. Grouping +`androidx.compose.*` into `pythonx.compose.*` is done **here, in real Python code**. + +Pythonic spellings *inside* a namespace (snake_case names and keyword arguments) are the binder's own +feature. The maintainer confirmed it (2026-10-03, relayed by the ecosystem lead): "파이썬 형태로 +인자랑 바꿔주는건 원래 python-multiplatform 자체에서 지원하는 기능". python-multiplatform #131 +restored it, by this package's rule, after a narrower reading of this section had removed it. `pythonx-map.toml` in this repository is the mapping manifest: which `pythonx.compose.*` module stands for which Kotlin package, and which value classes may be written as raw numbers. @@ -101,8 +106,8 @@ types) even though the runtime resolves bindings on demand. - **Not the binder.** Embedding CPython, the FFI, upcalls and the artefact walker belong to `python-multiplatform`. This package depends on that surface; it does not reimplement it. -- **Not a renamer inside the binder.** Nothing that maps `androidx` to `pythonx` may live in - `python-multiplatform` (§2.3). +- **Not a namespace renamer inside the binder.** Nothing that maps `androidx` to `pythonx` may live in + `python-multiplatform` (§2.3). Snake_case names and keywords within a namespace are the binder's. - **Not a reimplementation of Compose.** Widgets are Compose's own; this package changes how they are named and reached from Python, not what they do. - **Not an invented API.** Parameters that do not exist in Kotlin (for example the notebook's diff --git a/docs/SPEC.md b/docs/SPEC.md index 5cc6bfb..61eeca2 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -74,8 +74,8 @@ and the name the interpreter resolves cannot drift. - **What is generated.** `pythonx/compose/**/__init__.pyi` for every module in `pythonx-map.toml`, beside its `__init__.py`, and an empty `pythonx/compose/py.typed`. The stubs are committed; regenerating them from the same input produces no diff. -- **From what.** python-multiplatform's CI artefact `kotlin-stubs` (workflow run 37103737430, - commit `a5028618`): Kotlin-named stubs for Compose 1.11.1, one `androidx/compose/.../__init__.pyi` +- **From what.** python-multiplatform's CI artefact `kotlin-stubs` (workflow run 37131041879, + commit `452cc807`, python-multiplatform #131): Kotlin-named stubs for Compose 1.11.1, one `androidx/compose/.../__init__.pyi` per Kotlin package. `uv run python scripts/gen_stubs.py ` converts them; the first line of every generated stub records the artefact, run and commit it came from. - **The rule is the runtime's** (`_reexport.py`), applied by `scripts/gen_stubs.py`: @@ -84,11 +84,21 @@ and the name the interpreter resolves cannot drift. snake_case, except the positional-only `receiver` and anonymous `__aN` slots; keyword-only markers and the binder's `@overload` sets, in the binder's order, are kept; - a stub class keeps its name. Its extension members (`Modifier.fill_max_width`, a - `ClassVar` of a callable `Protocol`) are renamed with `python_name`, the member resolver's rule, + `ClassVar` of a callable `Protocol`) are renamed with `python_name`, the binder's rule since #131, and the protocol's `__call__` parameters are snake_case like a module function's (the receiver and anonymous slots keep their names), because the runtime translates a method's keywords (S4.1); the stubs are Pythonic-only, so a method's Kotlin keyword, which still works at run time, fails a type check; + - one name per declaration. Since python-multiplatform #131 the binder's stubs name a name twice: + a module stub carries `fill_max_width = fillMaxWidth` beside the Kotlin-named def, a proxy class + an alias `ClassVar` or property per member, and its signature keywords are already `snake_case` + where it has an alias. The generator drops the alias lines and the repeated member, because + renaming the Kotlin-named declaration by the same rule yields the same name, so no Kotlin + second name and no line such as `fill_max_width = fill_max_width` reaches the stubs; + `python_name` and `snake_case` are idempotent on a `snake_case` name, so a keyword the binder + already spells that way is unchanged. A name the binder declares as a callable module + (`PaddingValues: _PaddingValues_callable_module`, whose `__call__` holds the explicit overloads + and whose members are its constants) is that declaration, with no base-name `def` beside it; - a Kotlin object served as a sub-package (`Alignment`, `Arrangement`) becomes a class of that name in its parent module's stub, its constants `ClassVar`s and its functions snake_case static methods (an explicit overload set, `spaced_by__Dp`, also gets its base name `spaced_by`). A sub-package directory holding only `__init__.pyi` would be a namespace @@ -124,11 +134,12 @@ and the name the interpreter resolves cannot drift. from `pythonx.compose.layout`; a name lent under another one is assigned its path in the source's stub, `DefaultIcons = Icons.Default`, which upstream types as `Icons.Filled`); names a package defines itself (`runtime.Composable`) are carried over. -- **How it is verified.** `tests/test_stubs.py` converts three fixtures: the fake host's +- **How it is verified.** `tests/test_stubs.py` converts five fixtures: the fake host's declarations in the old and the current upstream format (and the current format with an object in its parent stub), and a third in the format of python-multiplatform #53/#44/#38 (nested types, object functions, property setters), and a fourth in the format of #71/#68 (a nested type with two bases, icons as typed - properties), and checks the generated signatures against `inspect.signature` at run time. + properties), and a fifth in the format of #131 (alias lines, alias members, a callable module), and checks + the generated signatures against `inspect.signature` at run time. `TheCommittedStubs` regenerates from `.tmp/kotlin-stubs.zip` and requires the committed files to be identical (it skips where the artefact is absent, which includes CI). `tests/test_typing.py` runs mypy over small programs against the committed stubs: correct code passes, a misspelled @@ -229,6 +240,13 @@ working, and lists Kotlin names only in `dir()`. This package no longer register the same. The stubs are Pythonic-only (§1, S1.2), so a type checker flags a Kotlin keyword the runtime accepts. +Division of labour since #131: the binder renames names and keywords (it never renames a namespace, +INTENT §2.3). This package keeps the module grouping (`pythonx-map.toml`), the snake-case-only +`pythonx` surface (a module lists and serves `snake_case` names only, where the binder's own +`androidx.*` serves both), the aliases, the objects and their grouped constants, the value-class +allowlist, the app root, and the Pythonic stubs. pythonx-compose needs python-multiplatform at #131 or +later for `snake_case` names and keywords on a proxy; on an older binder those calls skip in the suite. + ## 4. Naming ### S4.1 Kotlin parameters, `snake_case`: `implemented` @@ -239,6 +257,11 @@ 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. +Who does it: the binder renames names and keywords since python-multiplatform #131, with this +package's rule (`pythonx.compose._reexport.python_name`, pinned equal to the binder's by +`tests/test_chain.py::TheBindersNamingRuleIsThisPackages`). This package keeps the policy that a +`pythonx` module lists and serves the `snake_case` spelling only, and the stubs name only it. + Implemented for module-level names and keywords and for method names on a binder proxy (`tests/test_chain.py::Names`, 8 tests; `TheChain`) and for a method's keywords on a binder proxy (`tests/test_chain.py::MethodKeywords`, 5 tests; served by the binder since python-multiplatform @@ -379,7 +402,7 @@ proxy class itself. 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. +A snake_case method is served by the binder (python-multiplatform #131) and leaves its class Kotlin-named in `dir()`. ### S6.2 The empty-`Modifier` seam: `partial` diff --git a/docs/guide/concepts.html b/docs/guide/concepts.html index b18981e..be1e719 100644 --- a/docs/guide/concepts.html +++ b/docs/guide/concepts.html @@ -32,14 +32,14 @@

The two layers두 계층

-

python-multiplatform, the binder, embeds CPython in a Kotlin Multiplatform app and exposes Kotlin declarations to Python under their Kotlin names: androidx.compose.material3 is androidx.compose.material3. It never renames a namespace.

python-multiplatform(바인더)는 Kotlin Multiplatform 앱에 CPython 을 임베딩하고, Kotlin 선언을 Kotlin 이름 그대로 파이썬에 노출합니다: androidx.compose.material3 는 androidx.compose.material3 입니다. 네임스페이스 이름을 절대 바꾸지 않습니다.

+

python-multiplatform, the binder, embeds CPython in a Kotlin Multiplatform app and exposes Kotlin declarations to Python under their Kotlin names: androidx.compose.material3 is androidx.compose.material3. It never renames a namespace. Since python-multiplatform #131 it does serve snake_case names and keywords on its own callables and proxies, by the rule this package wrote.

python-multiplatform(바인더)는 Kotlin Multiplatform 앱에 CPython 을 임베딩하고, Kotlin 선언을 Kotlin 이름 그대로 파이썬에 노출합니다: androidx.compose.material3 는 androidx.compose.material3 입니다. 네임스페이스 이름을 절대 바꾸지 않습니다. 다만 python-multiplatform #131 부터는 이 패키지가 정한 규칙대로 자신의 함수와 프록시에서 snake_case 이름과 키워드를 제공합니다.

pythonx-compose, this package, is real Python on disk. Its modules import those androidx.compose.* modules and restructure them into pythonx.compose.*.

pythonx-compose(이 패키지)는 디스크에 있는 실제 파이썬입니다. 이 패키지의 모듈이 androidx.compose.* 를 import 해 pythonx.compose.* 로 재구성합니다.

@Composable def Screen()your Pythonpythonx.composethis package · real .pyandroidx.compose.*Kotlin names · binderComposeJetpack / Multiplatformpythonx-map.tomlthe one mapping.pyi stubsin the wheel@Composable def Screen()your Pythonpythonx.composethis package · real .pyandroidx.compose.*Kotlin names · binderComposeJetpack / Multiplatformpythonx-map.toml→ pythonx.compose, .pyi

The naming rule이름 규칙

-

Expose the original Kotlin declarations and parameters with Pythonic names. A name that starts upper-case (a type, an object, a composable) keeps its Kotlin spelling. Every other name is reached by its snake_case spelling only; the camelCase spelling is not a second name. An explicit overload keeps its type suffix.

Kotlin 의 원래 선언과 매개변수를 파이썬다운 이름으로 노출합니다. 대문자로 시작하는 이름(타입, 객체, 컴포저블)은 Kotlin 표기를 유지합니다. 그 밖의 이름은 snake_case 표기로만 접근하며, camelCase 표기가 또 하나의 이름이 되지는 않습니다. 명시적 오버로드는 타입 접미사를 유지합니다.

+

Expose the original Kotlin declarations and parameters with Pythonic names. A name that starts upper-case (a type, an object, a composable) keeps its Kotlin spelling. Every other name is reached by its snake_case spelling only; the camelCase spelling is not a second name. An explicit overload keeps its type suffix. Turning a Kotlin name or keyword into snake_case is the binder's work since python-multiplatform #131 (its python_name is this package's rule, and a test pins that the two agree). This package decides what the pythonx surface lists: snake_case only, with no camelCase second name.

Kotlin 의 원래 선언과 매개변수를 파이썬다운 이름으로 노출합니다. 대문자로 시작하는 이름(타입, 객체, 컴포저블)은 Kotlin 표기를 유지합니다. 그 밖의 이름은 snake_case 표기로만 접근하며, camelCase 표기가 또 하나의 이름이 되지는 않습니다. 명시적 오버로드는 타입 접미사를 유지합니다. Kotlin 이름과 키워드를 snake_case 로 바꾸는 일은 python-multiplatform #131 부터 바인더가 합니다(바인더의 python_name 은 이 패키지의 규칙이며, 둘이 일치함을 테스트가 확인합니다). pythonx 표면에 무엇을 나열할지는 이 패키지가 정하며, snake_case 만 나열하고 camelCase 두 번째 이름은 두지 않습니다.

@@ -58,7 +58,7 @@

The naming rule이름 규

One re-export rule재노출 규칙 하나

-

Every module in pythonx-map.toml is a real file on disk whose __init__.py is one line. Nothing is resolved at import; a name is looked up in the mapped Kotlin package the first time it is read, and then lives in the module like any attribute. There is no per-widget wrapper.

pythonx-map.toml 의 모든 모듈은 디스크에 있는 실제 파일이며, __init__.py 는 한 줄입니다. import 할 때는 아무것도 해석하지 않고, 이름을 처음 읽을 때 대응된 Kotlin 패키지에서 찾은 뒤 보통의 속성처럼 모듈에 남습니다. 위젯별 래퍼는 없습니다.

+

Every module in pythonx-map.toml is a real file on disk whose __init__.py is one line. Nothing is resolved at import; a name is looked up in the mapped Kotlin package the first time it is read, and then lives in the module like any attribute. There is no per-widget wrapper, and no keyword wrapper: the callables are the binder's own. What this package keeps is the module grouping, the aliases, the objects and their grouped constants, the value-class allowlist and the app root. It needs python-multiplatform #131 or later for snake_case on proxies.

pythonx-map.toml 의 모든 모듈은 디스크에 있는 실제 파일이며, __init__.py 는 한 줄입니다. import 할 때는 아무것도 해석하지 않고, 이름을 처음 읽을 때 대응된 Kotlin 패키지에서 찾은 뒤 보통의 속성처럼 모듈에 남습니다. 위젯별 래퍼도, 키워드 래퍼도 없습니다. 호출 대상은 바인더 자신의 함수입니다. 이 패키지가 맡는 것은 모듈 묶음, 별칭, 객체와 묶음 상수, 값 클래스 허용 목록, 앱 루트입니다. 프록시에서 snake_case 를 쓰려면 python-multiplatform #131 이상이 필요합니다.

# pythonx/compose/material3/__init__.py
 from pythonx.compose._reexport import reexport
 
@@ -67,7 +67,7 @@ 

One re-export rule재노출

Extension functions are methods확장 함수는 메서드

-

Most of Modifier is Kotlin extension functions. In Python each one is a method on the receiver's proxy, so a chain reads as it does in Kotlin. The binder's proxy class keeps Kotlin names; the same naming rule is registered as the binder's member resolver, so the snake_case spelling reaches the Kotlin member.

Modifier 의 대부분은 Kotlin 확장 함수입니다. 파이썬에서는 각각이 수신 객체 프록시의 메서드가 되어, 체인이 Kotlin 에서처럼 읽힙니다. 바인더의 프록시 클래스는 Kotlin 이름을 유지하며, 같은 이름 규칙이 바인더의 멤버 리졸버로 등록되어 snake_case 표기가 Kotlin 멤버에 닿습니다.

+

Most of Modifier is Kotlin extension functions. In Python each one is a method on the receiver's proxy, so a chain reads as it does in Kotlin. The binder serves the snake_case spelling on its proxies since python-multiplatform #131 (this package registers nothing), and its class lists Kotlin names only in dir().

Modifier 의 대부분은 Kotlin 확장 함수입니다. 파이썬에서는 각각이 수신 객체 프록시의 메서드가 되어, 체인이 Kotlin 에서처럼 읽힙니다. python-multiplatform #131 부터 바인더가 프록시에서 snake_case 표기를 제공하며(이 패키지는 아무것도 등록하지 않습니다), 바인더의 클래스는 dir() 에만 Kotlin 이름을 나열합니다.

Modifier.padding(16).size(24).fill_max_width()

Keyword arguments to such a method are snake_case too: m.padding(padding_values=…), as the module function takes padding(m, padding_values=…). The resolver also hands the binder a keyword map read from the declaration's own parameter names. Kotlin's spelling (paddingValues=) still works at run time; the type stubs offer only the Pythonic one.

이런 메서드의 키워드 인자도 snake_case 입니다: m.padding(padding_values=…) 이며, 모듈 함수는 padding(m, padding_values=…) 를 받습니다. 리졸버는 선언의 실제 파라미터 이름에서 읽은 키워드 맵도 바인더에 넘깁니다. Kotlin 이름(paddingValues=)도 실행 시에는 그대로 통하지만, 타입 스텁은 Pythonic 이름만 제시합니다.

diff --git a/docs/guide/guide-modifiers.html b/docs/guide/guide-modifiers.html index 9c9d891..6fcf57a 100644 --- a/docs/guide/guide-modifiers.html +++ b/docs/guide/guide-modifiers.html @@ -40,7 +40,7 @@

Chaining체이닝from pythonx.compose.ui import Modifier m = Modifier.padding(16).size(24).fill_max_width()

-

Each link returns a new receiver; nothing is mutated. Method names are snake_case (fill_max_width reaches fillMaxWidth): the naming rule is registered as the binder's member resolver, while the binder's proxy class itself keeps Kotlin names in dir(). A name that is not bound raises AttributeError naming the type it looked on.

각 단계는 새 수신 객체를 돌려주며, 아무것도 변경하지 않습니다. 메서드 이름은 snake_case 입니다(fill_max_width 가 fillMaxWidth 에 닿음): 이름 규칙이 바인더의 멤버 리졸버로 등록되어 있고, 바인더의 프록시 클래스 자체는 dir() 에 Kotlin 이름을 유지합니다. 바인딩되지 않은 이름은 어느 타입에서 찾았는지 밝히는 AttributeError 를 냅니다.

+

Each link returns a new receiver; nothing is mutated. Method names are snake_case (fill_max_width reaches fillMaxWidth): the binder serves the snake_case spelling on its proxies since python-multiplatform #131, while its proxy class keeps Kotlin names in dir(). A name that is not bound raises AttributeError naming the type it looked on.

각 단계는 새 수신 객체를 돌려주며, 아무것도 변경하지 않습니다. 메서드 이름은 snake_case 입니다(fill_max_width 가 fillMaxWidth 에 닿음): python-multiplatform #131 부터 바인더가 프록시에서 snake_case 표기를 제공하고, 바인더의 프록시 클래스 자체는 dir() 에 Kotlin 이름을 유지합니다. 바인딩되지 않은 이름은 어느 타입에서 찾았는지 밝히는 AttributeError 를 냅니다.

@@ -54,7 +54,7 @@

Overloads오버로드

Keywords: method or module function키워드: 메서드와 모듈 함수

-

The member resolver maps keywords as well as names, so a method takes snake_case keywords exactly as the same extension does when called as a module function; inspect.signature shows them. Kotlin's own spelling still works at run time, the type stubs offer only the Pythonic one, and the mapping needs python-multiplatform's describe_member (on an older binder a method's keywords are Kotlin's):

멤버 리졸버는 이름뿐 아니라 키워드도 대응시키므로, 메서드는 같은 확장 함수를 모듈 함수로 부를 때와 똑같이 snake_case 키워드를 받고, inspect.signature 도 그 이름을 보여 줍니다. Kotlin 이름도 실행 시에는 그대로 통하고 타입 스텁은 Pythonic 이름만 제시하며, 이 대응에는 python-multiplatform 의 describe_member 가 필요합니다(오래된 바인더에서는 메서드의 키워드가 Kotlin 이름입니다):

+

The binder maps keywords as well as names (python-multiplatform #131), so a method takes snake_case keywords exactly as the same extension does when called as a module function; inspect.signature shows them. Kotlin's own spelling still works at run time, the type stubs offer only the Pythonic one, and the mapping needs python-multiplatform #131 or later (on an older binder a method's keywords are Kotlin's):

바인더(python-multiplatform #131)는 이름뿐 아니라 키워드도 대응시키므로, 메서드는 같은 확장 함수를 모듈 함수로 부를 때와 똑같이 snake_case 키워드를 받고, inspect.signature 도 그 이름을 보여 줍니다. Kotlin 이름도 실행 시에는 그대로 통하고 타입 스텁은 Pythonic 이름만 제시하며, 이 대응에는 python-multiplatform #131 이상이 필요합니다(오래된 바인더에서는 메서드의 키워드가 Kotlin 이름입니다):

import inspect
 import pythonx.compose.layout as layout
 
diff --git a/docs/guide/index.html b/docs/guide/index.html
index c185c15..f039ac8 100644
--- a/docs/guide/index.html
+++ b/docs/guide/index.html
@@ -68,7 +68,7 @@ 

Same widgets. Python names.

Architecture구조

Two layers, one rule about names.두 계층, 이름에 관한 규칙 하나.

@Composable def Screen()your Pythonpythonx.composethis package · real .pyandroidx.compose.*Kotlin names · binderComposeJetpack / Multiplatformpythonx-map.tomlthe one mapping.pyi stubsin the wheel@Composable def Screen()your Pythonpythonx.composethis package · real .pyandroidx.compose.*Kotlin names · binderComposeJetpack / Multiplatformpythonx-map.toml→ pythonx.compose, .pyi -
The binder exposes Kotlin under its Kotlin names and never renames. This package is real Python that imports those modules and reshapes them. One manifest says which module maps to which.바인더는 Kotlin 을 Kotlin 이름 그대로 노출하고 절대 이름을 바꾸지 않습니다. 이 패키지는 그 모듈을 import 해 재구성하는 실제 파이썬입니다. 어떤 모듈이 무엇에 대응하는지는 매니페스트 하나가 말합니다.
+
The binder exposes Kotlin under its Kotlin names, never renames a namespace, and serves snake_case names and keywords (python-multiplatform #131). This package is real Python that imports those modules and reshapes them. One manifest says which module maps to which.바인더는 Kotlin 을 Kotlin 이름 그대로 노출하고, 네임스페이스 이름은 절대 바꾸지 않으며, snake_case 이름과 키워드를 제공합니다(python-multiplatform #131). 이 패키지는 그 모듈을 import 해 재구성하는 실제 파이썬입니다. 어떤 모듈이 무엇에 대응하는지는 매니페스트 하나가 말합니다.

diff --git a/docs/guide/status.html b/docs/guide/status.html index c0bdeb1..4f8e828 100644 --- a/docs/guide/status.html +++ b/docs/guide/status.html @@ -34,7 +34,7 @@

Implemented means code in this repository and a passing test that exercises it. Tests that only assert an absence are not counted.

구현은 이 저장소의 코드와 그것을 실행하는 통과 테스트가 함께 있다는 뜻입니다. 부재만 확인하는 테스트는 세지 않습니다.

KotlinPython
- +
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 체인, 오버로드 디스패치, 숫자 Dp
implemented구현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; a method's keywords need python-multiplatform's describe_member, and Kotlin's own spelling still works at run timesnake_case 이름·키워드·시그니처: 모듈과 메서드; 메서드 키워드에는 python-multiplatform 의 describe_member 가 필요하고, 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.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, 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, 코루틴 스코프
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 체인, 오버로드 디스패치, 숫자 Dp
implemented구현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.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, 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 를 읽는 테스트입니다.

diff --git a/docs/locale/README_ko.md b/docs/locale/README_ko.md index b79c9d8..b4f1c9c 100644 --- a/docs/locale/README_ko.md +++ b/docs/locale/README_ko.md @@ -53,7 +53,9 @@ def Greeting(): 수도 있습니다: `Alignment.Horizontal.End` 는 `Alignment.End` 입니다. - **`@Composable` 은 그대로.** 화면은 데코레이터가 붙은 파이썬 함수입니다. - **실제 패키지.** `pythonx/` 는 `androidx.compose.*` 를 import 해 재구성하는 평범한 파이썬 - 소스입니다. 바인더는 아무 이름도 바꾸지 않습니다. 이름을 바꾸는 것은 이 패키지의 일입니다. + 소스입니다. 바인더는 네임스페이스 이름을 바꾸지 않습니다. python-multiplatform #131 부터는 + `snake_case` 이름과 키워드를 이 패키지의 규칙대로 바인더가 직접 제공하며, 이 패키지는 모듈 묶음과 + `snake_case` 만 나열하는 표면을 맡습니다. - **하나의 매니페스트.** [`pythonx-map.toml`](https://github.com/thisisthepy/pythonx-compose/blob/main/pythonx/compose/pythonx-map.toml) 이 어떤 `pythonx.compose.*` 모듈이 어떤 Kotlin 패키지에 대응하는지 적습니다. 이 파일은 패키지 안에 있으며 wheel 에 함께 담깁니다. 런타임과 `.pyi` 생성기가 같은 파일을 읽으므로, 편집기가 자동완성하는 이름과 인터프리터가 diff --git a/pythonx/compose/__init__.pyi b/pythonx/compose/__init__.pyi index 94ee536..311d35c 100644 --- a/pythonx/compose/__init__.pyi +++ b/pythonx/compose/__init__.pyi @@ -1 +1 @@ -# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37131041879, commit 452cc807, Compose 1.11.1). Do not edit. diff --git a/pythonx/compose/foundation/__init__.pyi b/pythonx/compose/foundation/__init__.pyi index 9ca1498..6783b3a 100644 --- a/pythonx/compose/foundation/__init__.pyi +++ b/pythonx/compose/foundation/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37131041879, commit 452cc807, Compose 1.11.1). Do not edit. import typing as _t class BorderStroke: diff --git a/pythonx/compose/foundation/layout/__init__.pyi b/pythonx/compose/foundation/layout/__init__.pyi index 092e758..422a5db 100644 --- a/pythonx/compose/foundation/layout/__init__.pyi +++ b/pythonx/compose/foundation/layout/__init__.pyi @@ -1,2 +1,2 @@ -# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37131041879, commit 452cc807, Compose 1.11.1). Do not edit. from pythonx.compose.layout import * diff --git a/pythonx/compose/foundation/text/input/__init__.pyi b/pythonx/compose/foundation/text/input/__init__.pyi index 65a70ed..95889de 100644 --- a/pythonx/compose/foundation/text/input/__init__.pyi +++ b/pythonx/compose/foundation/text/input/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37131041879, commit 452cc807, Compose 1.11.1). Do not edit. import typing as _t class _TextFieldState_clearText(_t.Protocol): @@ -30,7 +30,7 @@ class TextFieldLineLimits: class TextFieldState: """Kotlin: androidx.compose.foundation.text.input.TextFieldState""" - def __init__(self, initialText: str = ..., initialSelection: _t.Any = ...) -> None: + def __init__(self, initial_text: str = ..., initial_selection: _t.Any = ...) -> None: """Kotlin constructor: androidx.compose.foundation.text.input.TextFieldState (androidx.compose.foundation.text.input.TextFieldState(initialText: kotlin.String, initialSelection: androidx.compose.ui.text.TextRange): androidx.compose.foundation.text.input.TextFieldState)""" ... clear_text: _t.ClassVar[_TextFieldState_clearText] diff --git a/pythonx/compose/layout/__init__.pyi b/pythonx/compose/layout/__init__.pyi index b3484c5..f2c5126 100644 --- a/pythonx/compose/layout/__init__.pyi +++ b/pythonx/compose/layout/__init__.pyi @@ -1,9 +1,19 @@ -# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37131041879, commit 452cc807, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.material3 import pythonx.compose.ui import pythonx.compose.ui.unit +class _PaddingValues_callable_module(_t.Protocol): + """Kotlin: the function PaddingValues, and the members of the module PaddingValues""" + @_t.overload + def __call__(self, all: float) -> _t.Any: ... + @_t.overload + def __call__(self, horizontal: float = ..., vertical: float = ...) -> _t.Any: ... # type: ignore[overload-cannot-match] + @_t.overload + def __call__(self, start: float = ..., top: float = ..., end: float = ..., bottom: float = ...) -> _t.Any: ... # type: ignore[overload-cannot-match] + Zero: _t.Any + class _RowScope_NavigationBarItem(_t.Protocol): def __call__(self, selected: bool, on_click: _t.Callable[[], None], icon: _t.Callable[[], None], modifier: pythonx.compose.ui.Modifier = ..., enabled: bool = ..., label: _t.Callable[[], None] | None = ..., always_show_label: bool = ..., colors: pythonx.compose.material3.NavigationBarItemColors = ..., interaction_source: _t.Any | None = ...) -> None: ... @@ -154,7 +164,7 @@ class IntrinsicSize: class MutableWindowInsets: """Kotlin: androidx.compose.foundation.layout.MutableWindowInsets""" - def __init__(self, initialInsets: _t.Any = ...) -> None: + def __init__(self, initial_insets: _t.Any = ...) -> None: """Kotlin constructor: androidx.compose.foundation.layout.MutableWindowInsets (androidx.compose.foundation.layout.MutableWindowInsets(initialInsets: androidx.compose.foundation.layout.WindowInsets): androidx.compose.foundation.layout.MutableWindowInsets)""" ... @property @@ -554,6 +564,8 @@ def wrap_content_width(receiver: pythonx.compose.ui.Modifier, /, align: pythonx. """Kotlin: androidx.compose.ui.Modifier.wrapContentWidth(align: androidx.compose.ui.Alignment.Horizontal, unbounded: kotlin.Boolean): androidx.compose.ui.Modifier""" ... +PaddingValues: _PaddingValues_callable_module + @_t.overload def Box(modifier: pythonx.compose.ui.Modifier) -> None: ... @_t.overload @@ -569,13 +581,6 @@ def FlowRow(modifier: pythonx.compose.ui.Modifier = ..., horizontal_arrangement: @_t.overload def FlowRow(modifier: pythonx.compose.ui.Modifier = ..., horizontal_arrangement: Arrangement.Horizontal = ..., vertical_arrangement: Arrangement.Vertical = ..., item_vertical_alignment: pythonx.compose.ui.Alignment.Vertical = ..., max_items_in_each_row: int = ..., max_lines: int = ..., *, content: _t.Callable[[FlowRowScope], None]) -> None: ... # type: ignore[overload-cannot-match] -@_t.overload -def PaddingValues(all: float) -> _t.Any: ... -@_t.overload -def PaddingValues(horizontal: float = ..., vertical: float = ...) -> _t.Any: ... # type: ignore[overload-cannot-match] -@_t.overload -def PaddingValues(start: float = ..., top: float = ..., end: float = ..., bottom: float = ...) -> _t.Any: ... # type: ignore[overload-cannot-match] - @_t.overload def WindowInsets() -> _t.Any: ... @_t.overload diff --git a/pythonx/compose/material/__init__.pyi b/pythonx/compose/material/__init__.pyi index 94ee536..311d35c 100644 --- a/pythonx/compose/material/__init__.pyi +++ b/pythonx/compose/material/__init__.pyi @@ -1 +1 @@ -# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37131041879, commit 452cc807, Compose 1.11.1). Do not edit. diff --git a/pythonx/compose/material/icons/__init__.pyi b/pythonx/compose/material/icons/__init__.pyi index 190f8b4..8a983a3 100644 --- a/pythonx/compose/material/icons/__init__.pyi +++ b/pythonx/compose/material/icons/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37131041879, commit 452cc807, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.ui.graphics.vector diff --git a/pythonx/compose/material3/__init__.pyi b/pythonx/compose/material3/__init__.pyi index d31f2b1..eb1635d 100644 --- a/pythonx/compose/material3/__init__.pyi +++ b/pythonx/compose/material3/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37131041879, commit 452cc807, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.foundation import pythonx.compose.foundation.text.input @@ -14,6 +14,11 @@ import pythonx.compose.material.icons as _alias_0 DefaultIcons = _alias_0.Icons.Default +class _BottomAppBarState_callable_module(_t.Protocol): + """Kotlin: the function BottomAppBarState, and the members of the module BottomAppBarState""" + def __call__(self, initial_height_offset_limit: float, initial_height_offset: float, initial_content_offset: float) -> _t.Any: ... + Saver: _t.Any + class _ColorScheme_contentColorFor(_t.Protocol): def __call__(self, background_color: _t.Any) -> _t.Any: ... @@ -74,7 +79,7 @@ class BottomAppBarScrollBehavior: class BottomSheetScaffoldState: """Kotlin: androidx.compose.material3.BottomSheetScaffoldState""" - def __init__(self, bottomSheetState: SheetState, snackbarHostState: SnackbarHostState) -> None: + def __init__(self, bottom_sheet_state: SheetState, snackbar_host_state: SnackbarHostState) -> None: """Kotlin constructor: androidx.compose.material3.BottomSheetScaffoldState (androidx.compose.material3.BottomSheetScaffoldState(bottomSheetState: androidx.compose.material3.SheetState, snackbarHostState: androidx.compose.material3.SnackbarHostState): androidx.compose.material3.BottomSheetScaffoldState)""" ... @property @@ -88,7 +93,7 @@ class BottomSheetScaffoldState: class ButtonColors: """Kotlin: androidx.compose.material3.ButtonColors""" - def __init__(self, containerColor: _t.Any, contentColor: _t.Any, disabledContainerColor: _t.Any, disabledContentColor: _t.Any) -> None: + def __init__(self, container_color: _t.Any, content_color: _t.Any, disabled_container_color: _t.Any, disabled_content_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.ButtonColors (androidx.compose.material3.ButtonColors(containerColor: androidx.compose.ui.graphics.Color, contentColor: androidx.compose.ui.graphics.Color, disabledContainerColor: androidx.compose.ui.graphics.Color, disabledContentColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.ButtonColors)""" ... @property @@ -113,7 +118,7 @@ class ButtonElevation: class CardColors: """Kotlin: androidx.compose.material3.CardColors""" - def __init__(self, containerColor: _t.Any, contentColor: _t.Any, disabledContainerColor: _t.Any, disabledContentColor: _t.Any) -> None: + def __init__(self, container_color: _t.Any, content_color: _t.Any, disabled_container_color: _t.Any, disabled_content_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.CardColors (androidx.compose.material3.CardColors(containerColor: androidx.compose.ui.graphics.Color, contentColor: androidx.compose.ui.graphics.Color, disabledContainerColor: androidx.compose.ui.graphics.Color, disabledContentColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.CardColors)""" ... @property @@ -138,7 +143,7 @@ class CardElevation: class CheckboxColors: """Kotlin: androidx.compose.material3.CheckboxColors""" - def __init__(self, checkedCheckmarkColor: _t.Any, uncheckedCheckmarkColor: _t.Any, checkedBoxColor: _t.Any, uncheckedBoxColor: _t.Any, disabledCheckedBoxColor: _t.Any, disabledUncheckedBoxColor: _t.Any, disabledIndeterminateBoxColor: _t.Any, checkedBorderColor: _t.Any, uncheckedBorderColor: _t.Any, disabledBorderColor: _t.Any, disabledUncheckedBorderColor: _t.Any, disabledIndeterminateBorderColor: _t.Any) -> None: + def __init__(self, checked_checkmark_color: _t.Any, unchecked_checkmark_color: _t.Any, checked_box_color: _t.Any, unchecked_box_color: _t.Any, disabled_checked_box_color: _t.Any, disabled_unchecked_box_color: _t.Any, disabled_indeterminate_box_color: _t.Any, checked_border_color: _t.Any, unchecked_border_color: _t.Any, disabled_border_color: _t.Any, disabled_unchecked_border_color: _t.Any, disabled_indeterminate_border_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.CheckboxColors (androidx.compose.material3.CheckboxColors(checkedCheckmarkColor: androidx.compose.ui.graphics.Color, uncheckedCheckmarkColor: androidx.compose.ui.graphics.Color, checkedBoxColor: androidx.compose.ui.graphics.Color, uncheckedBoxColor: androidx.compose.ui.graphics.Color, disabledCheckedBoxColor: androidx.compose.ui.graphics.Color, disabledUncheckedBoxColor: androidx.compose.ui.graphics.Color, disabledIndeterminateBoxColor: androidx.compose.ui.graphics.Color, checkedBorderColor: androidx.compose.ui.graphics.Color, uncheckedBorderColor: androidx.compose.ui.graphics.Color, disabledBorderColor: androidx.compose.ui.graphics.Color, disabledUncheckedBorderColor: androidx.compose.ui.graphics.Color, disabledIndeterminateBorderColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.CheckboxColors)""" ... @property @@ -192,7 +197,7 @@ class CheckboxColors: class ChipColors: """Kotlin: androidx.compose.material3.ChipColors""" - def __init__(self, containerColor: _t.Any, labelColor: _t.Any, leadingIconContentColor: _t.Any, trailingIconContentColor: _t.Any, disabledContainerColor: _t.Any, disabledLabelColor: _t.Any, disabledLeadingIconContentColor: _t.Any, disabledTrailingIconContentColor: _t.Any) -> None: + def __init__(self, container_color: _t.Any, label_color: _t.Any, leading_icon_content_color: _t.Any, trailing_icon_content_color: _t.Any, disabled_container_color: _t.Any, disabled_label_color: _t.Any, disabled_leading_icon_content_color: _t.Any, disabled_trailing_icon_content_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.ChipColors (androidx.compose.material3.ChipColors(containerColor: androidx.compose.ui.graphics.Color, labelColor: androidx.compose.ui.graphics.Color, leadingIconContentColor: androidx.compose.ui.graphics.Color, trailingIconContentColor: androidx.compose.ui.graphics.Color, disabledContainerColor: androidx.compose.ui.graphics.Color, disabledLabelColor: androidx.compose.ui.graphics.Color, disabledLeadingIconContentColor: androidx.compose.ui.graphics.Color, disabledTrailingIconContentColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.ChipColors)""" ... @property @@ -230,7 +235,7 @@ class ChipColors: class ChipElevation: """Kotlin: androidx.compose.material3.ChipElevation""" - def __init__(self, elevation: float, pressedElevation: float, focusedElevation: float, hoveredElevation: float, draggedElevation: float, disabledElevation: float) -> None: + def __init__(self, elevation: float, pressed_elevation: float, focused_elevation: float, hovered_elevation: float, dragged_elevation: float, disabled_elevation: float) -> None: """Kotlin constructor: androidx.compose.material3.ChipElevation (androidx.compose.material3.ChipElevation(elevation: androidx.compose.ui.unit.Dp, pressedElevation: androidx.compose.ui.unit.Dp, focusedElevation: androidx.compose.ui.unit.Dp, hoveredElevation: androidx.compose.ui.unit.Dp, draggedElevation: androidx.compose.ui.unit.Dp, disabledElevation: androidx.compose.ui.unit.Dp): androidx.compose.material3.ChipElevation)""" ... @property @@ -261,11 +266,11 @@ class ChipElevation: class ColorScheme: """Kotlin: androidx.compose.material3.ColorScheme""" @_t.overload - def __init__(self, primary: _t.Any, onPrimary: _t.Any, primaryContainer: _t.Any, onPrimaryContainer: _t.Any, inversePrimary: _t.Any, secondary: _t.Any, onSecondary: _t.Any, secondaryContainer: _t.Any, onSecondaryContainer: _t.Any, tertiary: _t.Any, onTertiary: _t.Any, tertiaryContainer: _t.Any, onTertiaryContainer: _t.Any, background: _t.Any, onBackground: _t.Any, surface: _t.Any, onSurface: _t.Any, surfaceVariant: _t.Any, onSurfaceVariant: _t.Any, surfaceTint: _t.Any, inverseSurface: _t.Any, inverseOnSurface: _t.Any, error: _t.Any, onError: _t.Any, errorContainer: _t.Any, onErrorContainer: _t.Any, outline: _t.Any, outlineVariant: _t.Any, scrim: _t.Any) -> None: ... + def __init__(self, primary: _t.Any, on_primary: _t.Any, primary_container: _t.Any, on_primary_container: _t.Any, inverse_primary: _t.Any, secondary: _t.Any, on_secondary: _t.Any, secondary_container: _t.Any, on_secondary_container: _t.Any, tertiary: _t.Any, on_tertiary: _t.Any, tertiary_container: _t.Any, on_tertiary_container: _t.Any, background: _t.Any, on_background: _t.Any, surface: _t.Any, on_surface: _t.Any, surface_variant: _t.Any, on_surface_variant: _t.Any, surface_tint: _t.Any, inverse_surface: _t.Any, inverse_on_surface: _t.Any, error: _t.Any, on_error: _t.Any, error_container: _t.Any, on_error_container: _t.Any, outline: _t.Any, outline_variant: _t.Any, scrim: _t.Any) -> None: ... @_t.overload - def __init__(self, primary: _t.Any, onPrimary: _t.Any, primaryContainer: _t.Any, onPrimaryContainer: _t.Any, inversePrimary: _t.Any, secondary: _t.Any, onSecondary: _t.Any, secondaryContainer: _t.Any, onSecondaryContainer: _t.Any, tertiary: _t.Any, onTertiary: _t.Any, tertiaryContainer: _t.Any, onTertiaryContainer: _t.Any, background: _t.Any, onBackground: _t.Any, surface: _t.Any, onSurface: _t.Any, surfaceVariant: _t.Any, onSurfaceVariant: _t.Any, surfaceTint: _t.Any, inverseSurface: _t.Any, inverseOnSurface: _t.Any, error: _t.Any, onError: _t.Any, errorContainer: _t.Any, onErrorContainer: _t.Any, outline: _t.Any, outlineVariant: _t.Any, scrim: _t.Any, surfaceBright: _t.Any, surfaceDim: _t.Any, surfaceContainer: _t.Any, surfaceContainerHigh: _t.Any, surfaceContainerHighest: _t.Any, surfaceContainerLow: _t.Any, surfaceContainerLowest: _t.Any) -> None: ... # type: ignore[overload-cannot-match] + def __init__(self, primary: _t.Any, on_primary: _t.Any, primary_container: _t.Any, on_primary_container: _t.Any, inverse_primary: _t.Any, secondary: _t.Any, on_secondary: _t.Any, secondary_container: _t.Any, on_secondary_container: _t.Any, tertiary: _t.Any, on_tertiary: _t.Any, tertiary_container: _t.Any, on_tertiary_container: _t.Any, background: _t.Any, on_background: _t.Any, surface: _t.Any, on_surface: _t.Any, surface_variant: _t.Any, on_surface_variant: _t.Any, surface_tint: _t.Any, inverse_surface: _t.Any, inverse_on_surface: _t.Any, error: _t.Any, on_error: _t.Any, error_container: _t.Any, on_error_container: _t.Any, outline: _t.Any, outline_variant: _t.Any, scrim: _t.Any, surface_bright: _t.Any, surface_dim: _t.Any, surface_container: _t.Any, surface_container_high: _t.Any, surface_container_highest: _t.Any, surface_container_low: _t.Any, surface_container_lowest: _t.Any) -> None: ... # type: ignore[overload-cannot-match] @_t.overload - def __init__(self, primary: _t.Any, onPrimary: _t.Any, primaryContainer: _t.Any, onPrimaryContainer: _t.Any, inversePrimary: _t.Any, secondary: _t.Any, onSecondary: _t.Any, secondaryContainer: _t.Any, onSecondaryContainer: _t.Any, tertiary: _t.Any, onTertiary: _t.Any, tertiaryContainer: _t.Any, onTertiaryContainer: _t.Any, background: _t.Any, onBackground: _t.Any, surface: _t.Any, onSurface: _t.Any, surfaceVariant: _t.Any, onSurfaceVariant: _t.Any, surfaceTint: _t.Any, inverseSurface: _t.Any, inverseOnSurface: _t.Any, error: _t.Any, onError: _t.Any, errorContainer: _t.Any, onErrorContainer: _t.Any, outline: _t.Any, outlineVariant: _t.Any, scrim: _t.Any, surfaceBright: _t.Any, surfaceDim: _t.Any, surfaceContainer: _t.Any, surfaceContainerHigh: _t.Any, surfaceContainerHighest: _t.Any, surfaceContainerLow: _t.Any, surfaceContainerLowest: _t.Any, primaryFixed: _t.Any, primaryFixedDim: _t.Any, onPrimaryFixed: _t.Any, onPrimaryFixedVariant: _t.Any, secondaryFixed: _t.Any, secondaryFixedDim: _t.Any, onSecondaryFixed: _t.Any, onSecondaryFixedVariant: _t.Any, tertiaryFixed: _t.Any, tertiaryFixedDim: _t.Any, onTertiaryFixed: _t.Any, onTertiaryFixedVariant: _t.Any) -> None: ... # type: ignore[overload-cannot-match] + def __init__(self, primary: _t.Any, on_primary: _t.Any, primary_container: _t.Any, on_primary_container: _t.Any, inverse_primary: _t.Any, secondary: _t.Any, on_secondary: _t.Any, secondary_container: _t.Any, on_secondary_container: _t.Any, tertiary: _t.Any, on_tertiary: _t.Any, tertiary_container: _t.Any, on_tertiary_container: _t.Any, background: _t.Any, on_background: _t.Any, surface: _t.Any, on_surface: _t.Any, surface_variant: _t.Any, on_surface_variant: _t.Any, surface_tint: _t.Any, inverse_surface: _t.Any, inverse_on_surface: _t.Any, error: _t.Any, on_error: _t.Any, error_container: _t.Any, on_error_container: _t.Any, outline: _t.Any, outline_variant: _t.Any, scrim: _t.Any, surface_bright: _t.Any, surface_dim: _t.Any, surface_container: _t.Any, surface_container_high: _t.Any, surface_container_highest: _t.Any, surface_container_low: _t.Any, surface_container_lowest: _t.Any, primary_fixed: _t.Any, primary_fixed_dim: _t.Any, on_primary_fixed: _t.Any, on_primary_fixed_variant: _t.Any, secondary_fixed: _t.Any, secondary_fixed_dim: _t.Any, on_secondary_fixed: _t.Any, on_secondary_fixed_variant: _t.Any, tertiary_fixed: _t.Any, tertiary_fixed_dim: _t.Any, on_tertiary_fixed: _t.Any, on_tertiary_fixed_variant: _t.Any) -> None: ... # type: ignore[overload-cannot-match] content_color_for: _t.ClassVar[_ColorScheme_contentColorFor] content_color_for__ColorScheme_Color: _t.ClassVar[_ColorScheme_contentColorFor__ColorScheme_Color] surface_color_at_elevation: _t.ClassVar[_ColorScheme_surfaceColorAtElevation] @@ -464,7 +469,7 @@ class ColorScheme: class DatePickerColors: """Kotlin: androidx.compose.material3.DatePickerColors""" - def __init__(self, containerColor: _t.Any, titleContentColor: _t.Any, headlineContentColor: _t.Any, weekdayContentColor: _t.Any, subheadContentColor: _t.Any, navigationContentColor: _t.Any, yearContentColor: _t.Any, disabledYearContentColor: _t.Any, currentYearContentColor: _t.Any, selectedYearContentColor: _t.Any, disabledSelectedYearContentColor: _t.Any, selectedYearContainerColor: _t.Any, disabledSelectedYearContainerColor: _t.Any, dayContentColor: _t.Any, disabledDayContentColor: _t.Any, selectedDayContentColor: _t.Any, disabledSelectedDayContentColor: _t.Any, selectedDayContainerColor: _t.Any, disabledSelectedDayContainerColor: _t.Any, todayContentColor: _t.Any, todayDateBorderColor: _t.Any, dayInSelectionRangeContainerColor: _t.Any, dayInSelectionRangeContentColor: _t.Any, dividerColor: _t.Any, dateTextFieldColors: TextFieldColors) -> None: + def __init__(self, container_color: _t.Any, title_content_color: _t.Any, headline_content_color: _t.Any, weekday_content_color: _t.Any, subhead_content_color: _t.Any, navigation_content_color: _t.Any, year_content_color: _t.Any, disabled_year_content_color: _t.Any, current_year_content_color: _t.Any, selected_year_content_color: _t.Any, disabled_selected_year_content_color: _t.Any, selected_year_container_color: _t.Any, disabled_selected_year_container_color: _t.Any, day_content_color: _t.Any, disabled_day_content_color: _t.Any, selected_day_content_color: _t.Any, disabled_selected_day_content_color: _t.Any, selected_day_container_color: _t.Any, disabled_selected_day_container_color: _t.Any, today_content_color: _t.Any, today_date_border_color: _t.Any, day_in_selection_range_container_color: _t.Any, day_in_selection_range_content_color: _t.Any, divider_color: _t.Any, date_text_field_colors: TextFieldColors) -> None: """Kotlin constructor: androidx.compose.material3.DatePickerColors (androidx.compose.material3.DatePickerColors(containerColor: androidx.compose.ui.graphics.Color, titleContentColor: androidx.compose.ui.graphics.Color, headlineContentColor: androidx.compose.ui.graphics.Color, weekdayContentColor: androidx.compose.ui.graphics.Color, subheadContentColor: androidx.compose.ui.graphics.Color, navigationContentColor: androidx.compose.ui.graphics.Color, yearContentColor: androidx.compose.ui.graphics.Color, disabledYearContentColor: androidx.compose.ui.graphics.Color, currentYearContentColor: androidx.compose.ui.graphics.Color, selectedYearContentColor: androidx.compose.ui.graphics.Color, disabledSelectedYearContentColor: androidx.compose.ui.graphics.Color, selectedYearContainerColor: androidx.compose.ui.graphics.Color, disabledSelectedYearContainerColor: androidx.compose.ui.graphics.Color, dayContentColor: androidx.compose.ui.graphics.Color, disabledDayContentColor: androidx.compose.ui.graphics.Color, selectedDayContentColor: androidx.compose.ui.graphics.Color, disabledSelectedDayContentColor: androidx.compose.ui.graphics.Color, selectedDayContainerColor: androidx.compose.ui.graphics.Color, disabledSelectedDayContainerColor: androidx.compose.ui.graphics.Color, todayContentColor: androidx.compose.ui.graphics.Color, todayDateBorderColor: androidx.compose.ui.graphics.Color, dayInSelectionRangeContainerColor: androidx.compose.ui.graphics.Color, dayInSelectionRangeContentColor: androidx.compose.ui.graphics.Color, dividerColor: androidx.compose.ui.graphics.Color, dateTextFieldColors: androidx.compose.material3.TextFieldColors): androidx.compose.material3.DatePickerColors)""" ... @property @@ -627,7 +632,7 @@ class DateRangePickerState: class DefaultTooltipCaretShape(pythonx.compose.ui.graphics.Shape): """Kotlin: androidx.compose.material3.DefaultTooltipCaretShape""" - def __init__(self, caretSize: _t.Any = ...) -> None: + def __init__(self, caret_size: _t.Any = ...) -> None: """Kotlin constructor: androidx.compose.material3.DefaultTooltipCaretShape (androidx.compose.material3.DefaultTooltipCaretShape(caretSize: androidx.compose.ui.unit.DpSize): androidx.compose.material3.DefaultTooltipCaretShape)""" ... @property @@ -637,7 +642,7 @@ class DefaultTooltipCaretShape(pythonx.compose.ui.graphics.Shape): class DragHandleColors: """Kotlin: androidx.compose.material3.DragHandleColors""" - def __init__(self, color: _t.Any, pressedColor: _t.Any, draggedColor: _t.Any) -> None: + def __init__(self, color: _t.Any, pressed_color: _t.Any, dragged_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.DragHandleColors (androidx.compose.material3.DragHandleColors(color: androidx.compose.ui.graphics.Color, pressedColor: androidx.compose.ui.graphics.Color, draggedColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.DragHandleColors)""" ... @property @@ -655,7 +660,7 @@ class DragHandleColors: class DragHandleShapes: """Kotlin: androidx.compose.material3.DragHandleShapes""" - def __init__(self, shape: pythonx.compose.ui.graphics.Shape, pressedShape: pythonx.compose.ui.graphics.Shape, draggedShape: pythonx.compose.ui.graphics.Shape) -> None: + def __init__(self, shape: pythonx.compose.ui.graphics.Shape, pressed_shape: pythonx.compose.ui.graphics.Shape, dragged_shape: pythonx.compose.ui.graphics.Shape) -> None: """Kotlin constructor: androidx.compose.material3.DragHandleShapes (androidx.compose.material3.DragHandleShapes(shape: androidx.compose.ui.graphics.Shape, pressedShape: androidx.compose.ui.graphics.Shape, draggedShape: androidx.compose.ui.graphics.Shape): androidx.compose.material3.DragHandleShapes)""" ... @property @@ -673,7 +678,7 @@ class DragHandleShapes: class DragHandleSizes: """Kotlin: androidx.compose.material3.DragHandleSizes""" - def __init__(self, size: _t.Any, pressedSize: _t.Any, draggedSize: _t.Any) -> None: + def __init__(self, size: _t.Any, pressed_size: _t.Any, dragged_size: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.DragHandleSizes (androidx.compose.material3.DragHandleSizes(size: androidx.compose.ui.unit.DpSize, pressedSize: androidx.compose.ui.unit.DpSize, draggedSize: androidx.compose.ui.unit.DpSize): androidx.compose.material3.DragHandleSizes)""" ... @property @@ -691,7 +696,7 @@ class DragHandleSizes: class DrawerState: """Kotlin: androidx.compose.material3.DrawerState""" - def __init__(self, initialValue: DrawerValue, confirmStateChange: _t.Callable[[DrawerValue], bool] = ...) -> None: + def __init__(self, initial_value: DrawerValue, confirm_state_change: _t.Callable[[DrawerValue], bool] = ...) -> None: """Kotlin constructor: androidx.compose.material3.DrawerState (androidx.compose.material3.DrawerState(initialValue: androidx.compose.material3.DrawerValue, confirmStateChange: kotlin.Function1): androidx.compose.material3.DrawerState)""" ... @property @@ -730,7 +735,7 @@ class FloatingActionButtonElevation: class IconButtonColors: """Kotlin: androidx.compose.material3.IconButtonColors""" - def __init__(self, containerColor: _t.Any, contentColor: _t.Any, disabledContainerColor: _t.Any, disabledContentColor: _t.Any) -> None: + def __init__(self, container_color: _t.Any, content_color: _t.Any, disabled_container_color: _t.Any, disabled_content_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.IconButtonColors (androidx.compose.material3.IconButtonColors(containerColor: androidx.compose.ui.graphics.Color, contentColor: androidx.compose.ui.graphics.Color, disabledContainerColor: androidx.compose.ui.graphics.Color, disabledContentColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.IconButtonColors)""" ... @property @@ -752,7 +757,7 @@ class IconButtonColors: class IconToggleButtonColors: """Kotlin: androidx.compose.material3.IconToggleButtonColors""" - def __init__(self, containerColor: _t.Any, contentColor: _t.Any, disabledContainerColor: _t.Any, disabledContentColor: _t.Any, checkedContainerColor: _t.Any, checkedContentColor: _t.Any) -> None: + def __init__(self, container_color: _t.Any, content_color: _t.Any, disabled_container_color: _t.Any, disabled_content_color: _t.Any, checked_container_color: _t.Any, checked_content_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.IconToggleButtonColors (androidx.compose.material3.IconToggleButtonColors(containerColor: androidx.compose.ui.graphics.Color, contentColor: androidx.compose.ui.graphics.Color, disabledContainerColor: androidx.compose.ui.graphics.Color, disabledContentColor: androidx.compose.ui.graphics.Color, checkedContainerColor: androidx.compose.ui.graphics.Color, checkedContentColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.IconToggleButtonColors)""" ... @property @@ -782,7 +787,7 @@ class IconToggleButtonColors: class ListItemColors: """Kotlin: androidx.compose.material3.ListItemColors""" - def __init__(self, containerColor: _t.Any, headlineColor: _t.Any, leadingIconColor: _t.Any, overlineColor: _t.Any, supportingTextColor: _t.Any, trailingIconColor: _t.Any, disabledHeadlineColor: _t.Any, disabledLeadingIconColor: _t.Any, disabledTrailingIconColor: _t.Any) -> None: + def __init__(self, container_color: _t.Any, headline_color: _t.Any, leading_icon_color: _t.Any, overline_color: _t.Any, supporting_text_color: _t.Any, trailing_icon_color: _t.Any, disabled_headline_color: _t.Any, disabled_leading_icon_color: _t.Any, disabled_trailing_icon_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.ListItemColors (androidx.compose.material3.ListItemColors(containerColor: androidx.compose.ui.graphics.Color, headlineColor: androidx.compose.ui.graphics.Color, leadingIconColor: androidx.compose.ui.graphics.Color, overlineColor: androidx.compose.ui.graphics.Color, supportingTextColor: androidx.compose.ui.graphics.Color, trailingIconColor: androidx.compose.ui.graphics.Color, disabledHeadlineColor: androidx.compose.ui.graphics.Color, disabledLeadingIconColor: androidx.compose.ui.graphics.Color, disabledTrailingIconColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.ListItemColors)""" ... @property @@ -824,7 +829,7 @@ class ListItemColors: class MenuItemColors: """Kotlin: androidx.compose.material3.MenuItemColors""" - def __init__(self, textColor: _t.Any, leadingIconColor: _t.Any, trailingIconColor: _t.Any, disabledTextColor: _t.Any, disabledLeadingIconColor: _t.Any, disabledTrailingIconColor: _t.Any) -> None: + def __init__(self, text_color: _t.Any, leading_icon_color: _t.Any, trailing_icon_color: _t.Any, disabled_text_color: _t.Any, disabled_leading_icon_color: _t.Any, disabled_trailing_icon_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.MenuItemColors (androidx.compose.material3.MenuItemColors(textColor: androidx.compose.ui.graphics.Color, leadingIconColor: androidx.compose.ui.graphics.Color, trailingIconColor: androidx.compose.ui.graphics.Color, disabledTextColor: androidx.compose.ui.graphics.Color, disabledLeadingIconColor: androidx.compose.ui.graphics.Color, disabledTrailingIconColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.MenuItemColors)""" ... @property @@ -854,7 +859,7 @@ class MenuItemColors: class ModalBottomSheetProperties: """Kotlin: androidx.compose.material3.ModalBottomSheetProperties""" - def __init__(self, shouldDismissOnBackPress: bool = ..., shouldDismissOnClickOutside: bool = ...) -> None: + def __init__(self, should_dismiss_on_back_press: bool = ..., should_dismiss_on_click_outside: bool = ...) -> None: """Kotlin constructor: androidx.compose.material3.ModalBottomSheetProperties (androidx.compose.material3.ModalBottomSheetProperties(shouldDismissOnBackPress: kotlin.Boolean, shouldDismissOnClickOutside: kotlin.Boolean): androidx.compose.material3.ModalBottomSheetProperties)""" ... @property @@ -868,7 +873,7 @@ class ModalBottomSheetProperties: class ModalWideNavigationRailProperties: """Kotlin: androidx.compose.material3.ModalWideNavigationRailProperties""" - def __init__(self, shouldDismissOnBackPress: bool = ...) -> None: + def __init__(self, should_dismiss_on_back_press: bool = ...) -> None: """Kotlin constructor: androidx.compose.material3.ModalWideNavigationRailProperties (androidx.compose.material3.ModalWideNavigationRailProperties(shouldDismissOnBackPress: kotlin.Boolean): androidx.compose.material3.ModalWideNavigationRailProperties)""" ... @property @@ -883,7 +888,7 @@ class MultiChoiceSegmentedButtonRowScope: class NavigationBarItemColors: """Kotlin: androidx.compose.material3.NavigationBarItemColors""" - def __init__(self, selectedIconColor: _t.Any, selectedTextColor: _t.Any, selectedIndicatorColor: _t.Any, unselectedIconColor: _t.Any, unselectedTextColor: _t.Any, disabledIconColor: _t.Any, disabledTextColor: _t.Any) -> None: + def __init__(self, selected_icon_color: _t.Any, selected_text_color: _t.Any, selected_indicator_color: _t.Any, unselected_icon_color: _t.Any, unselected_text_color: _t.Any, disabled_icon_color: _t.Any, disabled_text_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.NavigationBarItemColors (androidx.compose.material3.NavigationBarItemColors(selectedIconColor: androidx.compose.ui.graphics.Color, selectedTextColor: androidx.compose.ui.graphics.Color, selectedIndicatorColor: androidx.compose.ui.graphics.Color, unselectedIconColor: androidx.compose.ui.graphics.Color, unselectedTextColor: androidx.compose.ui.graphics.Color, disabledIconColor: androidx.compose.ui.graphics.Color, disabledTextColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.NavigationBarItemColors)""" ... @property @@ -920,7 +925,7 @@ class NavigationDrawerItemColors: class NavigationItemColors: """Kotlin: androidx.compose.material3.NavigationItemColors""" - def __init__(self, selectedIconColor: _t.Any, selectedTextColor: _t.Any, selectedIndicatorColor: _t.Any, unselectedIconColor: _t.Any, unselectedTextColor: _t.Any, disabledIconColor: _t.Any, disabledTextColor: _t.Any) -> None: + def __init__(self, selected_icon_color: _t.Any, selected_text_color: _t.Any, selected_indicator_color: _t.Any, unselected_icon_color: _t.Any, unselected_text_color: _t.Any, disabled_icon_color: _t.Any, disabled_text_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.NavigationItemColors (androidx.compose.material3.NavigationItemColors(selectedIconColor: androidx.compose.ui.graphics.Color, selectedTextColor: androidx.compose.ui.graphics.Color, selectedIndicatorColor: androidx.compose.ui.graphics.Color, unselectedIconColor: androidx.compose.ui.graphics.Color, unselectedTextColor: androidx.compose.ui.graphics.Color, disabledIconColor: androidx.compose.ui.graphics.Color, disabledTextColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.NavigationItemColors)""" ... @property @@ -954,7 +959,7 @@ class NavigationItemColors: class NavigationRailItemColors: """Kotlin: androidx.compose.material3.NavigationRailItemColors""" - def __init__(self, selectedIconColor: _t.Any, selectedTextColor: _t.Any, selectedIndicatorColor: _t.Any, unselectedIconColor: _t.Any, unselectedTextColor: _t.Any, disabledIconColor: _t.Any, disabledTextColor: _t.Any) -> None: + def __init__(self, selected_icon_color: _t.Any, selected_text_color: _t.Any, selected_indicator_color: _t.Any, unselected_icon_color: _t.Any, unselected_text_color: _t.Any, disabled_icon_color: _t.Any, disabled_text_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.NavigationRailItemColors (androidx.compose.material3.NavigationRailItemColors(selectedIconColor: androidx.compose.ui.graphics.Color, selectedTextColor: androidx.compose.ui.graphics.Color, selectedIndicatorColor: androidx.compose.ui.graphics.Color, unselectedIconColor: androidx.compose.ui.graphics.Color, unselectedTextColor: androidx.compose.ui.graphics.Color, disabledIconColor: androidx.compose.ui.graphics.Color, disabledTextColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.NavigationRailItemColors)""" ... @property @@ -988,7 +993,7 @@ class NavigationRailItemColors: class RadioButtonColors: """Kotlin: androidx.compose.material3.RadioButtonColors""" - def __init__(self, selectedColor: _t.Any, unselectedColor: _t.Any, disabledSelectedColor: _t.Any, disabledUnselectedColor: _t.Any) -> None: + def __init__(self, selected_color: _t.Any, unselected_color: _t.Any, disabled_selected_color: _t.Any, disabled_unselected_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.RadioButtonColors (androidx.compose.material3.RadioButtonColors(selectedColor: androidx.compose.ui.graphics.Color, unselectedColor: androidx.compose.ui.graphics.Color, disabledSelectedColor: androidx.compose.ui.graphics.Color, disabledUnselectedColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.RadioButtonColors)""" ... @property @@ -1033,7 +1038,7 @@ class RangeSliderState: class RichTooltipColors: """Kotlin: androidx.compose.material3.RichTooltipColors""" - def __init__(self, containerColor: _t.Any, contentColor: _t.Any, titleContentColor: _t.Any, actionContentColor: _t.Any) -> None: + def __init__(self, container_color: _t.Any, content_color: _t.Any, title_content_color: _t.Any, action_content_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.RichTooltipColors (androidx.compose.material3.RichTooltipColors(containerColor: androidx.compose.ui.graphics.Color, contentColor: androidx.compose.ui.graphics.Color, titleContentColor: androidx.compose.ui.graphics.Color, actionContentColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.RichTooltipColors)""" ... @property @@ -1055,7 +1060,7 @@ class RichTooltipColors: class RippleConfiguration: """Kotlin: androidx.compose.material3.RippleConfiguration""" - def __init__(self, color: _t.Any = ..., rippleAlpha: _t.Any | None = ...) -> None: + def __init__(self, color: _t.Any = ..., ripple_alpha: _t.Any | None = ...) -> None: """Kotlin constructor: androidx.compose.material3.RippleConfiguration (androidx.compose.material3.RippleConfiguration(color: androidx.compose.ui.graphics.Color, rippleAlpha: androidx.compose.material.ripple.RippleAlpha?): androidx.compose.material3.RippleConfiguration)""" ... @property @@ -1070,9 +1075,9 @@ class RippleConfiguration: class SearchBarColors: """Kotlin: androidx.compose.material3.SearchBarColors""" @_t.overload - def __init__(self, containerColor: _t.Any, dividerColor: _t.Any) -> None: ... + def __init__(self, container_color: _t.Any, divider_color: _t.Any) -> None: ... @_t.overload - def __init__(self, containerColor: _t.Any, dividerColor: _t.Any, inputFieldColors: TextFieldColors) -> None: ... # type: ignore[overload-cannot-match] + def __init__(self, container_color: _t.Any, divider_color: _t.Any, input_field_colors: TextFieldColors) -> None: ... # type: ignore[overload-cannot-match] @property def container_color(self) -> _t.Any: """Kotlin property: androidx.compose.material3.SearchBarColors.containerColor (androidx.compose.material3.SearchBarColors.containerColor: androidx.compose.ui.graphics.Color)""" @@ -1141,7 +1146,7 @@ class SearchBarValue: class SegmentedButtonColors: """Kotlin: androidx.compose.material3.SegmentedButtonColors""" - def __init__(self, activeContainerColor: _t.Any, activeContentColor: _t.Any, activeBorderColor: _t.Any, inactiveContainerColor: _t.Any, inactiveContentColor: _t.Any, inactiveBorderColor: _t.Any, disabledActiveContainerColor: _t.Any, disabledActiveContentColor: _t.Any, disabledActiveBorderColor: _t.Any, disabledInactiveContainerColor: _t.Any, disabledInactiveContentColor: _t.Any, disabledInactiveBorderColor: _t.Any) -> None: + def __init__(self, active_container_color: _t.Any, active_content_color: _t.Any, active_border_color: _t.Any, inactive_container_color: _t.Any, inactive_content_color: _t.Any, inactive_border_color: _t.Any, disabled_active_container_color: _t.Any, disabled_active_content_color: _t.Any, disabled_active_border_color: _t.Any, disabled_inactive_container_color: _t.Any, disabled_inactive_content_color: _t.Any, disabled_inactive_border_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.SegmentedButtonColors (androidx.compose.material3.SegmentedButtonColors(activeContainerColor: androidx.compose.ui.graphics.Color, activeContentColor: androidx.compose.ui.graphics.Color, activeBorderColor: androidx.compose.ui.graphics.Color, inactiveContainerColor: androidx.compose.ui.graphics.Color, inactiveContentColor: androidx.compose.ui.graphics.Color, inactiveBorderColor: androidx.compose.ui.graphics.Color, disabledActiveContainerColor: androidx.compose.ui.graphics.Color, disabledActiveContentColor: androidx.compose.ui.graphics.Color, disabledActiveBorderColor: androidx.compose.ui.graphics.Color, disabledInactiveContainerColor: androidx.compose.ui.graphics.Color, disabledInactiveContentColor: androidx.compose.ui.graphics.Color, disabledInactiveBorderColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.SegmentedButtonColors)""" ... @property @@ -1195,13 +1200,13 @@ class SegmentedButtonColors: class SelectableChipColors: """Kotlin: androidx.compose.material3.SelectableChipColors""" - def __init__(self, containerColor: _t.Any, labelColor: _t.Any, leadingIconColor: _t.Any, trailingIconColor: _t.Any, disabledContainerColor: _t.Any, disabledLabelColor: _t.Any, disabledLeadingIconColor: _t.Any, disabledTrailingIconColor: _t.Any, selectedContainerColor: _t.Any, disabledSelectedContainerColor: _t.Any, selectedLabelColor: _t.Any, selectedLeadingIconColor: _t.Any, selectedTrailingIconColor: _t.Any) -> None: + def __init__(self, container_color: _t.Any, label_color: _t.Any, leading_icon_color: _t.Any, trailing_icon_color: _t.Any, disabled_container_color: _t.Any, disabled_label_color: _t.Any, disabled_leading_icon_color: _t.Any, disabled_trailing_icon_color: _t.Any, selected_container_color: _t.Any, disabled_selected_container_color: _t.Any, selected_label_color: _t.Any, selected_leading_icon_color: _t.Any, selected_trailing_icon_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.SelectableChipColors (androidx.compose.material3.SelectableChipColors(containerColor: androidx.compose.ui.graphics.Color, labelColor: androidx.compose.ui.graphics.Color, leadingIconColor: androidx.compose.ui.graphics.Color, trailingIconColor: androidx.compose.ui.graphics.Color, disabledContainerColor: androidx.compose.ui.graphics.Color, disabledLabelColor: androidx.compose.ui.graphics.Color, disabledLeadingIconColor: androidx.compose.ui.graphics.Color, disabledTrailingIconColor: androidx.compose.ui.graphics.Color, selectedContainerColor: androidx.compose.ui.graphics.Color, disabledSelectedContainerColor: androidx.compose.ui.graphics.Color, selectedLabelColor: androidx.compose.ui.graphics.Color, selectedLeadingIconColor: androidx.compose.ui.graphics.Color, selectedTrailingIconColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.SelectableChipColors)""" ... class SelectableChipElevation: """Kotlin: androidx.compose.material3.SelectableChipElevation""" - def __init__(self, elevation: float, pressedElevation: float, focusedElevation: float, hoveredElevation: float, draggedElevation: float, disabledElevation: float) -> None: + def __init__(self, elevation: float, pressed_elevation: float, focused_elevation: float, hovered_elevation: float, dragged_elevation: float, disabled_elevation: float) -> None: """Kotlin constructor: androidx.compose.material3.SelectableChipElevation (androidx.compose.material3.SelectableChipElevation(elevation: androidx.compose.ui.unit.Dp, pressedElevation: androidx.compose.ui.unit.Dp, focusedElevation: androidx.compose.ui.unit.Dp, hoveredElevation: androidx.compose.ui.unit.Dp, draggedElevation: androidx.compose.ui.unit.Dp, disabledElevation: androidx.compose.ui.unit.Dp): androidx.compose.material3.SelectableChipElevation)""" ... @property @@ -1234,7 +1239,7 @@ class SelectableDates: class Shapes: """Kotlin: androidx.compose.material3.Shapes""" - def __init__(self, extraSmall: _t.Any = ..., small: _t.Any = ..., medium: _t.Any = ..., large: _t.Any = ..., extraLarge: _t.Any = ...) -> None: + def __init__(self, extra_small: _t.Any = ..., small: _t.Any = ..., medium: _t.Any = ..., large: _t.Any = ..., extra_large: _t.Any = ...) -> None: """Kotlin constructor: androidx.compose.material3.Shapes (androidx.compose.material3.Shapes(extraSmall: androidx.compose.foundation.shape.CornerBasedShape, small: androidx.compose.foundation.shape.CornerBasedShape, medium: androidx.compose.foundation.shape.CornerBasedShape, large: androidx.compose.foundation.shape.CornerBasedShape, extraLarge: androidx.compose.foundation.shape.CornerBasedShape): androidx.compose.material3.Shapes)""" ... @property @@ -1260,7 +1265,7 @@ class Shapes: class SheetState: """Kotlin: androidx.compose.material3.SheetState""" - def __init__(self, skipPartiallyExpanded: bool, positionalThreshold: _t.Callable[[], float], velocityThreshold: _t.Callable[[], float], initialValue: SheetValue = ..., confirmValueChange: _t.Callable[[SheetValue], bool] = ..., skipHiddenState: bool = ...) -> None: + def __init__(self, skip_partially_expanded: bool, positional_threshold: _t.Callable[[], float], velocity_threshold: _t.Callable[[], float], initial_value: SheetValue = ..., confirm_value_change: _t.Callable[[SheetValue], bool] = ..., skip_hidden_state: bool = ...) -> None: """Kotlin constructor: androidx.compose.material3.SheetState (androidx.compose.material3.SheetState(skipPartiallyExpanded: kotlin.Boolean, positionalThreshold: kotlin.Function0, velocityThreshold: kotlin.Function0, initialValue: androidx.compose.material3.SheetValue, confirmValueChange: kotlin.Function1, skipHiddenState: kotlin.Boolean): androidx.compose.material3.SheetState)""" ... @property @@ -1298,7 +1303,7 @@ class SingleChoiceSegmentedButtonRowScope: class SliderColors: """Kotlin: androidx.compose.material3.SliderColors""" - def __init__(self, thumbColor: _t.Any, activeTrackColor: _t.Any, activeTickColor: _t.Any, inactiveTrackColor: _t.Any, inactiveTickColor: _t.Any, disabledThumbColor: _t.Any, disabledActiveTrackColor: _t.Any, disabledActiveTickColor: _t.Any, disabledInactiveTrackColor: _t.Any, disabledInactiveTickColor: _t.Any) -> None: + def __init__(self, thumb_color: _t.Any, active_track_color: _t.Any, active_tick_color: _t.Any, inactive_track_color: _t.Any, inactive_tick_color: _t.Any, disabled_thumb_color: _t.Any, disabled_active_track_color: _t.Any, disabled_active_tick_color: _t.Any, disabled_inactive_track_color: _t.Any, disabled_inactive_tick_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.SliderColors (androidx.compose.material3.SliderColors(thumbColor: androidx.compose.ui.graphics.Color, activeTrackColor: androidx.compose.ui.graphics.Color, activeTickColor: androidx.compose.ui.graphics.Color, inactiveTrackColor: androidx.compose.ui.graphics.Color, inactiveTickColor: androidx.compose.ui.graphics.Color, disabledThumbColor: androidx.compose.ui.graphics.Color, disabledActiveTrackColor: androidx.compose.ui.graphics.Color, disabledActiveTickColor: androidx.compose.ui.graphics.Color, disabledInactiveTrackColor: androidx.compose.ui.graphics.Color, disabledInactiveTickColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.SliderColors)""" ... @property @@ -1415,9 +1420,9 @@ class SnackbarVisuals: class SwipeToDismissBoxState: """Kotlin: androidx.compose.material3.SwipeToDismissBoxState""" @_t.overload - def __init__(self, initialValue: SwipeToDismissBoxValue, density: _t.Any, confirmValueChange: _t.Callable[[SwipeToDismissBoxValue], bool] = ..., *, positionalThreshold: _t.Callable[[float], float]) -> None: ... + def __init__(self, initial_value: SwipeToDismissBoxValue, density: _t.Any, confirm_value_change: _t.Callable[[SwipeToDismissBoxValue], bool] = ..., *, positional_threshold: _t.Callable[[float], float]) -> None: ... @_t.overload - def __init__(self, initialValue: SwipeToDismissBoxValue, positionalThreshold: _t.Callable[[float], float]) -> None: ... # type: ignore[overload-cannot-match] + def __init__(self, initial_value: SwipeToDismissBoxValue, positional_threshold: _t.Callable[[float], float]) -> None: ... # type: ignore[overload-cannot-match] @property def current_value(self) -> SwipeToDismissBoxValue: """Kotlin property: androidx.compose.material3.SwipeToDismissBoxState.currentValue (androidx.compose.material3.SwipeToDismissBoxState.currentValue: androidx.compose.material3.SwipeToDismissBoxValue)""" @@ -1444,7 +1449,7 @@ class SwipeToDismissBoxValue: class SwitchColors: """Kotlin: androidx.compose.material3.SwitchColors""" - def __init__(self, checkedThumbColor: _t.Any, checkedTrackColor: _t.Any, checkedBorderColor: _t.Any, checkedIconColor: _t.Any, uncheckedThumbColor: _t.Any, uncheckedTrackColor: _t.Any, uncheckedBorderColor: _t.Any, uncheckedIconColor: _t.Any, disabledCheckedThumbColor: _t.Any, disabledCheckedTrackColor: _t.Any, disabledCheckedBorderColor: _t.Any, disabledCheckedIconColor: _t.Any, disabledUncheckedThumbColor: _t.Any, disabledUncheckedTrackColor: _t.Any, disabledUncheckedBorderColor: _t.Any, disabledUncheckedIconColor: _t.Any) -> None: + def __init__(self, checked_thumb_color: _t.Any, checked_track_color: _t.Any, checked_border_color: _t.Any, checked_icon_color: _t.Any, unchecked_thumb_color: _t.Any, unchecked_track_color: _t.Any, unchecked_border_color: _t.Any, unchecked_icon_color: _t.Any, disabled_checked_thumb_color: _t.Any, disabled_checked_track_color: _t.Any, disabled_checked_border_color: _t.Any, disabled_checked_icon_color: _t.Any, disabled_unchecked_thumb_color: _t.Any, disabled_unchecked_track_color: _t.Any, disabled_unchecked_border_color: _t.Any, disabled_unchecked_icon_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.SwitchColors (androidx.compose.material3.SwitchColors(checkedThumbColor: androidx.compose.ui.graphics.Color, checkedTrackColor: androidx.compose.ui.graphics.Color, checkedBorderColor: androidx.compose.ui.graphics.Color, checkedIconColor: androidx.compose.ui.graphics.Color, uncheckedThumbColor: androidx.compose.ui.graphics.Color, uncheckedTrackColor: androidx.compose.ui.graphics.Color, uncheckedBorderColor: androidx.compose.ui.graphics.Color, uncheckedIconColor: androidx.compose.ui.graphics.Color, disabledCheckedThumbColor: androidx.compose.ui.graphics.Color, disabledCheckedTrackColor: androidx.compose.ui.graphics.Color, disabledCheckedBorderColor: androidx.compose.ui.graphics.Color, disabledCheckedIconColor: androidx.compose.ui.graphics.Color, disabledUncheckedThumbColor: androidx.compose.ui.graphics.Color, disabledUncheckedTrackColor: androidx.compose.ui.graphics.Color, disabledUncheckedBorderColor: androidx.compose.ui.graphics.Color, disabledUncheckedIconColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.SwitchColors)""" ... @property @@ -1536,7 +1541,7 @@ class TabPosition: class TextFieldColors: """Kotlin: androidx.compose.material3.TextFieldColors""" - def __init__(self, focusedTextColor: _t.Any, unfocusedTextColor: _t.Any, disabledTextColor: _t.Any, errorTextColor: _t.Any, focusedContainerColor: _t.Any, unfocusedContainerColor: _t.Any, disabledContainerColor: _t.Any, errorContainerColor: _t.Any, cursorColor: _t.Any, errorCursorColor: _t.Any, textSelectionColors: _t.Any, focusedIndicatorColor: _t.Any, unfocusedIndicatorColor: _t.Any, disabledIndicatorColor: _t.Any, errorIndicatorColor: _t.Any, focusedLeadingIconColor: _t.Any, unfocusedLeadingIconColor: _t.Any, disabledLeadingIconColor: _t.Any, errorLeadingIconColor: _t.Any, focusedTrailingIconColor: _t.Any, unfocusedTrailingIconColor: _t.Any, disabledTrailingIconColor: _t.Any, errorTrailingIconColor: _t.Any, focusedLabelColor: _t.Any, unfocusedLabelColor: _t.Any, disabledLabelColor: _t.Any, errorLabelColor: _t.Any, focusedPlaceholderColor: _t.Any, unfocusedPlaceholderColor: _t.Any, disabledPlaceholderColor: _t.Any, errorPlaceholderColor: _t.Any, focusedSupportingTextColor: _t.Any, unfocusedSupportingTextColor: _t.Any, disabledSupportingTextColor: _t.Any, errorSupportingTextColor: _t.Any, focusedPrefixColor: _t.Any, unfocusedPrefixColor: _t.Any, disabledPrefixColor: _t.Any, errorPrefixColor: _t.Any, focusedSuffixColor: _t.Any, unfocusedSuffixColor: _t.Any, disabledSuffixColor: _t.Any, errorSuffixColor: _t.Any) -> None: + def __init__(self, focused_text_color: _t.Any, unfocused_text_color: _t.Any, disabled_text_color: _t.Any, error_text_color: _t.Any, focused_container_color: _t.Any, unfocused_container_color: _t.Any, disabled_container_color: _t.Any, error_container_color: _t.Any, cursor_color: _t.Any, error_cursor_color: _t.Any, text_selection_colors: _t.Any, focused_indicator_color: _t.Any, unfocused_indicator_color: _t.Any, disabled_indicator_color: _t.Any, error_indicator_color: _t.Any, focused_leading_icon_color: _t.Any, unfocused_leading_icon_color: _t.Any, disabled_leading_icon_color: _t.Any, error_leading_icon_color: _t.Any, focused_trailing_icon_color: _t.Any, unfocused_trailing_icon_color: _t.Any, disabled_trailing_icon_color: _t.Any, error_trailing_icon_color: _t.Any, focused_label_color: _t.Any, unfocused_label_color: _t.Any, disabled_label_color: _t.Any, error_label_color: _t.Any, focused_placeholder_color: _t.Any, unfocused_placeholder_color: _t.Any, disabled_placeholder_color: _t.Any, error_placeholder_color: _t.Any, focused_supporting_text_color: _t.Any, unfocused_supporting_text_color: _t.Any, disabled_supporting_text_color: _t.Any, error_supporting_text_color: _t.Any, focused_prefix_color: _t.Any, unfocused_prefix_color: _t.Any, disabled_prefix_color: _t.Any, error_prefix_color: _t.Any, focused_suffix_color: _t.Any, unfocused_suffix_color: _t.Any, disabled_suffix_color: _t.Any, error_suffix_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.TextFieldColors (androidx.compose.material3.TextFieldColors(focusedTextColor: androidx.compose.ui.graphics.Color, unfocusedTextColor: androidx.compose.ui.graphics.Color, disabledTextColor: androidx.compose.ui.graphics.Color, errorTextColor: androidx.compose.ui.graphics.Color, focusedContainerColor: androidx.compose.ui.graphics.Color, unfocusedContainerColor: androidx.compose.ui.graphics.Color, disabledContainerColor: androidx.compose.ui.graphics.Color, errorContainerColor: androidx.compose.ui.graphics.Color, cursorColor: androidx.compose.ui.graphics.Color, errorCursorColor: androidx.compose.ui.graphics.Color, textSelectionColors: androidx.compose.foundation.text.selection.TextSelectionColors, focusedIndicatorColor: androidx.compose.ui.graphics.Color, unfocusedIndicatorColor: androidx.compose.ui.graphics.Color, disabledIndicatorColor: androidx.compose.ui.graphics.Color, errorIndicatorColor: androidx.compose.ui.graphics.Color, focusedLeadingIconColor: androidx.compose.ui.graphics.Color, unfocusedLeadingIconColor: androidx.compose.ui.graphics.Color, disabledLeadingIconColor: androidx.compose.ui.graphics.Color, errorLeadingIconColor: androidx.compose.ui.graphics.Color, focusedTrailingIconColor: androidx.compose.ui.graphics.Color, unfocusedTrailingIconColor: androidx.compose.ui.graphics.Color, disabledTrailingIconColor: androidx.compose.ui.graphics.Color, errorTrailingIconColor: androidx.compose.ui.graphics.Color, focusedLabelColor: androidx.compose.ui.graphics.Color, unfocusedLabelColor: androidx.compose.ui.graphics.Color, disabledLabelColor: androidx.compose.ui.graphics.Color, errorLabelColor: androidx.compose.ui.graphics.Color, focusedPlaceholderColor: androidx.compose.ui.graphics.Color, unfocusedPlaceholderColor: androidx.compose.ui.graphics.Color, disabledPlaceholderColor: androidx.compose.ui.graphics.Color, errorPlaceholderColor: androidx.compose.ui.graphics.Color, focusedSupportingTextColor: androidx.compose.ui.graphics.Color, unfocusedSupportingTextColor: androidx.compose.ui.graphics.Color, disabledSupportingTextColor: androidx.compose.ui.graphics.Color, errorSupportingTextColor: androidx.compose.ui.graphics.Color, focusedPrefixColor: androidx.compose.ui.graphics.Color, unfocusedPrefixColor: androidx.compose.ui.graphics.Color, disabledPrefixColor: androidx.compose.ui.graphics.Color, errorPrefixColor: androidx.compose.ui.graphics.Color, focusedSuffixColor: androidx.compose.ui.graphics.Color, unfocusedSuffixColor: androidx.compose.ui.graphics.Color, disabledSuffixColor: androidx.compose.ui.graphics.Color, errorSuffixColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.TextFieldColors)""" ... @property @@ -1724,7 +1729,7 @@ class TextFieldLabelScope: class TimePickerColors: """Kotlin: androidx.compose.material3.TimePickerColors""" - def __init__(self, clockDialColor: _t.Any, selectorColor: _t.Any, containerColor: _t.Any, periodSelectorBorderColor: _t.Any, clockDialSelectedContentColor: _t.Any, clockDialUnselectedContentColor: _t.Any, periodSelectorSelectedContainerColor: _t.Any, periodSelectorUnselectedContainerColor: _t.Any, periodSelectorSelectedContentColor: _t.Any, periodSelectorUnselectedContentColor: _t.Any, timeSelectorSelectedContainerColor: _t.Any, timeSelectorUnselectedContainerColor: _t.Any, timeSelectorSelectedContentColor: _t.Any, timeSelectorUnselectedContentColor: _t.Any) -> None: + def __init__(self, clock_dial_color: _t.Any, selector_color: _t.Any, container_color: _t.Any, period_selector_border_color: _t.Any, clock_dial_selected_content_color: _t.Any, clock_dial_unselected_content_color: _t.Any, period_selector_selected_container_color: _t.Any, period_selector_unselected_container_color: _t.Any, period_selector_selected_content_color: _t.Any, period_selector_unselected_content_color: _t.Any, time_selector_selected_container_color: _t.Any, time_selector_unselected_container_color: _t.Any, time_selector_selected_content_color: _t.Any, time_selector_unselected_content_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.TimePickerColors (androidx.compose.material3.TimePickerColors(clockDialColor: androidx.compose.ui.graphics.Color, selectorColor: androidx.compose.ui.graphics.Color, containerColor: androidx.compose.ui.graphics.Color, periodSelectorBorderColor: androidx.compose.ui.graphics.Color, clockDialSelectedContentColor: androidx.compose.ui.graphics.Color, clockDialUnselectedContentColor: androidx.compose.ui.graphics.Color, periodSelectorSelectedContainerColor: androidx.compose.ui.graphics.Color, periodSelectorUnselectedContainerColor: androidx.compose.ui.graphics.Color, periodSelectorSelectedContentColor: androidx.compose.ui.graphics.Color, periodSelectorUnselectedContentColor: androidx.compose.ui.graphics.Color, timeSelectorSelectedContainerColor: androidx.compose.ui.graphics.Color, timeSelectorUnselectedContainerColor: androidx.compose.ui.graphics.Color, timeSelectorSelectedContentColor: androidx.compose.ui.graphics.Color, timeSelectorUnselectedContentColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.TimePickerColors)""" ... @property @@ -1792,9 +1797,9 @@ class TooltipScope: class TopAppBarColors: """Kotlin: androidx.compose.material3.TopAppBarColors""" @_t.overload - def __init__(self, containerColor: _t.Any, scrolledContainerColor: _t.Any, navigationIconContentColor: _t.Any, titleContentColor: _t.Any, actionIconContentColor: _t.Any) -> None: ... + def __init__(self, container_color: _t.Any, scrolled_container_color: _t.Any, navigation_icon_content_color: _t.Any, title_content_color: _t.Any, action_icon_content_color: _t.Any) -> None: ... @_t.overload - def __init__(self, containerColor: _t.Any, scrolledContainerColor: _t.Any, navigationIconContentColor: _t.Any, titleContentColor: _t.Any, actionIconContentColor: _t.Any, subtitleContentColor: _t.Any) -> None: ... # type: ignore[overload-cannot-match] + def __init__(self, container_color: _t.Any, scrolled_container_color: _t.Any, navigation_icon_content_color: _t.Any, title_content_color: _t.Any, action_icon_content_color: _t.Any, subtitle_content_color: _t.Any) -> None: ... # type: ignore[overload-cannot-match] @property def action_icon_content_color(self) -> _t.Any: """Kotlin property: androidx.compose.material3.TopAppBarColors.actionIconContentColor (androidx.compose.material3.TopAppBarColors.actionIconContentColor: androidx.compose.ui.graphics.Color)""" @@ -1837,7 +1842,7 @@ class TopAppBarScrollBehavior: class Typography: """Kotlin: androidx.compose.material3.Typography""" - def __init__(self, displayLarge: _t.Any, displayMedium: _t.Any, displaySmall: _t.Any, headlineLarge: _t.Any, headlineMedium: _t.Any, headlineSmall: _t.Any, titleLarge: _t.Any, titleMedium: _t.Any, titleSmall: _t.Any, bodyLarge: _t.Any, bodyMedium: _t.Any, bodySmall: _t.Any, labelLarge: _t.Any, labelMedium: _t.Any, labelSmall: _t.Any) -> None: + def __init__(self, display_large: _t.Any, display_medium: _t.Any, display_small: _t.Any, headline_large: _t.Any, headline_medium: _t.Any, headline_small: _t.Any, title_large: _t.Any, title_medium: _t.Any, title_small: _t.Any, body_large: _t.Any, body_medium: _t.Any, body_small: _t.Any, label_large: _t.Any, label_medium: _t.Any, label_small: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.Typography (androidx.compose.material3.Typography(displayLarge: androidx.compose.ui.text.TextStyle, displayMedium: androidx.compose.ui.text.TextStyle, displaySmall: androidx.compose.ui.text.TextStyle, headlineLarge: androidx.compose.ui.text.TextStyle, headlineMedium: androidx.compose.ui.text.TextStyle, headlineSmall: androidx.compose.ui.text.TextStyle, titleLarge: androidx.compose.ui.text.TextStyle, titleMedium: androidx.compose.ui.text.TextStyle, titleSmall: androidx.compose.ui.text.TextStyle, bodyLarge: androidx.compose.ui.text.TextStyle, bodyMedium: androidx.compose.ui.text.TextStyle, bodySmall: androidx.compose.ui.text.TextStyle, labelLarge: androidx.compose.ui.text.TextStyle, labelMedium: androidx.compose.ui.text.TextStyle, labelSmall: androidx.compose.ui.text.TextStyle): androidx.compose.material3.Typography)""" ... @property @@ -1903,7 +1908,7 @@ class Typography: class WideNavigationRailColors: """Kotlin: androidx.compose.material3.WideNavigationRailColors""" - def __init__(self, containerColor: _t.Any, contentColor: _t.Any, modalContainerColor: _t.Any, modalScrimColor: _t.Any, modalContentColor: _t.Any) -> None: + def __init__(self, container_color: _t.Any, content_color: _t.Any, modal_container_color: _t.Any, modal_scrim_color: _t.Any, modal_content_color: _t.Any) -> None: """Kotlin constructor: androidx.compose.material3.WideNavigationRailColors (androidx.compose.material3.WideNavigationRailColors(containerColor: androidx.compose.ui.graphics.Color, contentColor: androidx.compose.ui.graphics.Color, modalContainerColor: androidx.compose.ui.graphics.Color, modalScrimColor: androidx.compose.ui.graphics.Color, modalContentColor: androidx.compose.ui.graphics.Color): androidx.compose.material3.WideNavigationRailColors)""" ... @property @@ -1977,10 +1982,6 @@ def BasicAlertDialog(on_dismiss_request: _t.Callable[[], None], modifier: python """Kotlin: androidx.compose.material3.BasicAlertDialog(onDismissRequest: kotlin.Function0, modifier: androidx.compose.ui.Modifier, properties: androidx.compose.ui.window.DialogProperties, content: kotlin.Function0): kotlin.Unit""" ... -def BottomAppBarState(initial_height_offset_limit: float, initial_height_offset: float, initial_content_offset: float) -> _t.Any: - """Kotlin: androidx.compose.material3.BottomAppBarState(initialHeightOffsetLimit: kotlin.Float, initialHeightOffset: kotlin.Float, initialContentOffset: kotlin.Float): androidx.compose.material3.BottomAppBarState""" - ... - def BottomAppBar__Modifier_Color_Color_Dp_PaddingValues_WindowInsets_BottomAppBarScrollBehavior_Unit(modifier: pythonx.compose.ui.Modifier = ..., container_color: _t.Any = ..., content_color: _t.Any = ..., tonal_elevation: float = ..., content_padding: _t.Any = ..., window_insets: _t.Any = ..., scroll_behavior: BottomAppBarScrollBehavior | None = ..., *, content: _t.Callable[[pythonx.compose.layout.RowScope], None]) -> None: """Kotlin: androidx.compose.material3.BottomAppBar(modifier: androidx.compose.ui.Modifier, containerColor: androidx.compose.ui.graphics.Color, contentColor: androidx.compose.ui.graphics.Color, tonalElevation: androidx.compose.ui.unit.Dp, contentPadding: androidx.compose.foundation.layout.PaddingValues, windowInsets: androidx.compose.foundation.layout.WindowInsets, scrollBehavior: androidx.compose.material3.BottomAppBarScrollBehavior?, content: kotlin.Function1): kotlin.Unit""" ... @@ -2645,6 +2646,8 @@ def surface_color_at_elevation(receiver: ColorScheme, /, elevation: float) -> _t """Kotlin: androidx.compose.material3.ColorScheme.surfaceColorAtElevation(elevation: androidx.compose.ui.unit.Dp): androidx.compose.ui.graphics.Color""" ... +BottomAppBarState: _BottomAppBarState_callable_module + def ColorScheme__Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color_Color(primary: _t.Any, on_primary: _t.Any, primary_container: _t.Any, on_primary_container: _t.Any, inverse_primary: _t.Any, secondary: _t.Any, on_secondary: _t.Any, secondary_container: _t.Any, on_secondary_container: _t.Any, tertiary: _t.Any, on_tertiary: _t.Any, tertiary_container: _t.Any, on_tertiary_container: _t.Any, background: _t.Any, on_background: _t.Any, surface: _t.Any, on_surface: _t.Any, surface_variant: _t.Any, on_surface_variant: _t.Any, surface_tint: _t.Any, inverse_surface: _t.Any, inverse_on_surface: _t.Any, error: _t.Any, on_error: _t.Any, error_container: _t.Any, on_error_container: _t.Any, outline: _t.Any, outline_variant: _t.Any, scrim: _t.Any) -> ColorScheme: """Kotlin: androidx.compose.material3.ColorScheme(primary: androidx.compose.ui.graphics.Color, onPrimary: androidx.compose.ui.graphics.Color, primaryContainer: androidx.compose.ui.graphics.Color, onPrimaryContainer: androidx.compose.ui.graphics.Color, inversePrimary: androidx.compose.ui.graphics.Color, secondary: androidx.compose.ui.graphics.Color, onSecondary: androidx.compose.ui.graphics.Color, secondaryContainer: androidx.compose.ui.graphics.Color, onSecondaryContainer: androidx.compose.ui.graphics.Color, tertiary: androidx.compose.ui.graphics.Color, onTertiary: androidx.compose.ui.graphics.Color, tertiaryContainer: androidx.compose.ui.graphics.Color, onTertiaryContainer: androidx.compose.ui.graphics.Color, background: androidx.compose.ui.graphics.Color, onBackground: androidx.compose.ui.graphics.Color, surface: androidx.compose.ui.graphics.Color, onSurface: androidx.compose.ui.graphics.Color, surfaceVariant: androidx.compose.ui.graphics.Color, onSurfaceVariant: androidx.compose.ui.graphics.Color, surfaceTint: androidx.compose.ui.graphics.Color, inverseSurface: androidx.compose.ui.graphics.Color, inverseOnSurface: androidx.compose.ui.graphics.Color, error: androidx.compose.ui.graphics.Color, onError: androidx.compose.ui.graphics.Color, errorContainer: androidx.compose.ui.graphics.Color, onErrorContainer: androidx.compose.ui.graphics.Color, outline: androidx.compose.ui.graphics.Color, outlineVariant: androidx.compose.ui.graphics.Color, scrim: androidx.compose.ui.graphics.Color): androidx.compose.material3.ColorScheme""" ... diff --git a/pythonx/compose/runtime/__init__.pyi b/pythonx/compose/runtime/__init__.pyi index 70233c9..45b26b5 100644 --- a/pythonx/compose/runtime/__init__.pyi +++ b/pythonx/compose/runtime/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37131041879, commit 452cc807, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.ui diff --git a/pythonx/compose/ui/__init__.pyi b/pythonx/compose/ui/__init__.pyi index c3d1688..54e6c12 100644 --- a/pythonx/compose/ui/__init__.pyi +++ b/pythonx/compose/ui/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37131041879, commit 452cc807, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.layout import pythonx.compose.runtime @@ -545,7 +545,7 @@ class _Modifier_zIndex(_t.Protocol): class BiasAbsoluteAlignment: """Kotlin: androidx.compose.ui.BiasAbsoluteAlignment""" - def __init__(self, horizontalBias: float, verticalBias: float) -> None: + def __init__(self, horizontal_bias: float, vertical_bias: float) -> None: """Kotlin constructor: androidx.compose.ui.BiasAbsoluteAlignment (androidx.compose.ui.BiasAbsoluteAlignment(horizontalBias: kotlin.Float, verticalBias: kotlin.Float): androidx.compose.ui.BiasAbsoluteAlignment)""" ... @property @@ -559,7 +559,7 @@ class BiasAbsoluteAlignment: class BiasAlignment: """Kotlin: androidx.compose.ui.BiasAlignment""" - def __init__(self, horizontalBias: float, verticalBias: float) -> None: + def __init__(self, horizontal_bias: float, vertical_bias: float) -> None: """Kotlin constructor: androidx.compose.ui.BiasAlignment (androidx.compose.ui.BiasAlignment(horizontalBias: kotlin.Float, verticalBias: kotlin.Float): androidx.compose.ui.BiasAlignment)""" ... @property diff --git a/pythonx/compose/ui/graphics/__init__.pyi b/pythonx/compose/ui/graphics/__init__.pyi index 6465aad..7e1a297 100644 --- a/pythonx/compose/ui/graphics/__init__.pyi +++ b/pythonx/compose/ui/graphics/__init__.pyi @@ -1,7 +1,41 @@ -# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37131041879, commit 452cc807, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.ui +class _BlurEffect_callable_module(_t.Protocol): + """Kotlin: the function BlurEffect, and the members of the module BlurEffect""" + def __call__(self, radius_x: float, radius_y: float, edge_treatment: _t.Any = ...) -> _t.Any: ... + BlurSigmaScale: float + +class _Color_callable_module(_t.Protocol): + """Kotlin: the function Color, and the members of the module Color""" + @_t.overload + def __call__(self, red: float, green: float, blue: float, alpha: float = ..., color_space: _t.Any = ...) -> _t.Any: ... + @_t.overload + def __call__(self, color: int) -> _t.Any: ... # type: ignore[overload-cannot-match] + @_t.overload + def __call__(self, red: int, green: int, blue: int, alpha: int = ...) -> _t.Any: ... # type: ignore[overload-cannot-match] + @_t.overload + def __call__(self, color: int) -> _t.Any: ... # type: ignore[overload-cannot-match] + Black: _t.Any + Blue: _t.Any + Cyan: _t.Any + DarkGray: _t.Any + Gray: _t.Any + Green: _t.Any + LightGray: _t.Any + Magenta: _t.Any + Red: _t.Any + Transparent: _t.Any + Unspecified: _t.Any + White: _t.Any + Yellow: _t.Any + +class _TransformOrigin_callable_module(_t.Protocol): + """Kotlin: the function TransformOrigin, and the members of the module TransformOrigin""" + def __call__(self, pivot_fraction_x: float, pivot_fraction_y: float) -> _t.Any: ... + Center: _t.Any + class _ColorFilter_asDesktopColorFilter(_t.Protocol): def __call__(self) -> _t.Any: ... @@ -19,7 +53,7 @@ class _RenderEffect_asDesktopImageFilter(_t.Protocol): class BlendModeColorFilter(ColorFilter): """Kotlin: androidx.compose.ui.graphics.BlendModeColorFilter""" - def __init__(self, color: _t.Any, blendMode: _t.Any) -> None: + def __init__(self, color: _t.Any, blend_mode: _t.Any) -> None: """Kotlin constructor: androidx.compose.ui.graphics.BlendModeColorFilter (androidx.compose.ui.graphics.BlendModeColorFilter(color: androidx.compose.ui.graphics.Color, blendMode: androidx.compose.ui.graphics.BlendMode): androidx.compose.ui.graphics.BlendModeColorFilter)""" ... @property @@ -48,7 +82,7 @@ class ColorMatrix: class ColorMatrixColorFilter(ColorFilter): """Kotlin: androidx.compose.ui.graphics.ColorMatrixColorFilter""" - def __init__(self, colorMatrix: ColorMatrix) -> None: + def __init__(self, color_matrix: ColorMatrix) -> None: """Kotlin constructor: androidx.compose.ui.graphics.ColorMatrixColorFilter (androidx.compose.ui.graphics.ColorMatrixColorFilter(colorMatrix: androidx.compose.ui.graphics.ColorMatrix): androidx.compose.ui.graphics.ColorMatrixColorFilter)""" ... @@ -187,7 +221,7 @@ class Shape: class SkiaGraphicsContext(GraphicsContext): """Kotlin: androidx.compose.ui.graphics.SkiaGraphicsContext""" - def __init__(self, measureDrawBounds: bool = ...) -> None: + def __init__(self, measure_draw_bounds: bool = ...) -> None: """Kotlin constructor: androidx.compose.ui.graphics.SkiaGraphicsContext (androidx.compose.ui.graphics.SkiaGraphicsContext(measureDrawBounds: kotlin.Boolean): androidx.compose.ui.graphics.SkiaGraphicsContext)""" ... @property @@ -212,10 +246,6 @@ class Vertices: """Kotlin property: androidx.compose.ui.graphics.Vertices.vertexMode (androidx.compose.ui.graphics.Vertices.vertexMode: androidx.compose.ui.graphics.VertexMode)""" ... -def BlurEffect(radius_x: float, radius_y: float, edge_treatment: _t.Any = ...) -> _t.Any: - """Kotlin: androidx.compose.ui.graphics.BlurEffect(radiusX: kotlin.Float, radiusY: kotlin.Float, edgeTreatment: androidx.compose.ui.graphics.TileMode): androidx.compose.ui.graphics.BlurEffect""" - ... - def Canvas(image: _t.Any) -> _t.Any: """Kotlin: androidx.compose.ui.graphics.Canvas(image: androidx.compose.ui.graphics.ImageBitmap): androidx.compose.ui.graphics.Canvas""" ... @@ -280,10 +310,6 @@ def ShaderBrush(shader: Shader) -> _t.Any: """Kotlin: androidx.compose.ui.graphics.ShaderBrush(shader: androidx.compose.ui.graphics.Shader): androidx.compose.ui.graphics.ShaderBrush""" ... -def TransformOrigin(pivot_fraction_x: float, pivot_fraction_y: float) -> _t.Any: - """Kotlin: androidx.compose.ui.graphics.TransformOrigin(pivotFractionX: kotlin.Float, pivotFractionY: kotlin.Float): androidx.compose.ui.graphics.TransformOrigin""" - ... - def add_outline(receiver: _t.Any, /, outline: Outline) -> None: """Kotlin: androidx.compose.ui.graphics.Path.addOutline(outline: androidx.compose.ui.graphics.Outline): kotlin.Unit""" ... @@ -524,14 +550,11 @@ def with_save_layer(receiver: _t.Any, /, bounds: _t.Any, paint: _t.Any, block: _ """Kotlin: androidx.compose.ui.graphics.Canvas.withSaveLayer(bounds: androidx.compose.ui.geometry.Rect, paint: androidx.compose.ui.graphics.Paint, block: kotlin.Function0): kotlin.Unit""" ... -@_t.overload -def Color(red: float, green: float, blue: float, alpha: float = ..., color_space: _t.Any = ...) -> _t.Any: ... -@_t.overload -def Color(color: int) -> _t.Any: ... # type: ignore[overload-cannot-match] -@_t.overload -def Color(red: int, green: int, blue: int, alpha: int = ...) -> _t.Any: ... # type: ignore[overload-cannot-match] -@_t.overload -def Color(color: int) -> _t.Any: ... # type: ignore[overload-cannot-match] +BlurEffect: _BlurEffect_callable_module + +Color: _Color_callable_module + +TransformOrigin: _TransformOrigin_callable_module @_t.overload def as_compose_path_effect(receiver: _t.Any, /) -> PathEffect: ... @@ -727,6 +750,10 @@ 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/pythonx/compose/ui/graphics/vector/__init__.pyi b/pythonx/compose/ui/graphics/vector/__init__.pyi index 21fc88b..86c7b2b 100644 --- a/pythonx/compose/ui/graphics/vector/__init__.pyi +++ b/pythonx/compose/ui/graphics/vector/__init__.pyi @@ -1,4 +1,4 @@ -# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37131041879, commit 452cc807, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.runtime import pythonx.compose.ui.graphics diff --git a/pythonx/compose/ui/text/__init__.pyi b/pythonx/compose/ui/text/__init__.pyi index 27b36db..9c55f19 100644 --- a/pythonx/compose/ui/text/__init__.pyi +++ b/pythonx/compose/ui/text/__init__.pyi @@ -1,8 +1,24 @@ -# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37131041879, commit 452cc807, Compose 1.11.1). Do not edit. import typing as _t import pythonx.compose.ui.graphics import pythonx.compose.ui.unit +class _AnnotatedString_callable_module(_t.Protocol): + """Kotlin: the function AnnotatedString, and the members of the module AnnotatedString""" + @_t.overload + def __call__(self, text: str, paragraph_style: ParagraphStyle) -> _t.Any: ... + @_t.overload + def __call__(self, text: str, span_style: SpanStyle, paragraph_style: ParagraphStyle | None = ...) -> _t.Any: ... # type: ignore[overload-cannot-match] + Saver: _t.Any + +class _TextRange_callable_module(_t.Protocol): + """Kotlin: the function TextRange, and the members of the module TextRange""" + @_t.overload + def __call__(self, index: int) -> _t.Any: ... + @_t.overload + def __call__(self, start: int, end: int) -> _t.Any: ... # type: ignore[overload-cannot-match] + Zero: _t.Any + class LinkAnnotation: """Kotlin: androidx.compose.ui.text.LinkAnnotation""" @property @@ -20,9 +36,9 @@ class LinkInteractionListener: class MultiParagraph: """Kotlin: androidx.compose.ui.text.MultiParagraph""" @_t.overload - def __init__(self, intrinsics: MultiParagraphIntrinsics, constraints: int, maxLines: int = ..., overflow: _t.Any = ...) -> None: ... + def __init__(self, intrinsics: MultiParagraphIntrinsics, constraints: int, max_lines: int = ..., overflow: _t.Any = ...) -> None: ... @_t.overload - def __init__(self, intrinsics: MultiParagraphIntrinsics, maxLines: int = ..., ellipsis: bool = ..., *, width: float) -> None: ... # type: ignore[overload-cannot-match] + def __init__(self, intrinsics: MultiParagraphIntrinsics, max_lines: int = ..., ellipsis: bool = ..., *, width: float) -> None: ... # type: ignore[overload-cannot-match] @property def did_exceed_max_lines(self) -> bool: """Kotlin property: androidx.compose.ui.text.MultiParagraph.didExceedMaxLines (androidx.compose.ui.text.MultiParagraph.didExceedMaxLines: kotlin.Boolean)""" @@ -100,7 +116,7 @@ class ParagraphIntrinsics: class ParagraphStyle: """Kotlin: androidx.compose.ui.text.ParagraphStyle""" - def __init__(self, textAlign: _t.Any, textDirection: _t.Any, lineHeight: _t.Any, textIndent: _t.Any | None, platformStyle: _t.Any | None, lineHeightStyle: _t.Any | None, lineBreak: _t.Any, hyphens: _t.Any, textMotion: _t.Any | None) -> None: + def __init__(self, text_align: _t.Any, text_direction: _t.Any, line_height: _t.Any, text_indent: _t.Any | None, platform_style: _t.Any | None, line_height_style: _t.Any | None, line_break: _t.Any, hyphens: _t.Any, text_motion: _t.Any | None) -> None: """Kotlin constructor: androidx.compose.ui.text.ParagraphStyle (androidx.compose.ui.text.ParagraphStyle(textAlign: androidx.compose.ui.text.style.TextAlign, textDirection: androidx.compose.ui.text.style.TextDirection, lineHeight: androidx.compose.ui.unit.TextUnit, textIndent: androidx.compose.ui.text.style.TextIndent?, platformStyle: androidx.compose.ui.text.PlatformParagraphStyle?, lineHeightStyle: androidx.compose.ui.text.style.LineHeightStyle?, lineBreak: androidx.compose.ui.text.style.LineBreak, hyphens: androidx.compose.ui.text.style.Hyphens, textMotion: androidx.compose.ui.text.style.TextMotion?): androidx.compose.ui.text.ParagraphStyle)""" ... @property @@ -158,7 +174,7 @@ class ParagraphStyle: class Placeholder: """Kotlin: androidx.compose.ui.text.Placeholder""" - def __init__(self, width: _t.Any, height: _t.Any, placeholderVerticalAlign: _t.Any) -> None: + def __init__(self, width: _t.Any, height: _t.Any, placeholder_vertical_align: _t.Any) -> None: """Kotlin constructor: androidx.compose.ui.text.Placeholder (androidx.compose.ui.text.Placeholder(width: androidx.compose.ui.unit.TextUnit, height: androidx.compose.ui.unit.TextUnit, placeholderVerticalAlign: androidx.compose.ui.text.PlaceholderVerticalAlign): androidx.compose.ui.text.Placeholder)""" ... @property @@ -177,9 +193,9 @@ class Placeholder: class PlatformTextStyle: """Kotlin: androidx.compose.ui.text.PlatformTextStyle""" @_t.overload - def __init__(self, spanStyle: _t.Any | None, paragraphStyle: _t.Any | None) -> None: ... + def __init__(self, span_style: _t.Any | None, paragraph_style: _t.Any | None) -> None: ... @_t.overload - def __init__(self, textDecorationLineStyle: _t.Any | None) -> None: ... # type: ignore[overload-cannot-match] + def __init__(self, text_decoration_line_style: _t.Any | None) -> None: ... # type: ignore[overload-cannot-match] @property def paragraph_style(self) -> _t.Any | None: """Kotlin property: androidx.compose.ui.text.PlatformTextStyle.paragraphStyle (androidx.compose.ui.text.PlatformTextStyle.paragraphStyle: androidx.compose.ui.text.PlatformParagraphStyle?)""" @@ -192,9 +208,9 @@ class PlatformTextStyle: class SpanStyle: """Kotlin: androidx.compose.ui.text.SpanStyle""" @_t.overload - def __init__(self, brush: pythonx.compose.ui.graphics.Brush | None, alpha: float, fontSize: _t.Any, fontWeight: _t.Any | None, fontStyle: _t.Any | None, fontSynthesis: _t.Any | None, fontFamily: _t.Any | None, fontFeatureSettings: str | None, letterSpacing: _t.Any, baselineShift: _t.Any | None, textGeometricTransform: _t.Any | None, localeList: _t.Any | None, background: _t.Any, textDecoration: _t.Any | None, shadow: pythonx.compose.ui.graphics.Shadow | None, platformStyle: _t.Any | None, drawStyle: _t.Any | None) -> None: ... + def __init__(self, brush: pythonx.compose.ui.graphics.Brush | None, alpha: float, font_size: _t.Any, font_weight: _t.Any | None, font_style: _t.Any | None, font_synthesis: _t.Any | None, font_family: _t.Any | None, font_feature_settings: str | None, letter_spacing: _t.Any, baseline_shift: _t.Any | None, text_geometric_transform: _t.Any | None, locale_list: _t.Any | None, background: _t.Any, text_decoration: _t.Any | None, shadow: pythonx.compose.ui.graphics.Shadow | None, platform_style: _t.Any | None, draw_style: _t.Any | None) -> None: ... @_t.overload - def __init__(self, color: _t.Any, fontSize: _t.Any, fontWeight: _t.Any | None, fontStyle: _t.Any | None, fontSynthesis: _t.Any | None, fontFamily: _t.Any | None, fontFeatureSettings: str | None, letterSpacing: _t.Any, baselineShift: _t.Any | None, textGeometricTransform: _t.Any | None, localeList: _t.Any | None, background: _t.Any, textDecoration: _t.Any | None, shadow: pythonx.compose.ui.graphics.Shadow | None, platformStyle: _t.Any | None, drawStyle: _t.Any | None) -> None: ... # type: ignore[overload-cannot-match] + def __init__(self, color: _t.Any, font_size: _t.Any, font_weight: _t.Any | None, font_style: _t.Any | None, font_synthesis: _t.Any | None, font_family: _t.Any | None, font_feature_settings: str | None, letter_spacing: _t.Any, baseline_shift: _t.Any | None, text_geometric_transform: _t.Any | None, locale_list: _t.Any | None, background: _t.Any, text_decoration: _t.Any | None, shadow: pythonx.compose.ui.graphics.Shadow | None, platform_style: _t.Any | None, draw_style: _t.Any | None) -> None: ... # type: ignore[overload-cannot-match] @property def alpha(self) -> float: """Kotlin property: androidx.compose.ui.text.SpanStyle.alpha (androidx.compose.ui.text.SpanStyle.alpha: kotlin.Float)""" @@ -313,7 +329,7 @@ class TextLayoutInput: class TextLayoutResult: """Kotlin: androidx.compose.ui.text.TextLayoutResult""" - def __init__(self, layoutInput: TextLayoutInput, multiParagraph: MultiParagraph, size: _t.Any) -> None: + def __init__(self, layout_input: TextLayoutInput, multi_paragraph: MultiParagraph, size: _t.Any) -> None: """Kotlin constructor: androidx.compose.ui.text.TextLayoutResult (androidx.compose.ui.text.TextLayoutResult(layoutInput: androidx.compose.ui.text.TextLayoutInput, multiParagraph: androidx.compose.ui.text.MultiParagraph, size: androidx.compose.ui.unit.IntSize): androidx.compose.ui.text.TextLayoutResult)""" ... @property @@ -355,7 +371,7 @@ class TextLayoutResult: class TextLinkStyles: """Kotlin: androidx.compose.ui.text.TextLinkStyles""" - def __init__(self, style: SpanStyle | None = ..., focusedStyle: SpanStyle | None = ..., hoveredStyle: SpanStyle | None = ..., pressedStyle: SpanStyle | None = ...) -> None: + def __init__(self, style: SpanStyle | None = ..., focused_style: SpanStyle | None = ..., hovered_style: SpanStyle | None = ..., pressed_style: SpanStyle | None = ...) -> None: """Kotlin constructor: androidx.compose.ui.text.TextLinkStyles (androidx.compose.ui.text.TextLinkStyles(style: androidx.compose.ui.text.SpanStyle?, focusedStyle: androidx.compose.ui.text.SpanStyle?, hoveredStyle: androidx.compose.ui.text.SpanStyle?, pressedStyle: androidx.compose.ui.text.SpanStyle?): androidx.compose.ui.text.TextLinkStyles)""" ... @property @@ -377,7 +393,7 @@ class TextLinkStyles: class TextMeasurer: """Kotlin: androidx.compose.ui.text.TextMeasurer""" - def __init__(self, defaultFontFamilyResolver: _t.Any, defaultDensity: _t.Any, defaultLayoutDirection: pythonx.compose.ui.unit.LayoutDirection, cacheSize: int = ...) -> None: + def __init__(self, default_font_family_resolver: _t.Any, default_density: _t.Any, default_layout_direction: pythonx.compose.ui.unit.LayoutDirection, cache_size: int = ...) -> None: """Kotlin constructor: androidx.compose.ui.text.TextMeasurer (androidx.compose.ui.text.TextMeasurer(defaultFontFamilyResolver: androidx.compose.ui.text.font.FontFamily.Resolver, defaultDensity: androidx.compose.ui.unit.Density, defaultLayoutDirection: androidx.compose.ui.unit.LayoutDirection, cacheSize: kotlin.Int): androidx.compose.ui.text.TextMeasurer)""" ... @@ -528,6 +544,10 @@ def to_upper_case__String_LocaleList(receiver: str, /, locale_list: _t.Any) -> s """Kotlin: kotlin.String.toUpperCase(localeList: androidx.compose.ui.text.intl.LocaleList): kotlin.String""" ... +AnnotatedString: _AnnotatedString_callable_module + +TextRange: _TextRange_callable_module + def MultiParagraph__MultiParagraphIntrinsics_Constraints_Int_TextOverflow(intrinsics: MultiParagraphIntrinsics, constraints: int, max_lines: int = ..., overflow: _t.Any = ...) -> MultiParagraph: """Kotlin: androidx.compose.ui.text.MultiParagraph(intrinsics: androidx.compose.ui.text.MultiParagraphIntrinsics, constraints: androidx.compose.ui.unit.Constraints, maxLines: kotlin.Int, overflow: androidx.compose.ui.text.style.TextOverflow): androidx.compose.ui.text.MultiParagraph""" ... @@ -552,21 +572,11 @@ def SpanStyle__Color_TextUnit_FontWeight_FontStyle_FontSynthesis_FontFamily_Stri """Kotlin: androidx.compose.ui.text.SpanStyle(color: androidx.compose.ui.graphics.Color, fontSize: androidx.compose.ui.unit.TextUnit, fontWeight: androidx.compose.ui.text.font.FontWeight?, fontStyle: androidx.compose.ui.text.font.FontStyle?, fontSynthesis: androidx.compose.ui.text.font.FontSynthesis?, fontFamily: androidx.compose.ui.text.font.FontFamily?, fontFeatureSettings: kotlin.String?, letterSpacing: androidx.compose.ui.unit.TextUnit, baselineShift: androidx.compose.ui.text.style.BaselineShift?, textGeometricTransform: androidx.compose.ui.text.style.TextGeometricTransform?, localeList: androidx.compose.ui.text.intl.LocaleList?, background: androidx.compose.ui.graphics.Color, textDecoration: androidx.compose.ui.text.style.TextDecoration?, shadow: androidx.compose.ui.graphics.Shadow?, platformStyle: androidx.compose.ui.text.PlatformSpanStyle?, drawStyle: androidx.compose.ui.graphics.drawscope.DrawStyle?): androidx.compose.ui.text.SpanStyle""" ... -@_t.overload -def AnnotatedString(text: str, paragraph_style: ParagraphStyle) -> _t.Any: ... -@_t.overload -def AnnotatedString(text: str, span_style: SpanStyle, paragraph_style: ParagraphStyle | None = ...) -> _t.Any: ... # type: ignore[overload-cannot-match] - @_t.overload def Paragraph(paragraph_intrinsics: ParagraphIntrinsics, constraints: int, max_lines: int = ..., overflow: _t.Any = ...) -> _t.Any: ... @_t.overload def Paragraph(paragraph_intrinsics: ParagraphIntrinsics, max_lines: int = ..., ellipsis: bool = ..., *, width: float) -> _t.Any: ... # type: ignore[overload-cannot-match] -@_t.overload -def TextRange(index: int) -> _t.Any: ... -@_t.overload -def TextRange(start: int, end: int) -> _t.Any: ... # type: ignore[overload-cannot-match] - @_t.overload def capitalize(receiver: _t.Any, /, locale_list: _t.Any = ...) -> _t.Any: ... @_t.overload diff --git a/pythonx/compose/ui/unit/__init__.pyi b/pythonx/compose/ui/unit/__init__.pyi index c10242d..8db6b6e 100644 --- a/pythonx/compose/ui/unit/__init__.pyi +++ b/pythonx/compose/ui/unit/__init__.pyi @@ -1,6 +1,54 @@ -# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37103737430, commit a5028618, Compose 1.11.1). Do not edit. +# GENERATED by scripts/gen_stubs.py from python-multiplatform's Kotlin-named stubs (artifact kotlin-stubs, workflow run 37131041879, commit 452cc807, Compose 1.11.1). Do not edit. import typing as _t +class _Constraints_callable_module(_t.Protocol): + """Kotlin: the function Constraints, and the members of the module Constraints""" + def __call__(self, min_width: int = ..., max_width: int = ..., min_height: int = ..., max_height: int = ...) -> _t.Any: ... + Infinity: int + +class _DpOffset_callable_module(_t.Protocol): + """Kotlin: the function DpOffset, and the members of the module DpOffset""" + def __call__(self, x: float, y: float) -> int: ... + Unspecified: int + Zero: int + +class _DpSize_callable_module(_t.Protocol): + """Kotlin: the function DpSize, and the members of the module DpSize""" + def __call__(self, width: float, height: float) -> _t.Any: ... + Unspecified: _t.Any + Zero: _t.Any + +class _IntOffset_callable_module(_t.Protocol): + """Kotlin: the function IntOffset, and the members of the module IntOffset""" + def __call__(self, x: int, y: int) -> int: ... + Max: int + Zero: int + +class _IntRect_callable_module(_t.Protocol): + """Kotlin: the function IntRect, and the members of the module IntRect""" + @_t.overload + def __call__(self, center: int, radius: int) -> _t.Any: ... + @_t.overload + def __call__(self, top_left: int, bottom_right: int) -> _t.Any: ... # type: ignore[overload-cannot-match] + @_t.overload + def __call__(self, offset: int, size: _t.Any) -> _t.Any: ... # type: ignore[overload-cannot-match] + Zero: _t.Any + +class _IntSize_callable_module(_t.Protocol): + """Kotlin: the function IntSize, and the members of the module IntSize""" + def __call__(self, width: int, height: int) -> int: ... + Zero: int + +class _TextUnit_callable_module(_t.Protocol): + """Kotlin: the function TextUnit, and the members of the module TextUnit""" + def __call__(self, value: float, type: int) -> _t.Any: ... + Unspecified: _t.Any + +class _Velocity_callable_module(_t.Protocol): + """Kotlin: the function Velocity, and the members of the module Velocity""" + def __call__(self, x: float, y: float) -> _t.Any: ... + Zero: _t.Any + class DpRect: """Kotlin: androidx.compose.ui.unit.DpRect""" @_t.overload @@ -46,26 +94,10 @@ class FontScalingLinear: class LayoutDirection: """Kotlin: androidx.compose.ui.unit.LayoutDirection""" -def Constraints(min_width: int = ..., max_width: int = ..., min_height: int = ..., max_height: int = ...) -> _t.Any: - """Kotlin: androidx.compose.ui.unit.Constraints(minWidth: kotlin.Int, maxWidth: kotlin.Int, minHeight: kotlin.Int, maxHeight: kotlin.Int): androidx.compose.ui.unit.Constraints""" - ... - def Density(density: float, font_scale: float = ...) -> _t.Any: """Kotlin: androidx.compose.ui.unit.Density(density: kotlin.Float, fontScale: kotlin.Float): androidx.compose.ui.unit.Density""" ... -def DpOffset(x: float, y: float) -> int: - """Kotlin: androidx.compose.ui.unit.DpOffset(x: androidx.compose.ui.unit.Dp, y: androidx.compose.ui.unit.Dp): androidx.compose.ui.unit.DpOffset""" - ... - -def DpSize(width: float, height: float) -> _t.Any: - """Kotlin: androidx.compose.ui.unit.DpSize(width: androidx.compose.ui.unit.Dp, height: androidx.compose.ui.unit.Dp): androidx.compose.ui.unit.DpSize""" - ... - -def IntOffset(x: int, y: int) -> int: - """Kotlin: androidx.compose.ui.unit.IntOffset(x: kotlin.Int, y: kotlin.Int): androidx.compose.ui.unit.IntOffset""" - ... - def IntRect__IntOffset_Int(center: int, radius: int) -> _t.Any: """Kotlin: androidx.compose.ui.unit.IntRect(center: androidx.compose.ui.unit.IntOffset, radius: kotlin.Int): androidx.compose.ui.unit.IntRect""" ... @@ -78,18 +110,6 @@ def IntRect__IntOffset_IntSize(offset: int, size: _t.Any) -> _t.Any: """Kotlin: androidx.compose.ui.unit.IntRect(offset: androidx.compose.ui.unit.IntOffset, size: androidx.compose.ui.unit.IntSize): androidx.compose.ui.unit.IntRect""" ... -def IntSize(width: int, height: int) -> int: - """Kotlin: androidx.compose.ui.unit.IntSize(width: kotlin.Int, height: kotlin.Int): androidx.compose.ui.unit.IntSize""" - ... - -def TextUnit(value: float, type: int) -> _t.Any: - """Kotlin: androidx.compose.ui.unit.TextUnit(value: kotlin.Float, type: androidx.compose.ui.unit.TextUnitType): androidx.compose.ui.unit.TextUnit""" - ... - -def Velocity(x: float, y: float) -> _t.Any: - """Kotlin: androidx.compose.ui.unit.Velocity(x: kotlin.Float, y: kotlin.Float): androidx.compose.ui.unit.Velocity""" - ... - def coerce_at_least(receiver: float, /, minimum_value: float) -> float: """Kotlin: androidx.compose.ui.unit.Dp.coerceAtLeast(minimumValue: androidx.compose.ui.unit.Dp): androidx.compose.ui.unit.Dp""" ... @@ -258,6 +278,22 @@ def to_size(receiver: _t.Any, /) -> int: """Kotlin: androidx.compose.ui.unit.IntSize.toSize(): androidx.compose.ui.geometry.Size""" ... +Constraints: _Constraints_callable_module + +DpOffset: _DpOffset_callable_module + +DpSize: _DpSize_callable_module + +IntOffset: _IntOffset_callable_module + +IntRect: _IntRect_callable_module + +IntSize: _IntSize_callable_module + +TextUnit: _TextUnit_callable_module + +Velocity: _Velocity_callable_module + def DpRect__DpOffset_DpSize(origin: int, size: _t.Any) -> DpRect: """Kotlin: androidx.compose.ui.unit.DpRect(origin: androidx.compose.ui.unit.DpOffset, size: androidx.compose.ui.unit.DpSize): androidx.compose.ui.unit.DpRect""" ... @@ -266,13 +302,6 @@ def DpRect__Dp_Dp_Dp_Dp(left: float, top: float, right: float, bottom: float) -> """Kotlin: androidx.compose.ui.unit.DpRect(left: androidx.compose.ui.unit.Dp, top: androidx.compose.ui.unit.Dp, right: androidx.compose.ui.unit.Dp, bottom: androidx.compose.ui.unit.Dp): androidx.compose.ui.unit.DpRect""" ... -@_t.overload -def IntRect(center: int, radius: int) -> _t.Any: ... -@_t.overload -def IntRect(top_left: int, bottom_right: int) -> _t.Any: ... # type: ignore[overload-cannot-match] -@_t.overload -def IntRect(offset: int, size: _t.Any) -> _t.Any: ... # type: ignore[overload-cannot-match] - @_t.overload def constrain(receiver: int, /, other_constraints: int) -> _t.Any: ... @_t.overload diff --git a/scripts/gen_stubs.py b/scripts/gen_stubs.py index d1414e3..ab29ed9 100644 --- a/scripts/gen_stubs.py +++ b/scripts/gen_stubs.py @@ -17,12 +17,17 @@ input with explicit overloads only (`padding__Dp`, the first upstream format) also gets `@overload`s of the base name, fewest parameters first because a type checker takes the first match; -- a stub class keeps its name. Its members are renamed with `python_name`, which is the binder's - member-resolver rule, and the parameters of a member's callable protocol are snake_case like a - module function's, because the resolver also translates a method's keywords (the receiver and +- a stub class keeps its name. Its members are renamed with `python_name`, the binder's own + rule since #131, and the parameters of a member's callable protocol are snake_case like a + module function's, because the binder also translates a method's keywords (the receiver and anonymous slots keep their names). The stubs are Pythonic-only: a method's Kotlin keyword still works at run time and is a type error here. A static method (a function of a Kotlin object) is reached through `KotlinObject`, so its parameters are snake_case too; +- the binder's own alias names (python-multiplatform #131: `fill_max_width = fillMaxWidth` in a module + stub, an alias `ClassVar` or property per proxy member) are not carried over. Renaming the + Kotlin-named declaration by the rule above gives the same name, so each name is declared once and + no Kotlin second name or self-referential line remains. A name the binder declares as a callable + module (`PaddingValues: _PaddingValues_callable_module`) gets no base-name overloads beside it; - a Kotlin object served as a sub-package (`Alignment`) becomes a class in its parent module's stub, whether the input has it as `/__init__.pyi` or already as a class in the parent stub (python-multiplatform #44). A sub-package directory holding only `__init__.pyi` would be a @@ -188,6 +193,39 @@ def _is_static(function: ast.FunctionDef) -> bool: return any(ast.unparse(d) == "staticmethod" for d in function.decorator_list) +def _is_alias_line(node: ast.stmt) -> bool: + """`fill_max_width = fillMaxWidth`, the binder's own snake_case name (python-multiplatform #131). + + The generator names every declaration by the same rule, so the line adds nothing: dropped. + """ + return isinstance(node, ast.Assign) and isinstance(node.value, ast.Name) and all( + isinstance(target, ast.Name) for target in node.targets + ) + + +def _without_repeats(body: list[ast.stmt]) -> list[ast.stmt]: + """[body] without a member that repeats the one before it under the same name. + + The binder's alias member (`fill_max_width` beside `fillMaxWidth`, python-multiplatform #131) + is, once both are renamed, the same declaration twice. A member keeps its docstring statement. + """ + seen: set[str] = set() + kept: list[ast.stmt] = [] + skipping = False + for node in body: + if isinstance(node, ast.Expr) and kept and (skipping or isinstance(kept[-1], ast.AnnAssign)): + if not skipping: + kept.append(node) + continue + key = ast.dump(node) + skipping = key in seen and not isinstance(node, ast.ClassDef) + if skipping: + continue + seen.add(key) + kept.append(node) + return kept + + def _is_protocol(cls: ast.ClassDef) -> bool: return any(ast.unparse(base) in (f"{TYPING}.Protocol", "Protocol") for base in cls.bases) @@ -416,7 +454,7 @@ def convert(self, source: str, kotlin_package: str = "", module_name: str = "") body: list[ast.stmt] = [] explicit: dict[str, list[ast.FunctionDef]] = {} for node in tree.body: - if isinstance(node, (ast.Import, ast.ImportFrom)): + if isinstance(node, (ast.Import, ast.ImportFrom)) or _is_alias_line(node): continue if isinstance(node, ast.FunctionDef): node.name = python_name(node.name) @@ -431,7 +469,12 @@ def convert(self, source: str, kotlin_package: str = "", module_name: str = "") self._convert_class(node) body.append(node) - defined = {node.name for node in body if isinstance(node, ast.FunctionDef)} + # A name the binder declares as a callable module (`PaddingValues: _PaddingValues_callable_module`) + # is that declaration; its explicit overloads are the protocol's `__call__`, not a second name. + defined = {node.name for node in body if isinstance(node, ast.FunctionDef)} | { + node.target.id for node in body + if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) + } classes = {node.name for node in body if isinstance(node, ast.ClassDef)} for base, variants in explicit.items(): if base in defined: @@ -492,6 +535,7 @@ def _convert_class(self, cls: ast.ClassDef) -> None: _rename_arguments(member.args) elif isinstance(member, ast.ClassDef): self._convert_class(member) + cls.body = _without_repeats(cls.body) def _add_object(self, body: list[ast.stmt], kotlin_package: str, name: str, imported: set[str]) -> None: """Fold the object stub `//__init__.pyi` into a class of the parent.""" diff --git a/tests/fixtures/kotlin_stubs_v5/androidx/compose/foundation/layout/__init__.pyi b/tests/fixtures/kotlin_stubs_v5/androidx/compose/foundation/layout/__init__.pyi new file mode 100644 index 0000000..5d0ba50 --- /dev/null +++ b/tests/fixtures/kotlin_stubs_v5/androidx/compose/foundation/layout/__init__.pyi @@ -0,0 +1,35 @@ +# GENERATED by python-multiplatform-gradle-plugin. Do not edit. + +import typing as _t +import androidx.compose.ui + +class _PaddingValues_callable_module(_t.Protocol): + """Kotlin: the function PaddingValues, and the members of the module PaddingValues""" + def __call__(self, all: float) -> _t.Any: ... + Zero: _t.Any + +def Column(verticalAlignment: androidx.compose.ui.Alignment.Vertical = ..., *, content: _t.Callable[[], None]) -> None: + """Kotlin: androidx.compose.foundation.layout.Column(verticalAlignment: androidx.compose.ui.Alignment.Vertical, content: kotlin.Function0): kotlin.Unit""" + ... + +def fillMaxWidth(receiver: androidx.compose.ui.Modifier, /, fraction: float = ...) -> androidx.compose.ui.Modifier: + """Kotlin: androidx.compose.ui.Modifier.fillMaxWidth(fraction: kotlin.Float): androidx.compose.ui.Modifier""" + ... + +def fillMaxHeight(receiver: androidx.compose.ui.Modifier, /, fraction: float = ...) -> androidx.compose.ui.Modifier: + """Kotlin: androidx.compose.ui.Modifier.fillMaxHeight(fraction: kotlin.Float): androidx.compose.ui.Modifier""" + ... + +def scrollable(receiver: androidx.compose.ui.Modifier, /, state: _t.Any, enabled: bool = ..., on_click: _t.Callable[[], None] = ...) -> androidx.compose.ui.Modifier: + """Kotlin: androidx.compose.ui.Modifier.scrollable(state: kotlin.Any, enabled: kotlin.Boolean, onClick: kotlin.Function0): androidx.compose.ui.Modifier""" + ... + +def PaddingValues__Dp(all: float) -> _t.Any: + """Kotlin: androidx.compose.foundation.layout.PaddingValues(all: androidx.compose.ui.unit.Dp): androidx.compose.foundation.layout.PaddingValues""" + ... + +PaddingValues: _PaddingValues_callable_module + +fill_max_height = fillMaxHeight + +fill_max_width = fillMaxWidth diff --git a/tests/fixtures/kotlin_stubs_v5/androidx/compose/ui/__init__.pyi b/tests/fixtures/kotlin_stubs_v5/androidx/compose/ui/__init__.pyi new file mode 100644 index 0000000..7698b6c --- /dev/null +++ b/tests/fixtures/kotlin_stubs_v5/androidx/compose/ui/__init__.pyi @@ -0,0 +1,44 @@ +# GENERATED by python-multiplatform-gradle-plugin. Do not edit. + +import typing as _t +import androidx.compose.ui.unit + +class _Modifier_fillMaxWidth(_t.Protocol): + def __call__(self, fraction: float = ...) -> Modifier: ... + +class _Modifier_onClick(_t.Protocol): + def __call__(self, enabled: bool = ..., on_click: _t.Callable[[], None] = ...) -> Modifier: ... + +class Modifier: + """Kotlin: androidx.compose.ui.Modifier""" + fillMaxWidth: _t.ClassVar[_Modifier_fillMaxWidth] + onClick: _t.ClassVar[_Modifier_onClick] + fill_max_width: _t.ClassVar[_Modifier_fillMaxWidth] + on_click: _t.ClassVar[_Modifier_onClick] + +class Scene: + """Kotlin: androidx.compose.ui.Scene""" + @property + def layoutDirection(self) -> androidx.compose.ui.unit.LayoutDirection: + """Kotlin property: androidx.compose.ui.Scene.layoutDirection (androidx.compose.ui.Scene.layoutDirection: androidx.compose.ui.unit.LayoutDirection)""" + ... + @layoutDirection.setter + def layoutDirection(self, value: androidx.compose.ui.unit.LayoutDirection) -> None: + """Kotlin property: androidx.compose.ui.Scene.layoutDirection=""" + ... + @property + def isMinimized(self) -> bool: + """Kotlin property: androidx.compose.ui.Scene.isMinimized (androidx.compose.ui.Scene.isMinimized: kotlin.Boolean)""" + ... + @property + def layout_direction(self) -> androidx.compose.ui.unit.LayoutDirection: + """Kotlin property: androidx.compose.ui.Scene.layoutDirection (androidx.compose.ui.Scene.layoutDirection: androidx.compose.ui.unit.LayoutDirection)""" + ... + @layout_direction.setter + def layout_direction(self, value: androidx.compose.ui.unit.LayoutDirection) -> None: + """Kotlin property: androidx.compose.ui.Scene.layoutDirection=""" + ... + @property + def is_minimized(self) -> bool: + """Kotlin property: androidx.compose.ui.Scene.isMinimized (androidx.compose.ui.Scene.isMinimized: kotlin.Boolean)""" + ... diff --git a/tests/test_stubs.py b/tests/test_stubs.py index c1dd33e..16fa412 100644 --- a/tests/test_stubs.py +++ b/tests/test_stubs.py @@ -40,6 +40,7 @@ class per bound Kotlin type with its extension members as `ClassVar`s of callabl KOTLIN_STUBS_V2 = HERE / "fixtures" / "kotlin_stubs_v2" KOTLIN_STUBS_V3 = HERE / "fixtures" / "kotlin_stubs_v3" KOTLIN_STUBS_V4 = HERE / "fixtures" / "kotlin_stubs_v4" +KOTLIN_STUBS_V5 = HERE / "fixtures" / "kotlin_stubs_v5" ARTEFACT = REPO / ".tmp" / "kotlin-stubs.zip" PACKAGE = REPO / "pythonx" COMPOSE = PACKAGE / "compose" @@ -411,6 +412,65 @@ def test_a_class_with_no_such_constants_gets_no_groups(self): self.assertEqual({}, _nested(cls)) +class TheBindersOwnAliases(unittest.TestCase): + """The format of python-multiplatform #131: the binder writes its snake_case aliases itself. + + A module stub carries `fill_max_width = fillMaxWidth` beside the Kotlin-named def, and a proxy + class carries an extra `ClassVar` or property per member. The pythonx stubs stay Pythonic-only: + one name per declaration, no Kotlin second name, no self-referential line. + """ + + def setUp(self): + self.stubs = _generated(KOTLIN_STUBS_V5) + + def test_a_module_function_has_one_declaration_under_its_pythonic_name(self): + functions = _functions(self.stubs[LAYOUT]) + for name in ("fill_max_width", "fill_max_height", "scrollable"): + with self.subTest(name=name): + self.assertEqual(1, len(functions[name])) + self.assertNotIn("fillMaxWidth", functions) + + def test_the_binders_alias_lines_are_not_carried_over(self): + tree = ast.parse(self.stubs[LAYOUT]) + self.assertEqual([], [ast.unparse(n) for n in tree.body if isinstance(n, ast.Assign)]) + + def test_a_module_the_binder_calls_has_no_second_declaration_of_its_name(self): + """`PaddingValues: _PaddingValues_callable_module` is the name; its explicit overloads add no `def`.""" + text = self.stubs[LAYOUT] + self.assertNotIn("PaddingValues", _functions(text)) + self.assertEqual(1, sum(line.startswith("PaddingValues:") for line in text.splitlines())) + self.assertIn("class _PaddingValues_callable_module", text) + + def test_no_line_assigns_a_name_to_itself(self): + for path, text in self.stubs.items(): + for line in text.splitlines(): + left, sep, right = line.partition(" = ") + with self.subTest(path=path.relative_to(PACKAGE).as_posix(), line=line): + self.assertFalse(sep and left.isidentifier() and left == right.strip()) + + def test_a_proxy_class_has_each_member_once_under_its_pythonic_name(self): + modifier = _classes(self.stubs[UI])["Modifier"] + names = [n.target.id for n in modifier.body if isinstance(n, ast.AnnAssign)] + self.assertEqual(["fill_max_width", "on_click"], names) + + def test_a_property_and_its_setter_are_kept_once(self): + scene = _classes(self.stubs[UI])["Scene"] + functions = [n for n in scene.body if isinstance(n, ast.FunctionDef)] + self.assertEqual(["layout_direction", "layout_direction", "is_minimized"], [f.name for f in functions]) + self.assertEqual( + [["property"], ["layout_direction.setter"], ["property"]], + [[ast.unparse(d) for d in f.decorator_list] for f in functions], + ) + + def test_keywords_the_binder_already_wrote_in_snake_case_stay_unchanged(self): + scrollable = _functions(self.stubs[LAYOUT])["scrollable"][0] + self.assertEqual(["receiver", "state", "enabled", "on_click"], _parameter_names(scrollable)) + for name in ("on_click", "fill_max_width", "max_height", "index_in_line", "always_show_label"): + with self.subTest(name=name): + self.assertEqual(name, gen_stubs.python_name(name)) + self.assertEqual(name, gen_stubs.snake_case(name)) + + class TheObjectAndPropertyFormat(unittest.TestCase): """The upstream format of python-multiplatform #53 (objects), #38 (properties) and #44.""" From 4eef3081e66c2e5a9c43a2b72750a3bc3208fec1 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:20:15 +0900 Subject: [PATCH 12/16] Fix: Rename the members of an object's nested classes in the stubs Issue #86. gen_stubs.py's object folding appended an object's nested classes (Companion, Horizontal, ...) to the parent class without passing them through _convert_class, which is what applies the member rule and drops the binder's own aliases. So PathOperation.Companion.reverseDifference kept its Kotlin spelling, beside the binder's reverse_difference alias. One call fixes it at any depth: upper-case names kept, receiver and anonymous slots kept, aliases dropped, no duplicates. A scan of every committed stub found no other camelCase nested member. The binder serves a nested companion's member under its snake_case alias like any proxy member (python-multiplatform #131, read from its source; the fake host does not model nested companions). The documented baselines are brought up to date as well: they still showed the counts from before #79's step 5. defect fixed: nested-class member names in stubs. test added: TheNestedCompanion (depth 2 and 3, upper-case kept, alias collapsed). Red: the camelCase name and its alias both present. documentation corrected: baselines in SPEC 0, PROJECT.md, guide. Suite with python-multiplatform 452cc807 (worktree, zip): 256 passed, 1 skipped; without a checkout, 148 passed, 109 skipped. Generating twice gives no diff; one stub changed (ui/graphics: 1 rename, 4 duplicate lines gone). Refs #86 --- PROJECT.md | 4 +-- docs/SPEC.md | 8 ++--- docs/guide/getting-started.html | 4 +-- docs/guide/status.html | 2 +- pythonx/compose/ui/graphics/__init__.pyi | 6 +--- scripts/gen_stubs.py | 1 + tests/test_stubs.py | 46 ++++++++++++++++++++++++ 7 files changed, 57 insertions(+), 14 deletions(-) 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. From 557a2b710ad195ce0afe9adc100f17190c14963a Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:33:24 +0900 Subject: [PATCH 13/16] Fix: Serve a Kotlin object that is also a function as callable (Color) `pythonx.compose.ui.graphics.Color` came out as a plain KotlinObject, so `Color(0xFFFFFFFF)` raised "'KotlinObject' object is not callable" even though the binder makes the `Color` module callable (python-multiplatform #78). A Kotlin object module the binder made callable is now served as a CallableKotlinObject, which forwards the call to the binder's module; `Color.Red` stays a constant and `Alignment` stays uncallable. Tests: the fake host gains Color rows; test_chain CallableKotlinObjects (3 tests); test_wheel installs the built wheel into a venv and calls `Color(0xFFFFFFFF)` through the binder from there. SPEC and PROJECT baselines updated (148/113 and 260/1). --- PROJECT.md | 4 ++-- docs/SPEC.md | 14 +++++++++--- pythonx/compose/_reexport.py | 23 ++++++++++++++++++-- tests/fake_host.py | 15 +++++++++++++ tests/test_chain.py | 24 +++++++++++++++++++++ tests/test_wheel.py | 42 ++++++++++++++++++++++++++++++++++++ 6 files changed, 115 insertions(+), 7 deletions(-) diff --git a/PROJECT.md b/PROJECT.md index 5879281..aad80bf 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 체크아웃 없음 148 passed, 109 skipped - python-multiplatform develop 31c092f0+ 256 passed, 1 skipped + PythonMultiplatform 체크아웃 없음 148 passed, 113 skipped + python-multiplatform develop 31c092f0+ 260 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 55ec7fa..9869fe7 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -21,14 +21,15 @@ Run from a worktree with `uv run --with pytest --with mypy pytest tests -q -rs` | Environment | Result | |---|---| -| 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 | +| No `PythonMultiplatform` checkout found | **148 passed, 113 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)`) | **260 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 -108 tests that install the binder's layers through `tests/adapter.py` skip, plus the notebook test. +112 tests that install the binder's layers through `tests/adapter.py` skip (one of them against the +installed wheel, #88), 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 @@ -454,6 +455,13 @@ every access; a function inside the object is snake_case (`Arrangement.spaced_by `tests/test_chain.py::ObjectNamespaces` (6 tests); without the sub-package step 6 fail. The binder now lists these objects itself (python-multiplatform #35), and either way reaches them. +A Kotlin object whose name is also a function in its package is **callable**: `Color` is the object +holding `Color.Red` and the function `Color(0xFFFFFFFF)`. The binder makes such a module callable +(python-multiplatform #78), and the namespace serves the same call, so both `Color(0xFFFFFFFF)` and +`Color.Red` work; an object that is no function (`Alignment`) stays uncallable. Evidence: +`tests/test_chain.py::CallableKotlinObjects` (3 tests) and, from the installed wheel rather than the +source tree, `tests/test_wheel.py::TheWheel::test_color_is_callable_from_the_installed_wheel` (#88). + ### S7.1 Grouped constants, `Alignment.Horizontal.End`: `implemented` INTENT §5.3: the notebook groups constants by type, `Alignment.Horizontal.End`, and Kotlin writes diff --git a/pythonx/compose/_reexport.py b/pythonx/compose/_reexport.py index 165f1d4..f627da4 100644 --- a/pythonx/compose/_reexport.py +++ b/pythonx/compose/_reexport.py @@ -171,7 +171,7 @@ def _pythonic(value, name): if isinstance(value, types.ModuleType): # A Kotlin object served as a sub-package. Since python-multiplatform #35 the binder lists it # and exposes it as an attribute of its parent, so the name table reaches it directly. - return KotlinObject(value) + return _kotlin_object(value) # A function is the binder's own callable: since python-multiplatform #131 it takes snake_case # keywords and reports them in `inspect.signature` itself. return value @@ -196,7 +196,7 @@ def _resolve(kotlin, name: str): nested = importlib.import_module(kotlin.__name__ + "." + name) except ImportError: return None - return KotlinObject(nested), True + return _kotlin_object(nested), True return None @@ -276,6 +276,25 @@ def __repr__(self): return f"" +class CallableKotlinObject(KotlinObject): + """A Kotlin object whose name is also a function in its package (`Color`), callable as that function. + + The binder makes such a module callable (python-multiplatform #78): `Color(0xFFFFFFFF)` calls + the `Color` function, while `Color.Red` is still a constant of the object. Calling goes to the + binder's module itself, which resolves the function on every call. + """ + + __slots__ = () + + def __call__(self, *args, **kwargs): + return self._kotlin(*args, **kwargs) + + +def _kotlin_object(kotlin_module): + """The namespace for a Kotlin object module: callable only when the binder made the module callable.""" + return CallableKotlinObject(kotlin_module) if callable(kotlin_module) else KotlinObject(kotlin_module) + + class ConstantGroup: """The constants of a Kotlin object declared as one type nested in it: `Alignment.Horizontal`. diff --git a/tests/fake_host.py b/tests/fake_host.py index c9a9237..ae4ff1e 100644 --- a/tests/fake_host.py +++ b/tests/fake_host.py @@ -62,6 +62,7 @@ ARRANGEMENT_VERTICAL = ARRANGEMENT + ".Vertical" ARRANGEMENT_BOTH = ARRANGEMENT + ".HorizontalOrVertical" ALIGNMENT = "androidx.compose.ui.Alignment" +COLOR = "androidx.compose.ui.graphics.Color" ALIGNMENT_HORIZONTAL = ALIGNMENT + ".Horizontal" ALIGNMENT_VERTICAL = ALIGNMENT + ".Vertical" @@ -303,6 +304,7 @@ def body(receiver, args): def _build(self): self._scalar_boxes() self._build_text_field() + self._build_color() self._add( EMPTY_MODIFIER, 0, (), (), (), "OBJECT", MODIFIER, False, None, (), lambda args: StubModifier(), @@ -542,6 +544,19 @@ def _unbox(self, args): self.release(args[0]) return held + def _build_color(self): + """`Color`: a Kotlin object whose name is also a function in its package (python-multiplatform #78). + + The binder makes such a module callable (`_make_callable`): `Color(0xFFFFFFFF)` calls the + function, `Color.Red` reads a constant of the object. One overload is modelled; the real + `Color(Int)` / `Color(Long)` pair and its ambiguity are python-multiplatform #146. + """ + self._add( + COLOR, 1, ("color",), ("INT",), ("kotlin.Long",), "OBJECT", COLOR, False, None, + (False,), lambda args: StubConstant(f"Color({args[0]:#x})"), + ) + self._constant(f"{COLOR}.Red", COLOR) + def _constant(self, name, declared): self._add(name, 0, (), (), (), "OBJECT", declared, False, None, (), lambda args, label=name.rpartition(".")[2]: StubConstant(label), kind="STATIC_GETTER") diff --git a/tests/test_chain.py b/tests/test_chain.py index a0a43f5..aef4587 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -715,6 +715,30 @@ def test_a_kotlin_name_still_says_the_binder_is_missing(self): ui.Modifier # noqa: B018 +class CallableKotlinObjects(AdapterCase): + """A Kotlin object that is also a function (`Color`) is callable through pythonx (issue #88).""" + + def label_of(self, proxy): + return self.host._object(proxy._pm_handle).label + + def test_calling_the_object_calls_the_function(self): + import pythonx.compose.ui.graphics as graphics + + self.assertEqual("Color(0xffffffff)", self.label_of(graphics.Color(0xFFFFFFFF))) + + def test_its_constants_are_still_a_namespace(self): + import pythonx.compose.ui.graphics as graphics + + self.assertEqual("Red", self.label_of(graphics.Color.Red)) + + def test_an_object_that_is_no_function_stays_uncallable(self): + import pythonx.compose.ui as ui + + self.assertFalse(callable(ui.Alignment)) + with self.assertRaises(TypeError): + ui.Alignment() + + class Handles(AdapterCase): """The binder's proxy owns a handle; dropping it gives the handle back.""" diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 6299391..86d76c5 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -11,6 +11,7 @@ from __future__ import annotations import importlib.util +import os import shutil import subprocess import sys @@ -41,6 +42,24 @@ def _build_wheel(out: Path) -> Path: return wheel +_SCENARIO = """ +import sys +from pathlib import Path +sys.path.insert(0, {tests!r}) +import adapter, fake_host +adapter._REPO = Path({tests!r}) # do not put the source checkout on sys.path +binding = adapter.install() +host = fake_host.FakeHost(); host.bind(); host.register(binding) +import pythonx.compose.ui.graphics as graphics +assert "site-packages" in graphics.__file__, graphics.__file__ +white = graphics.Color(0xFFFFFFFF) # held: dropping a proxy releases its handle +red = graphics.Color.Red +assert host._object(white._pm_handle).label == "Color(0xffffffff)" +assert host._object(red._pm_handle).label == "Red" +print("installed wheel ok") +""" + + class TheWheel(unittest.TestCase): @classmethod @@ -51,6 +70,7 @@ def setUpClass(cls): SCRATCH.mkdir(exist_ok=True) cls._dir = tempfile.TemporaryDirectory(dir=SCRATCH) wheel = _build_wheel(Path(cls._dir.name)) + cls.wheel = wheel with zipfile.ZipFile(wheel) as archive: cls.names = set(archive.namelist()) @@ -86,6 +106,28 @@ def test_the_generated_stubs_ship(self): with self.subTest(stub=stub): self.assertIn(stub, self.names) + def test_color_is_callable_from_the_installed_wheel(self): + """Issue #88: the installed package, not the source tree, calls `Color(0xFFFFFFFF)` through the binder.""" + sys.path.insert(0, str(REPO / "tests")) + try: + import adapter + home = adapter.python_multiplatform_home() + finally: + sys.path.pop(0) + if not (home / adapter._ADAPTER_KT).is_file(): + self.skipTest("no python-multiplatform checkout to install the binder from") + venv = Path(self._dir.name) / "venv" + subprocess.run([sys.executable, "-m", "venv", str(venv)], check=True) + python = venv / "bin" / "python" + subprocess.run([str(python), "-m", "pip", "install", "-q", "--no-deps", "--no-index", str(self.wheel)], + check=True, capture_output=True, text=True) + script = _SCENARIO.format(tests=str(REPO / "tests")) + env = dict(os.environ, PYTHONMULTIPLATFORM_HOME=str(home)) + result = subprocess.run([str(python), "-c", script], capture_output=True, text=True, env=env, + cwd=str(venv)) + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("installed wheel ok", result.stdout) + if __name__ == "__main__": unittest.main() From 3be67e1603c008e9a77b139cfbe1e4dc528efa31 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:38:22 +0900 Subject: [PATCH 14/16] Docs: Record the callable Color and the release PR waiting on review Closes #90 --- PROJECT.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/PROJECT.md b/PROJECT.md index aad80bf..4167e3c 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -21,13 +21,13 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **` 노트북의 함수 시그니처는 **이름 예시**다. 실제 규칙은 "Kotlin 원래 매개변수를 `snake_case` 로 노출한다" 이다 (`onclick` 이 아니라 `on_click`). -## 3. 현황 (2026-10-03) +## 3. 현황 (2026-10-04) `docs/SPEC.md` 기준. 구현 = 이 저장소의 코드 + 읽고 통과를 확인한 테스트가 있는 것. | 상태 | 항목 | |---|---| -| 구현 | 매핑 매니페스트 `pythonx-map.toml`, `@Composable` 항등 데코레이터, 실제 디스크 패키지 `pythonx` 와 모듈 수준 규칙 기반 재노출(snake_case 이름·키워드·시그니처, 오버로드 디스패치, 값 클래스 허용 목록), 실제 Compose 1.11.1 에서 생성한 `.pyi` 스텁과 `py.typed`(wheel 포함, mypy 로 검사), `Alignment`/`Arrangement` 상수(평면 `Alignment.End` 와 묶음 `Alignment.Horizontal.End` 모두, #9), proxy 메서드의 snake_case 이름과 키워드 인자(`m.padding(padding_values=...)`; python-multiplatform #131 이 바인더 쪽에서 제공하므로 #131 이상 필요; Kotlin 이름도 실행 시 그대로 통하고 스텁은 snake_case 만) | +| 구현 | 매핑 매니페스트 `pythonx-map.toml`, `@Composable` 항등 데코레이터, 실제 디스크 패키지 `pythonx` 와 모듈 수준 규칙 기반 재노출(snake_case 이름·키워드·시그니처, 오버로드 디스패치, 값 클래스 허용 목록), 실제 Compose 1.11.1 에서 생성한 `.pyi` 스텁과 `py.typed`(wheel 포함, mypy 로 검사), `Alignment`/`Arrangement` 상수(평면 `Alignment.End` 와 묶음 `Alignment.Horizontal.End` 모두, #9), proxy 메서드의 snake_case 이름과 키워드 인자(`m.padding(padding_values=...)`; python-multiplatform #131 이 바인더 쪽에서 제공하므로 #131 이상 필요; Kotlin 이름도 실행 시 그대로 통하고 스텁은 snake_case 만), 함수이기도 한 Kotlin object 의 호출(`Color(0xFFFFFFFF)` 과 `Color.Red` 둘 다; #88, 설치한 wheel 에서도 테스트됨. Int/Long 오버로드 선택은 python-multiplatform #146) | | 부분 | 선언형 앱 루트 `@app`·`state`·`app_root`(#11; 로직은 테스트됨, 바인더 경로는 #38 모양의 가짜 호스트로 테스트됨, 실제 Compose 증거는 E2E 모듈 #19; 숫자·문자열 상태는 python-multiplatform #69 부터 왕복됨), 배포 설정, Material 3 위젯(렌더 증거는 python-multiplatform 에만), `Icon`(import 만 됨)·`DefaultIcons`(`Icons.Default` alias, 가짜 호스트로만 테스트됨, 스텁에서는 `DefaultIcons.Add` 가 `ImageVector`; `DefaultIcons.Add` 로 쓴다; 노트북의 `DefaultIcons.Add()` 가 아님), `TextField(state=...)`·`TextFieldState`(#10; `pythonx.compose.foundation.text.input`, 가짜 호스트로 테스트됨, 노트북의 `text_state=`/`padding=` 는 `state=`/`modifier=Modifier.padding(8)`; IME 조합 증거는 python-multiplatform E2E #26) | | 계획 | `remember_saveable`·색 스킴·코루틴 스코프 | @@ -115,6 +115,8 @@ uv run python .github/scripts/check_guide.py # 가이드: HTML 파싱, 링크 3. **릴리스·Pages 활성화**: `.github/scripts/release/` 와 `.github/workflows/`(release-sync, pages)는 들어와 있다. main 보호는 메인테이너가 저장소 설정에서 직접 관리하며 main 은 잠겨 있다. 스크립트나 에이전트는 보호 설정을 만들거나 바꾸지 않는다. release→main PR 머지는 메인테이너가 한다. + **현황(2026-10-04):** release→main PR #70 이 메인테이너 리뷰를 기다린다. Pages 는 PR #1 머지 때 + (2026-10-03) 배포된 문서를 보여 주며, #70 이 머지되어야 그 뒤의 가이드가 반영된다. ### 결정됨 (2026-10-03, #60) From 55e8f85b64b0d5cf2c711a140e9fc611c0553971 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:47:02 +0900 Subject: [PATCH 15/16] Docs: Name the host entry point PythonAppView, as python-multiplatform #105 restored it PythonContent no longer exists. The contract line is now PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root"), in SPEC S5.4, the guide, the runtime docstrings and the test docstring. SPEC and PROJECT also record that the root-swap entry point (#11) is the binder's. Docs and comments only: counts unchanged (149/112 and 261 in the main checkout). Closes #92 --- PROJECT.md | 2 +- docs/SPEC.md | 6 ++++-- docs/guide/guide-composables.html | 2 +- pythonx/compose/runtime/__init__.py | 6 ++++-- tests/test_app_root.py | 3 ++- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/PROJECT.md b/PROJECT.md index 4167e3c..f06716e 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -61,7 +61,7 @@ GitHub 마일스톤과 같은 내용이다. 날짜는 2026-10-03 에 정했고, 날짜 근거: - **M1 (2주):** 바인더 계약(`inspect.signature`, `python_multiplatform.describe`)이 PythonMultiplatform `d00f413f` 에 착지했다. 남은 일은 이 저장소의 순수 Python 과 가짜 호스트 하네스뿐이라 JVM 빌드가 필요 없다. -- **M2 (M1 뒤 3주):** 위젯마다 JVM 렌더 테스트가 필요하다. 처음엔 Gradle 캐시 링크가 끊겼다고 보고 4주로 잡았으나, 캐시는 `PythonMultiplatform/.caches` 로 다시 걸려 Compose 픽스처가 통과하고 있었고, 노트북 관련 질문도 INTENT §5 로 결정됐다. 남은 위험은 `TextField(state=...)` 의 IME 조합 증거(python-multiplatform E2E #26)와, 앱 루트 교체를 Kotlin 컴포지션이 관찰하는 진입점(바인더 몫일 수 있음)이다. +- **M2 (M1 뒤 3주):** 위젯마다 JVM 렌더 테스트가 필요하다. 처음엔 Gradle 캐시 링크가 끊겼다고 보고 4주로 잡았으나, 캐시는 `PythonMultiplatform/.caches` 로 다시 걸려 Compose 픽스처가 통과하고 있었고, 노트북 관련 질문도 INTENT §5 로 결정됐다. 남은 위험은 `TextField(state=...)` 의 IME 조합 증거(python-multiplatform E2E #26)와, 앱 루트 교체를 Kotlin 컴포지션이 관찰하는 진입점이다. 진입점은 바인더 몫으로 정해졌다(2026-10-04): python-multiplatform 의 `PythonAppView` 가 `app_root` State 를 컴포지션 안에서 읽고, #26 진단에서 다시 선언한 루트가 실제로 다시 그려짐을 확인했다. - **M3 (M2 뒤 2주):** 스텁은 M1 규칙만 있으면 만들 수 있어 M2 와 겹쳐 진행할 수 있다. 다만 노트북 위젯의 최종 이름이 M2 에서 정해지므로 마감은 M2 뒤로 둔다. ## 4. 구조 diff --git a/docs/SPEC.md b/docs/SPEC.md index 9869fe7..2f3112e 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -286,8 +286,10 @@ Tests: `tests/test_runtime_module.py::TheRuntimeSeam` (4 tests) and `::TheChaquo ### S5.4 The app root, `app` and `state`: `partial` (the binder path is tested against a fake host shaped after python-multiplatform #38; real Compose is the E2E module, issue #19) -The host draws with `PythonContent("pythonx.compose.runtime", "app_root")` (python-multiplatform #18, -issue #11). `pythonx.compose.runtime` provides: +The host draws with `PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root")` +(python-multiplatform #18 and #105; issue #11). The binder owns the redraw: `PythonAppView` reads +the `app_root` State inside the composition, so a redeclared root is drawn again (seen in the +python-multiplatform #26 diagnosis). `pythonx.compose.runtime` provides: - `app_root`: a Compose `MutableState` whose `.value` is a zero-argument callable, or `None` (the host draws nothing). It is created on first read, by the binder's diff --git a/docs/guide/guide-composables.html b/docs/guide/guide-composables.html index 0324809..a9346ea 100644 --- a/docs/guide/guide-composables.html +++ b/docs/guide/guide-composables.html @@ -60,7 +60,7 @@

Declare the app and keep state@app declares the screen the host draws and returns the function unchanged. Declaring it again, for example in a notebook cell, replaces the screen; there is no update call. state(initial) is a Compose state, read and written through .value. The host is configured once with PythonContent("pythonx.compose.runtime", "app_root").

@app 은 호스트가 그리는 화면을 선언하고 함수를 그대로 돌려줍니다. 노트북 셀 등에서 다시 선언하면 화면이 바뀌며, 갱신 호출은 없습니다. state(initial) 는 Compose 상태이며 .value 로 읽고 씁니다. 호스트는 PythonContent("pythonx.compose.runtime", "app_root") 로 한 번만 설정합니다.

+

@app declares the screen the host draws and returns the function unchanged. Declaring it again, for example in a notebook cell, replaces the screen; there is no update call. state(initial) is a Compose state, read and written through .value. The host is configured once with PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root").

@app 은 호스트가 그리는 화면을 선언하고 함수를 그대로 돌려줍니다. 노트북 셀 등에서 다시 선언하면 화면이 바뀌며, 갱신 호출은 없습니다. state(initial) 는 Compose 상태이며 .value 로 읽고 씁니다. 호스트는 PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root") 로 한 번만 설정합니다.

Status: partial. The binder path, which creates the state through Compose's mutableStateOf, is tested against a fake host shaped after python-multiplatform #38; proof against real Compose is still the E2E module. Numbers and strings round-trip: state(0) and count.value += 1 work (python-multiplatform #69).

상태: 부분. Compose 의 mutableStateOf 로 상태를 만드는 바인더 경로는 python-multiplatform #38 모양의 가짜 호스트로 테스트되었으며, 실제 Compose 증거는 아직 E2E 모듈입니다. 숫자와 문자열은 왕복됩니다: state(0) 과 count.value += 1 이 동작합니다(python-multiplatform #69).

diff --git a/pythonx/compose/runtime/__init__.py b/pythonx/compose/runtime/__init__.py index 3bc9441..429bae3 100644 --- a/pythonx/compose/runtime/__init__.py +++ b/pythonx/compose/runtime/__init__.py @@ -38,7 +38,8 @@ ## The app root and `state` `@app` declares the screen the host draws; `state(initial)` makes a value Compose observes. The host -is configured once with `PythonContent("pythonx.compose.runtime", "app_root")`: it reads the module +is configured once with +`PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root")`: it reads the module attribute `app_root`, a Compose `MutableState` whose `.value` is a zero-argument callable or `None` (nothing is drawn). `app(fn)` writes `app_root.value = fn` and returns `fn` unchanged, so declaring the root again -- in a notebook cell, say -- replaces the screen. There is deliberately no update or @@ -76,7 +77,8 @@ def Composable(target): app_root: State -"""The state the host reads (`PythonContent("pythonx.compose.runtime", "app_root")`); annotated +"""The state the host reads +(`PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root")`); annotated only, created on first read by `__getattr__` below.""" diff --git a/tests/test_app_root.py b/tests/test_app_root.py index 2830cc0..70b4278 100644 --- a/tests/test_app_root.py +++ b/tests/test_app_root.py @@ -1,6 +1,7 @@ """`@app`, `state` and `app_root` in `pythonx.compose.runtime` (issue #11, docs/INTENT.md section 5.1). -The host draws with `PythonContent("pythonx.compose.runtime", "app_root")`: it reads the module +The host draws with +`PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root")`: it reads the module attribute `app_root`, a Compose State whose `.value` is a zero-argument callable or None. `@app` writes that value, so redeclaring the root replaces the screen; there is no update function. From f2662b35db08ec2e3051a4ece5eafc4b9831c2bf Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sun, 4 Oct 2026 01:05:33 +0900 Subject: [PATCH 16/16] Docs: Cite the python-multiplatform notebook E2E as the evidence for S5.2 to S5.5 python-multiplatform #26 (develop 844cf291) passes 20 of 20 against this package's develop wheel. SPEC S5.2 to S5.5 and section 8, and PROJECT section 3, now cite it for the widget render proofs, the declared root and shared state, and the TextField IME composition. Statuses stay partial, because the evidence is outside this repository. Docs only: counts unchanged (149/112 and 261 in the main checkout). Closes #95 --- PROJECT.md | 4 ++-- docs/SPEC.md | 26 +++++++++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/PROJECT.md b/PROJECT.md index f06716e..38c78da 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -28,7 +28,7 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **` | 상태 | 항목 | |---|---| | 구현 | 매핑 매니페스트 `pythonx-map.toml`, `@Composable` 항등 데코레이터, 실제 디스크 패키지 `pythonx` 와 모듈 수준 규칙 기반 재노출(snake_case 이름·키워드·시그니처, 오버로드 디스패치, 값 클래스 허용 목록), 실제 Compose 1.11.1 에서 생성한 `.pyi` 스텁과 `py.typed`(wheel 포함, mypy 로 검사), `Alignment`/`Arrangement` 상수(평면 `Alignment.End` 와 묶음 `Alignment.Horizontal.End` 모두, #9), proxy 메서드의 snake_case 이름과 키워드 인자(`m.padding(padding_values=...)`; python-multiplatform #131 이 바인더 쪽에서 제공하므로 #131 이상 필요; Kotlin 이름도 실행 시 그대로 통하고 스텁은 snake_case 만), 함수이기도 한 Kotlin object 의 호출(`Color(0xFFFFFFFF)` 과 `Color.Red` 둘 다; #88, 설치한 wheel 에서도 테스트됨. Int/Long 오버로드 선택은 python-multiplatform #146) | -| 부분 | 선언형 앱 루트 `@app`·`state`·`app_root`(#11; 로직은 테스트됨, 바인더 경로는 #38 모양의 가짜 호스트로 테스트됨, 실제 Compose 증거는 E2E 모듈 #19; 숫자·문자열 상태는 python-multiplatform #69 부터 왕복됨), 배포 설정, Material 3 위젯(렌더 증거는 python-multiplatform 에만), `Icon`(import 만 됨)·`DefaultIcons`(`Icons.Default` alias, 가짜 호스트로만 테스트됨, 스텁에서는 `DefaultIcons.Add` 가 `ImageVector`; `DefaultIcons.Add` 로 쓴다; 노트북의 `DefaultIcons.Add()` 가 아님), `TextField(state=...)`·`TextFieldState`(#10; `pythonx.compose.foundation.text.input`, 가짜 호스트로 테스트됨, 노트북의 `text_state=`/`padding=` 는 `state=`/`modifier=Modifier.padding(8)`; IME 조합 증거는 python-multiplatform E2E #26) | +| 부분 | 선언형 앱 루트 `@app`·`state`·`app_root`(#11; 로직은 테스트됨, 바인더 경로는 #38 모양의 가짜 호스트로 테스트됨, 실제 Compose 증거는 python-multiplatform 노트북 E2E #26 에서 통과; 숫자·문자열 상태는 python-multiplatform #69 부터 왕복됨), 배포 설정, Material 3 위젯(렌더 증거는 python-multiplatform 노트북 E2E 에만, 설치한 wheel 에서 Kotlin 과 픽셀 일치), `Icon`(import 만 됨)·`DefaultIcons`(`Icons.Default` alias, 가짜 호스트로만 테스트됨, 스텁에서는 `DefaultIcons.Add` 가 `ImageVector`; `DefaultIcons.Add` 로 쓴다; 노트북의 `DefaultIcons.Add()` 가 아님), `TextField(state=...)`·`TextFieldState`(#10; `pythonx.compose.foundation.text.input`, 가짜 호스트로 테스트됨, 노트북의 `text_state=`/`padding=` 는 `state=`/`modifier=Modifier.padding(8)`; IME 조합 증거는 python-multiplatform 노트북 E2E #26 에서 통과) | | 계획 | `remember_saveable`·색 스킴·코루틴 스코프 | ### 테스트 기준선 @@ -61,7 +61,7 @@ GitHub 마일스톤과 같은 내용이다. 날짜는 2026-10-03 에 정했고, 날짜 근거: - **M1 (2주):** 바인더 계약(`inspect.signature`, `python_multiplatform.describe`)이 PythonMultiplatform `d00f413f` 에 착지했다. 남은 일은 이 저장소의 순수 Python 과 가짜 호스트 하네스뿐이라 JVM 빌드가 필요 없다. -- **M2 (M1 뒤 3주):** 위젯마다 JVM 렌더 테스트가 필요하다. 처음엔 Gradle 캐시 링크가 끊겼다고 보고 4주로 잡았으나, 캐시는 `PythonMultiplatform/.caches` 로 다시 걸려 Compose 픽스처가 통과하고 있었고, 노트북 관련 질문도 INTENT §5 로 결정됐다. 남은 위험은 `TextField(state=...)` 의 IME 조합 증거(python-multiplatform E2E #26)와, 앱 루트 교체를 Kotlin 컴포지션이 관찰하는 진입점이다. 진입점은 바인더 몫으로 정해졌다(2026-10-04): python-multiplatform 의 `PythonAppView` 가 `app_root` State 를 컴포지션 안에서 읽고, #26 진단에서 다시 선언한 루트가 실제로 다시 그려짐을 확인했다. +- **M2 (M1 뒤 3주):** 위젯마다 JVM 렌더 테스트가 필요하다. 처음엔 Gradle 캐시 링크가 끊겼다고 보고 4주로 잡았으나, 캐시는 `PythonMultiplatform/.caches` 로 다시 걸려 Compose 픽스처가 통과하고 있었고, 노트북 관련 질문도 INTENT §5 로 결정됐다. 남은 위험이던 `TextField(state=...)` 의 IME 조합 증거와 앱 루트 교체 진입점은 python-multiplatform 노트북 E2E 로 해소됐다(#26, develop `844cf291`; 2026-10-04 에 이 패키지의 `develop` wheel 로 20개 중 20개 통과, 아직 CI 는 돌리지 않음). 그래서 #9·#10·#11·#19 를 닫았다. 진입점은 바인더 몫으로 정해졌다(2026-10-04): python-multiplatform 의 `PythonAppView` 가 `app_root` State 를 컴포지션 안에서 읽고, #26 진단에서 다시 선언한 루트가 실제로 다시 그려짐을 확인했다. - **M3 (M2 뒤 2주):** 스텁은 M1 규칙만 있으면 만들 수 있어 M2 와 겹쳐 진행할 수 있다. 다만 노트북 위젯의 최종 이름이 M2 에서 정해지므로 마감은 M2 뒤로 둔다. ## 4. 구조 diff --git a/docs/SPEC.md b/docs/SPEC.md index 2f3112e..0f309f0 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -284,7 +284,7 @@ job. Tests: `tests/test_runtime_module.py::TheRuntimeSeam` (4 tests) and `::TheChaquopyMechanismIsGone` (3 tests), importing `pythonx.compose.runtime` the ordinary way. -### S5.4 The app root, `app` and `state`: `partial` (the binder path is tested against a fake host shaped after python-multiplatform #38; real Compose is the E2E module, issue #19) +### S5.4 The app root, `app` and `state`: `partial` (the binder path is tested here against a fake host shaped after python-multiplatform #38; the real-Compose evidence is outside this repository, in the notebook E2E module) The host draws with `PythonAppView(module = "pythonx.compose.runtime", attribute = "app_root")` (python-multiplatform #18 and #105; issue #11). The binder owns the redraw: `PythonAppView` reads @@ -315,7 +315,10 @@ reason. `pythonx` does none of this itself. Tests: `tests/test_app_root.py::TheL real `_new_state` through the binder's Python layer and `tests/fake_host.py`, whose `mutableStateOf` and `MutableState.value` rows are shaped after #38 (`KotlinSurface.kt`, `PythonxAdapter.kt`), not walked from a jar. It skips against a binder older than #38. That Compose observes a write and -recomposes is not shown here; it is the E2E module (python-multiplatform #26, issue #19). The stub carries `app`, +recomposes is not shown here. It is shown in python-multiplatform `ksp-fixtures/notebook-e2e` (#26, develop `844cf291`; 20 of 20 passed against this package's `develop` wheel on 2026-10-04, not run here, and no CI workflow runs it yet): `NotebookRootTest` declares the root, shares one state between the +notebook and the screen, and redeclares the root with `@app`, which reaches the screen within four +frames with no update call; with `@app` stubbed out, or the host handed the root once, the screen +does not change. The stub carries `app`, `state` and `app_root` (`tests/test_stubs.py`). ### S5.2 Material 3 composables reach Python without per-widget wrappers: `partial` @@ -331,8 +334,10 @@ toggle family, `TextField`, `Checkbox` and `Switch` are meant to be reached thro (commits `a6742a1c`, `3fde8bd6`, `cac8243f`, cited in `pythonx/compose/material3/__init__.py`) and was not re-run for this document. - `pythonx.compose.material3` re-exports `androidx.compose.material3` by the §3 rule, but the fake - host binds no material3 declaration, so no test here exercises one; the render proof per widget - is issue #9. + host binds no material3 declaration, so no test here exercises one. +- The render proof per widget is in python-multiplatform `ksp-fixtures/notebook-e2e` (#26, develop `844cf291`; 20 of 20 passed against this package's `develop` wheel on 2026-10-04, not run here, and no CI workflow runs it yet): `NotebookPracticeTest` (13 tests) draws `Text`, + `Button`, `Card`, `Icon`, `Column`, `Row` (with an `Arrangement` and an `Alignment.Horizontal`) + and `Spacer` from the installed wheel, each pixel-equal to the same composables drawn from Kotlin. ### S5.3 `DefaultIcons`, `Icon` and colour schemes: `partial` @@ -346,7 +351,7 @@ the notebook's `DefaultIcons.Add()` would call an `ImageVector`, which is not ca says the intent is `DefaultIcons.Add`. The rows are shaped after python-multiplatform `31c092f0` (`_PROPERTIES`, `_read_property`) and the ecosystem report for #37; no walked `material-icons-core` row exists here, and `Icon(DefaultIcons.Add, ...)` is drawn only in -python-multiplatform's render test. Tests: `tests/test_chain.py::DefaultIconsAlias`; stubs: the +python-multiplatform's render test and its notebook E2E (cells 28 to 30). Tests: `tests/test_chain.py::DefaultIconsAlias`; stubs: the `material3` stub assigns `DefaultIcons = Icons.Default` (typed `Icons.Filled`; `Add` is an `ImageVector`, python-multiplatform #68, whose package `androidx.compose.ui.graphics.vector` is the manifest row `pythonx.compose.ui.graphics.vector`), checked by `tests/test_typing.py::test_default_icons_imports_from_material3` and `test_a_default_icon_is_an_image_vector` @@ -391,7 +396,10 @@ Status `partial`: this is wired and tested against the fake host, whose rows are python-multiplatform `26485a02` (#73), not a walked jar (`tests/fake_host.py` says which fields). The proof that matters, typing a composing input-method sequence with no Python callback per keystroke and the field's text asserted afterwards, is python-multiplatform's E2E test (#26), judged over up to -four frames; it is not run here. Not modelled: a function-typed slot (`label`, `on_value_change`), which needs +four frames. It passes there (`NotebookTextFieldTest`; python-multiplatform `ksp-fixtures/notebook-e2e` (#26, develop `844cf291`; 20 of 20 passed against this package's `develop` wheel on 2026-10-04, not run here, and no CI workflow runs it yet)): a Hangul sequence +keeps its composing range in Compose, no Python function starts while it types, and the notebook +reads the committed text. Not exercised there: the AWT layer that decodes an `InputMethodEvent`, +which needs a window. Not modelled here: a function-typed slot (`label`, `on_value_change`), which needs the binder's `NewFunction` rows. ## 6. Modifiers: extension functions as methods @@ -532,10 +540,10 @@ the binder lists no member that would win over these groups. | `modifier` | lower-case instance from `pythonx.compose.ui` | INTENT §5.4: not provided; written `Modifier` | | `color=0xFFFF0000` | ARGB integer for a colour | INTENT §5.5: not accepted; written `Color(0xFFFF0000)` | | `Spacer(start=..., top=...)` | spacing parameters | INTENT §5.6: not supported; written `Spacer(modifier=Modifier.padding(...))` | -| `Column`, `Row`, `Spacer` | imported from `pythonx.compose.material3` | INTENT §5.2: served from both, by the manifest's `[aliases]` (`tests/test_chain.py::ManifestAliases`); render proof pending (#9) | -| `TextField(text_state=..., padding=8)` | state object and spacing parameter | INTENT §5.8: written `TextField(state=..., modifier=Modifier.padding(8))`; S5.5, `partial` (#10) | +| `Column`, `Row`, `Spacer` | imported from `pythonx.compose.material3` | INTENT §5.2: served from both, by the manifest's `[aliases]` (`tests/test_chain.py::ManifestAliases`); rendered from the installed wheel in python-multiplatform's notebook E2E (#9, S5.2) | +| `TextField(text_state=..., padding=8)` | state object and spacing parameter | INTENT §5.8: written `TextField(state=..., modifier=Modifier.padding(8))`; S5.5, `partial`: IME evidence only in python-multiplatform's notebook E2E (#10) | | `Card`, `Button`, `Text`, `TextField` | as above | S5.2 | -| `main.App`, `App.update(...)` | live screen replacement from a cell | INTENT §5.1: `@app` declares the root, a redeclaration replaces it, no update function (S5.4, `partial`, issue #11) | +| `main.App`, `App.update(...)` | live screen replacement from a cell | INTENT §5.1: `@app` declares the root, a redeclaration replaces it, no update function (S5.4, `partial`: real-Compose evidence only in python-multiplatform's notebook E2E, #11) | ## 9. Repository hygiene (not behaviour)