Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions tools/check_guide.py → .github/scripts/check_guide.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
is code, a diagram, a proper name in `NEUTRAL_TEXT`, or inside an element marked `translate="no"`;
* a page loads an external resource other than Google Fonts.

python3 tools/check_guide.py # exit status 0 = clean, 1 = findings printed
python3 .github/scripts/check_guide.py # exit status 0 = clean, 1 = findings printed

Standard library only, so it runs anywhere the repository's tests run.
"""
Expand All @@ -24,7 +24,7 @@
from pathlib import Path
from urllib.parse import urlparse

REPO = Path(__file__).resolve().parents[1]
REPO = Path(__file__).resolve().parents[2]
GUIDE = REPO / "docs" / "guide"

VOID = {
Expand Down
14 changes: 7 additions & 7 deletions tools/release/README.md → .github/scripts/release/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ between runs, and any existing `release` (e.g. a stale one from an older workflo
ignored and overwritten.

1. Every push to `develop` triggers `.github/workflows/release-sync.yml`, which runs
`tools/release/sync-release.sh --push`. It rebuilds `release` from scratch as exactly
`.github/scripts/release/sync-release.sh --push`. It rebuilds `release` from scratch as exactly
one commit (`Release: sync from develop <sha>`) whose parent is that develop commit and
whose tree is the develop tree minus the files below. This happens even when nothing
is dropped. It is a no-op only if `release` already has that tree and that parent.
Expand All @@ -33,18 +33,18 @@ Only Markdown is ever dropped.

## Local use

tools/release/sync-release.sh --dry-run # list paths that would be dropped
tools/release/sync-release.sh # regenerate local release branch
tools/release/sync-release.sh --push # also push to origin
tools/release/test-sync-release.sh # tests (throwaway repo in .tmp/)
.github/scripts/release/sync-release.sh --dry-run # list paths that would be dropped
.github/scripts/release/sync-release.sh # regenerate local release branch
.github/scripts/release/sync-release.sh --push # also push to origin
.github/scripts/release/test-sync-release.sh # tests (throwaway repo in .tmp/)

The script uses plumbing and a private index, so your working tree, index and HEAD are untouched.
It refuses to run while `release` is checked out.

## Protecting main

tools/release/protect-main.sh # print what would be applied
tools/release/protect-main.sh --apply # apply via gh api (needs admin)
.github/scripts/release/protect-main.sh # print what would be applied
.github/scripts/release/protect-main.sh --apply # apply via gh api (needs admin)

This requires a pull request, blocks force-push and deletion. Classic protection cannot
restrict a PR's source branch, so add the check `only-release-into-main` as required
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Tests for sync-release.sh. Builds a throwaway repo under <repo>/.tmp/.
set -u
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
ROOT="$(cd "$HERE/../../.." && pwd)"
SYNC="$HERE/sync-release.sh"
SCRATCH="$ROOT/.tmp/release-sync-test.$$"
FAILS=0; PASSES=0
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Not used as a base (release is regenerated); the script reads the
# remote sha itself for --force-with-lease.
bash tools/release/sync-release.sh --source "${GITHUB_SHA}" --push
bash .github/scripts/release/sync-release.sh --source "${GITHUB_SHA}" --push

- name: Open or update PR release -> main
env:
Expand All @@ -44,7 +44,7 @@ jobs:
echo "release already equals main; nothing to PR"; exit 0
fi
title="Release: merge release into main"
body="Automated sync of \`develop\` (${GITHUB_SHA::7}) into \`release\`. Markdown outside README.md and docs/ subfolders is removed by tools/release/sync-release.sh."
body="Automated sync of \`develop\` (${GITHUB_SHA::7}) into \`release\`. Markdown outside README.md and docs/ subfolders is removed by .github/scripts/release/sync-release.sh."
existing="$(gh pr list --head release --base main --state open --json number --jq '.[0].number // empty')"
if [ -n "$existing" ]; then
gh pr edit "$existing" --title "$title" --body "$body"
Expand Down
3 changes: 0 additions & 3 deletions .gitmodules

This file was deleted.

30 changes: 21 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ files — lives **inside this repository's root directory.**
| Worktrees | `.worktrees/<name>` (git-ignored) |
| Temporary files | `.tmp/` (git-ignored); delete when done |
| Benchmarks | `benchmarks/` |
| Developer tooling | `tools/` |
| CI scripts | `.github/scripts/` |
| Build tooling (stub generator) | `pythonx/_build/` (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
Expand All @@ -31,6 +32,17 @@ ask first.**

Writing to *another* repository is not an exception either. Do it only when told to work there.

### Do not add top-level folders

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/`.

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
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

A worktree is a full checkout. Copying large untracked artefacts (prebuilt runtimes, vendored trees,
Expand All @@ -39,7 +51,7 @@ build caches, model weights, `node_modules`) into every worktree is how 86 workt

- Create worktrees under `.worktrees/<name>`.
- **Symlink** large untracked directories from the main checkout instead of copying or rebuilding
them. If `tools/worktree-add.sh` exists, use it — it does the linking.
them. If a worktree-linking script exists under `.github/scripts/`, use it.
- Delete a worktree once its branch is merged: `git worktree remove .worktrees/<name>`.
- Periodically delete `build/` directories inside worktrees; they only grow.

Expand All @@ -54,7 +66,7 @@ build caches, model weights, `node_modules`) into every worktree is how 86 workt

`main` carries a reduced layout: of the Markdown files, only `README.md` stays at the repository
root, and `docs/` keeps only its subdirectories (no Markdown files directly under `docs/`).
CI runs `tools/release/sync-release.sh` (`.github/workflows/release-sync.yml`) to produce that layout; do not hand-edit `release` or `main`.
CI runs `.github/scripts/release/sync-release.sh` (`.github/workflows/release-sync.yml`) to produce that layout; do not hand-edit `release` or `main`.

Only `main`, `release` and `develop` stand. A feature branch is deleted when it merges, with its
local branch and worktree. Sweep periodically: delete every remote and local branch that
Expand Down Expand Up @@ -215,17 +227,17 @@ worktree so a test can see it. Read it in place.

## 16. Things you do not change without the maintainer

- **`test/`** — the 2023–2024 Kotlin Multiplatform sample. Do not delete, move, or modernise it;
its fate is an open decision (`PROJECT.md`).
- **`pythonx/compose/native`** — a git submodule. Do not update its pointer as a side effect.
- **`pythonx/compose/lite/`** — a retired 2024 JPype prototype with tracked binaries. Leave it until
the maintainer decides.
- **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.

## 17. Documentation layout

- `README.md` (English) and `docs/locale/README_ko.md` (Korean) say the same thing; change both.
- `docs/guide/` is the bilingual GitHub Pages site. Every visible string exists in English and
Korean. `python3 tools/check_guide.py` is its test; run it after any guide change.
Korean. `python3 .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`.
Expand Down
32 changes: 17 additions & 15 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **`

```
python3 -m pytest tests -q
PythonMultiplatform 체크아웃 없음 134 passed, 112 skipped
python-multiplatform develop 31c092f0+ 245 passed, 1 skipped
PythonMultiplatform 체크아웃 없음 133 passed, 112 skipped
python-multiplatform develop 31c092f0+ 244 passed, 1 skipped
describe_member 없는 체크아웃 같은 수, 그중 7 개 건너뜀 (python-multiplatform #54 이전)
describe(module, name) 없는 체크아웃 같은 수, 그중 13 개 건너뜀 (python-multiplatform #36 이전)
그보다 오래된 체크아웃 거기에 5 개 더 건너뜀 (member resolver 없음)
Expand Down Expand Up @@ -74,20 +74,19 @@ pythonx/compose/ import 패키지 (현재 대부분 독스트링만 있
ui/ modifier.py (빈 Modifier 등록 지점), alignment.py
layout/ arrangement.py
material3/ __init__.py 하나 (위젯은 모두 재노출 규칙으로)
lite/ 2024 JPype 프로토타입 (은퇴, 바이너리 97 개 추적 중)
native/ 서브모듈 → thisisthepy/swing-graalvm-demo
tests/ pytest (unittest 스타일)
test/ 2023–2024 Kotlin Multiplatform 샘플 (pycomposeui) — 처리 미정
pythonx/_build/ gen_stubs.py (스텁 생성기, wheel 에 싣지 않음)
tests/ pytest (unittest 스타일), 유일한 테스트 폴더
docs/INTENT.md docs/SPEC.md docs/locale/ docs/guide/
tools/check_guide.py 가이드 사이트 검사 (가이드의 테스트)
.github/workflows/ CI
.github/scripts/ check_guide.py (tests/test_guide.py 가 실행), release/ (CI 전용)
```

## 5. 빌드·테스트

```bash
python3 -m pytest tests -q # 파이썬 테스트
PYTHONMULTIPLATFORM_HOME=<checkout> python3 -m pytest tests -q # 바인더 계층 포함
python3 tools/check_guide.py # 가이드: HTML 파싱, 링크, en/ko 짝
python3 .github/scripts/check_guide.py # 가이드: HTML 파싱, 링크, en/ko 짝 (tests/test_guide.py 도 실행)
```

- pytest 가 없으면 `.tmp/` 아래에 가상환경을 만든다 (홈 디렉터리 금지).
Expand All @@ -107,17 +106,20 @@ python3 tools/check_guide.py # 가이드: HTML 파싱, 링

## 7. 열린 질문 (메인테이너 결정 필요)

1. **`test/` 디렉터리** — 2023–2024 Kotlin Multiplatform 샘플. 유지 / 이동 / 삭제 중 무엇인지.
결정 전까지 손대지 않는다.
2. **코루틴 스코프** — 노트북이 import 하는 `DefaultCoroutineScope`·`MainCoroutineScope`.
3. **`pythonx/compose/lite/release/`** 의 Windows 바이너리·jar 97 개와 빈 `material3/*.py` 28 개,
서브모듈 `native` 의 처리.
4. **`pythonx-map.toml` 의 이중 표기** — `pythonx.compose.layout` 과
1. **코루틴 스코프** — 노트북이 import 하는 `DefaultCoroutineScope`·`MainCoroutineScope`.
2. **`pythonx-map.toml` 의 이중 표기** — `pythonx.compose.layout` 과
`pythonx.compose.foundation.layout` 를 둘 다 유지할지.
5. **릴리스·Pages 활성화** — `tools/release/` 와 `.github/workflows/`(release-sync, main-source-guard,
3. **릴리스·Pages 활성화** — `.github/scripts/release/` 와 `.github/workflows/`(release-sync, main-source-guard,
pages)는 들어와 있다. 실제로 돌려면 원격 푸시, `RELEASE_PR_TOKEN` 시크릿, main 보호 적용이
필요하고, 셋 다 메인테이너 승인 사항이다.

### 결정됨 (2026-10-03, #60)

- **저장소 정리.** `test/`(2023–2024 샘플), `pythonx/compose/lite/`(은퇴한 JPype 프로토타입),
서브모듈 `pythonx/compose/native`(→ thisisthepy/swing-graalvm-demo @ 090f0190)는 삭제했고,
태그 `archive/pre-restructure` 로 되찾을 수 있다. `tools/` 는 없앴다: 스텁 생성기는
`pythonx/_build/`, 가이드 검사기와 릴리스 스크립트는 `.github/scripts/`.

### 결정됨 (2026-10-03, `docs/INTENT.md` §5)

- **선언형 앱 루트, 갱신 함수 없음.** 노트북의 `main.App.update(...)`·`getValue()`/`setValue()` 는
Expand Down
4 changes: 2 additions & 2 deletions docs/INTENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ 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. **The `test/` directory** — a 2023–2024 Kotlin Multiplatform sample (`pycomposeui`). Whether it
is kept, moved, or removed is the maintainer's decision.
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)

Expand Down
30 changes: 15 additions & 15 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | **134 passed, 112 skipped**, 173 subtests passed |
| python-multiplatform `develop` at `31c092f0` or later (property rows, #38; `describe_member`, #54; besides `add_member_resolver` and `describe(module, name)`) | **245 passed, 1 skipped**, 204 subtests passed |
| No `PythonMultiplatform` checkout found | **133 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)`) | **244 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 |
Expand All @@ -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 134 that pass without a checkout, 61 check the type stubs and the wheel (S1.2); most of the
Of the 133 that pass without a checkout, 73 check the type stubs and the wheel (S1.2); most of the
rest assert **absence** (a retired token, a deleted file, a docstring that exists). Those are listed
in §9 and are not counted as features.

Expand Down Expand Up @@ -65,9 +65,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 tools/gen_stubs.py <kotlin-stubs.zip or directory>` converts them;
per Kotlin package. `python3 pythonx/_build/gen_stubs.py <kotlin-stubs.zip or directory>` 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 `tools/gen_stubs.py`:
- **The rule is the runtime's** (`_reexport.py`), applied by `pythonx/_build/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
Expand Down Expand Up @@ -463,7 +463,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.** `tools/gen_stubs.py` emits each group as a class nested in the object's stub class,
- **Stubs.** `pythonx/_build/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
Expand Down Expand Up @@ -501,8 +501,8 @@ the binder lists no member that would win over these groups.

These tests pass and assert that retired 2024 mechanisms are gone. They are not features:

- `tests/test_legacy_modules.py` (6 tests): no chaquopy / JPype tokens or dead relative imports in
`layout/__init__.py`, `lite/*.py`, `test/main.py`, `ui/unit/__init__.py`,
- `tests/test_legacy_modules.py` (4 tests): no chaquopy / JPype tokens or dead relative imports in
`layout/__init__.py`, `ui/unit/__init__.py`,
`material3/__init__.py`; each retired module carries a real docstring.
- `tests/test_material3_module.py::TheDeadFilesAreGone` (2 tests): `material3/` holds only its
`__init__.py`, and `wrapper/` is gone (#31).
Expand All @@ -521,15 +521,15 @@ Found in the repository; not covered by `docs/INTENT.md`, or in conflict with it
`python_multiplatform` and `python_multiplatform.binding` from the binder's sources and imports
`pythonx` from disk. `test_chain.py` and `test_modifier_module.py` still use the old call and
are rewritten with issue #8.
3. **Prebuilt binaries inside the package directory.** `pythonx/compose/lite/release/` tracks 97
files, including a Windows `.exe`, `.dll` and Compose desktop jars from the 2024 JPype
prototype, alongside a Gradle project in `pythonx/compose/lite/`.
3. *(Resolved, #60.)* `pythonx/compose/lite/` (the retired 2024 JPype prototype, with 97 tracked
binaries) is deleted; the tag `archive/pre-restructure` keeps it.
4. **Two spellings for one module.** The manifest maps both `pythonx.compose.layout` and
`pythonx.compose.foundation.layout` (plus `pythonx.compose.foundation` and `pythonx.compose`)
"while the spelling settles".
5. *(Resolved, #31.)* `material3/icon.py` and `color_scheme.py`, dead reflection code, are deleted.
6. *(Resolved, INTENT §5.3, issue #9.)* Grouped alignment constants: both spellings are served (§7, S7.1).
7. The submodule `pythonx/compose/native` → `thisisthepy/swing-graalvm-demo`, which INTENT does not
mention. (The 29 empty `material3/*.py` files are deleted, #31.)
8. **The `test/` directory** — a 2023–2024 Kotlin Multiplatform sample (`pycomposeui`, chaquopy
era). Kept as-is pending the maintainer's decision (INTENT §4.2).
7. *(Resolved, #60.)* The submodule `pythonx/compose/native` → `thisisthepy/swing-graalvm-demo`
(pinned at `090f0190`) is removed; INTENT does not mention it. (The 29 empty `material3/*.py`
files are deleted, #31.)
8. *(Resolved, #60, INTENT §4.2.)* The `test/` directory, a 2023–2024 Kotlin Multiplatform sample
(`pycomposeui`, chaquopy era), is deleted; the tag `archive/pre-restructure` keeps it.
Loading
Loading