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
36 changes: 36 additions & 0 deletions .github/scripts/check_release_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Fail unless a release tag is `v` + the version in `pyproject.toml`.

python3 .github/scripts/check_release_version.py "$GITHUB_REF_NAME" # exit 0 = match, 1 = not

`publish-pypi.yml` runs this before building, so a tag cannot publish a version it does not name.
A full ref (`refs/tags/v0.1.0a1`) is accepted too. Standard library only.
"""

from __future__ import annotations

import sys
import tomllib
from pathlib import Path

REPO = Path(__file__).resolve().parents[2]


def main(argv: list[str]) -> int:
if len(argv) != 2:
print("usage: check_release_version.py <tag>", file=sys.stderr)
return 2
tag = argv[1].removeprefix("refs/tags/")
with open(REPO / "pyproject.toml", "rb") as f:
version = tomllib.load(f)["project"]["version"]
expected = f"v{version}"
if tag != expected:
print(f"error: tag {tag!r} does not match pyproject.toml, which expects {expected!r}.\n"
f"Fix the version in pyproject.toml or tag the right commit.", file=sys.stderr)
return 1
print(f"ok: tag {tag} matches pyproject.toml version {version}")
return 0


if __name__ == "__main__":
sys.exit(main(sys.argv))
54 changes: 54 additions & 0 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Publish to PyPI

# A pushed `v*` tag releases that commit. The tag must be `v` + the version in pyproject.toml. The
# upload uses PyPI trusted publishing: no token is stored anywhere; PyPI trusts this workflow and
# the `pypi` environment, whose deployment policy allows `v*` tags only.
on:
push:
tags: ['v*']

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: actions/setup-python@v5
with:
python-version: '3.13'
- run: python -m pip install build twine
- name: Check the tag against pyproject.toml
run: python .github/scripts/check_release_version.py "$GITHUB_REF_NAME"
- name: Build the sdist and the wheel
run: python -m build
- run: twine check dist/*
# Importing needs no binder: names resolve lazily, on first use.
- name: Smoke-install the wheel in a fresh venv
run: |
python -m venv .smoke
.smoke/bin/pip install dist/*.whl
.smoke/bin/python -c "import pythonx.compose, pythonx.compose.ui, pythonx.compose.material3; import importlib.resources as r; assert r.files('pythonx.compose').joinpath('pythonx-map.toml').is_file()"
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
if-no-files-found: error

publish:
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/pythonx-compose
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml → .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ permissions:
contents: read

concurrency:
group: tests-${{ github.ref }}
group: test-${{ github.ref }}
cancel-in-progress: true

jobs:
Expand Down
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ worktree so a test can see it. Read it in place.
feature it needs (the member resolver, `ba4c6f49`; `describe(module, name)`, #36; see
`docs/SPEC.md` §0). Report both numbers and say which environment you ran in. A skip is not
a pass.
- The suite runs on GitHub Actions (`.github/workflows/tests.yml`) in both environments, without and
- The suite runs on GitHub Actions (`.github/workflows/test.yml`) in both environments, without and
with a python-multiplatform checkout, for every pull request into `develop`. Local runs are for
quick checks of the part you changed; leave the full two-environment run to CI.
It also runs daily against python-multiplatform `develop` as it is that day; a failed daily run
Expand All @@ -222,6 +222,10 @@ worktree so a test can see it. Read it in place.

- The distribution name is `pythonx-compose` (`pyproject.toml`); the import package is
`pythonx.compose`.
- **Release.** After the maintainer approves, tag the commit to release `v<version>` (the
`pyproject.toml` version, e.g. `v0.1.0a1`) and push the tag; `.github/workflows/publish-pypi.yml`
builds, checks and publishes through PyPI trusted publishing (environment `pypi`). 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.

Expand Down
14 changes: 12 additions & 2 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **`

```
python3 -m pytest tests -q
PythonMultiplatform 체크아웃 없음 133 passed, 112 skipped
python-multiplatform develop 31c092f0+ 244 passed, 1 skipped
PythonMultiplatform 체크아웃 없음 139 passed, 112 skipped
python-multiplatform develop 31c092f0+ 250 passed, 1 skipped
describe_member 없는 체크아웃 같은 수, 그중 7 개 건너뜀 (python-multiplatform #54 이전)
describe(module, name) 없는 체크아웃 같은 수, 그중 13 개 건너뜀 (python-multiplatform #36 이전)
그보다 오래된 체크아웃 거기에 5 개 더 건너뜀 (member resolver 없음)
Expand Down Expand Up @@ -120,6 +120,16 @@ python3 .github/scripts/check_guide.py # 가이드: HTML 파싱, 링
태그 `archive/pre-restructure` 로 되찾을 수 있다. `tools/` 는 없앴다: 스텁 생성기는
`pythonx/_build/`, 가이드 검사기와 릴리스 스크립트는 `.github/scripts/`.

### 결정됨 (2026-10-03, #62) — 첫 PyPI 릴리스

- 워크플로 이름: `tests.yml` → `test.yml`, PyPI 배포는 `publish-pypi.yml`.
- 첫 버전은 `0.1.0a1`(알파, 프리릴리스). PyPI 메타데이터(설명·README·MIT·저자·URL·분류자)는 채웠고,
README 의 링크는 pypi.org 에서 열리도록 절대 URL 이다(`main` 링크는 `main` 이 `develop` 을 따라잡은 뒤에 열린다).
- 배포 방법: 릴리스할 커밋에 `v0.1.0a1` 태그를 푸시하면 `publish-pypi.yml` 이 태그와 `pyproject.toml`
버전을 대조하고, sdist·wheel 을 빌드하고, `twine check`·새 venv 스모크를 거쳐 트러스티드 퍼블리싱으로
올린다. **현황: 업로드 전.** 태그 푸시는 메인테이너 승인을 기다리고, GitHub 환경 `pypi`(`v*` 태그만 허용)
와 PyPI pending publisher 는 메인테이너가 설정한다.

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

- **선언형 앱 루트, 갱신 함수 없음.** 노트북의 `main.App.update(...)`·`getValue()`/`setValue()` 는
Expand Down
27 changes: 15 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
English | [한국어](docs/locale/README_ko.md)
English | [한국어](https://github.com/thisisthepy/pythonx-compose/blob/main/docs/locale/README_ko.md)

<div align="center">

# pythonx-compose

**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)](LICENSE)
[![Python](https://img.shields.io/badge/python-3.11%2B-7c4dff.svg)](pyproject.toml)
[![pip](https://img.shields.io/badge/pip-pythonx--compose-7c4dff.svg)](pyproject.toml)
[![License: MIT](https://img.shields.io/badge/license-MIT-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)

[Guide](docs/guide/index.html) · [Getting started](docs/guide/getting-started.html) · [Concepts](docs/guide/concepts.html) · [Status](docs/guide/status.html)
[Guide](https://thisisthepy.github.io/pythonx-compose/) · [Getting started](https://thisisthepy.github.io/pythonx-compose/getting-started.html) · [Concepts](https://thisisthepy.github.io/pythonx-compose/concepts.html) · [Status](https://thisisthepy.github.io/pythonx-compose/status.html)

</div>

Expand Down Expand Up @@ -56,7 +56,7 @@ proofs are still pending. See [Status](#-status).</sub>
- **`@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.
- **One manifest.** [`pythonx-map.toml`](pythonx/compose/pythonx-map.toml) says which `pythonx.compose.*` module
- **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
resolves.
Expand Down Expand Up @@ -86,7 +86,10 @@ Each module is a real file whose `__init__.py` calls one re-export rule; names r
## 🚀 Quick start

> [!NOTE]
> `pythonx-compose` is **pre-alpha** and not yet published to PyPI.
> `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`.

From a clone, to run the tests:

```bash
git clone https://github.com/thisisthepy/pythonx-compose
Expand Down Expand Up @@ -164,12 +167,12 @@ standalone desktop toolkit.
| Colour schemes | ⏳ planned |
| `remember_saveable`, coroutine scopes | ⏳ planned |

The full list is on the guide's [Status page](docs/guide/status.html).
The full list is on the guide's [Status page](https://thisisthepy.github.io/pythonx-compose/status.html).

## 📖 Documentation

- **Guide** — [`docs/guide/`](docs/guide/index.html), bilingual (English / 한국어)
- **Korean README** — [`docs/locale/README_ko.md`](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

Expand All @@ -185,7 +188,7 @@ The full list is on the guide's [Status page](docs/guide/status.html).
## 🤝 Contributing

Development is intent-first and test-first: a change starts as a specification change, then a
failing test, then code. The [guide](docs/guide/status.html) lists what is open and where help is
failing test, then code. The [guide](https://thisisthepy.github.io/pythonx-compose/status.html) lists what is open and where help is
useful. Please open an issue before large changes.

## Maintainers
Expand All @@ -197,4 +200,4 @@ useful. Please open an issue before large changes.

## License

[MIT](LICENSE) © 2023–2024 BREW (b-re-w), Jong-uk Lee (rnoro5122)
[MIT](https://github.com/thisisthepy/pythonx-compose/blob/main/LICENSE) © 2023–2024 BREW (b-re-w), Jong-uk Lee (rnoro5122)
17 changes: 13 additions & 4 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 | **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 |
| 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 |
| 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 133 that pass without a checkout, 73 check the type stubs and the wheel (S1.2); most of the
Of the 139 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 All @@ -46,13 +46,22 @@ in §9 and are not counted as features.

### S1.1 Installed as `pythonx-compose`, importing `pythonx.compose` — `partial`

`pyproject.toml` declares `name = "pythonx-compose"`, version `0.0.1`, setuptools build,
`pyproject.toml` declares `name = "pythonx-compose"`, version `0.1.0a1`, setuptools build,
`requires-python >= 3.11` (the runtime reads the manifest with `tomllib`), and
`packages.find include = ["pythonx.compose*"]`.

- Shipped and tested: `tests/test_wheel.py` builds the wheel from a copy of the sources and finds
`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
`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.
- Released by pushing a tag `v<version>` (`.github/workflows/publish-pypi.yml`): the workflow checks
the tag equals `v` + the `pyproject.toml` version (`.github/scripts/check_release_version.py`,
`tests/test_release_version.py`), builds the sdist and wheel, runs `twine check`, smoke-installs
the wheel in a fresh venv, then uploads through PyPI trusted publishing (environment `pypi`, no
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`
Expand Down
6 changes: 3 additions & 3 deletions docs/guide/getting-started.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<header class="page-head wrap"><p class="kicker"><span data-lang="en">Getting started</span><span data-lang="ko">시작하기</span></p><h1><span data-lang="en">From clone to first call</span><span data-lang="ko">클론부터 첫 호출까지</span></h1><p class="lead"><span data-lang="en">Five minutes, honest about what runs.</span><span data-lang="ko">5 분, 동작하는 범위를 솔직하게.</span></p></header>

<div class="wrap doc narrow">
<div class="note"><p data-lang="en">pythonx-compose is pre-alpha and not yet on PyPI. This page shows how to get the source, run the tests, and what you can call today.</p><p data-lang="ko">pythonx-compose 는 pre-alpha 이며 아직 PyPI 에 없습니다. 이 페이지는 소스를 받고, 테스트를 돌리고, 지금 호출할 수 있는 것을 보여줍니다.</p></div>
<div class="note"><p data-lang="en">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 <code>pip install --pre pythonx-compose</code>. This page shows how to get the source, run the tests, and what you can call today.</p><p data-lang="ko">pythonx-compose 는 pre-alpha 입니다. 첫 알파 0.1.0a1 은 메인테이너가 태그를 달면 PyPI 에 배포되며, 프리릴리스는 <code>pip install --pre pythonx-compose</code> 로 설치합니다. 이 페이지는 소스를 받고, 테스트를 돌리고, 지금 호출할 수 있는 것을 보여줍니다.</p></div>

<section class="flush">
<h2><span data-lang="en">1. Get the source</span><span data-lang="ko">1. 소스 받기</span></h2>
Expand All @@ -43,9 +43,9 @@ <h2><span data-lang="en">1. Get the source</span><span data-lang="ko">1. 소스
<section>
<h2><span data-lang="en">2. Run the tests</span><span data-lang="ko">2. 테스트 실행</span></h2>
<pre><code class="language-bash">python3 -m pytest tests -q</code></pre>
<p data-lang="en">Expect <strong>133 passed, 112 skipped</strong>. One skipped test reads the maintainer's <code>UI.ipynb</code>, which a clone does not have; mypy type-checks the stubs. The other skipped tests drive the binder's adaptation layer and need a <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> checkout beside this one, or:</p><p data-lang="ko"><strong>133 passed, 112 skipped</strong> 가 정상입니다. 건너뛴 테스트 하나는 클론에는 없는 메인테이너의 <code>UI.ipynb</code> 를 읽으며, mypy 는 스텁을 타입 검사합니다. 나머지 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> 체크아웃이 있거나 다음이 필요합니다:</p>
<p data-lang="en">Expect <strong>139 passed, 112 skipped</strong>. One skipped test reads the maintainer's <code>UI.ipynb</code>, which a clone does not have; mypy type-checks the stubs. The other skipped tests drive the binder's adaptation layer and need a <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> checkout beside this one, or:</p><p data-lang="ko"><strong>139 passed, 112 skipped</strong> 가 정상입니다. 건너뛴 테스트 하나는 클론에는 없는 메인테이너의 <code>UI.ipynb</code> 를 읽으며, mypy 는 스텁을 타입 검사합니다. 나머지 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> 체크아웃이 있거나 다음이 필요합니다:</p>
<pre><code class="language-bash">PYTHONMULTIPLATFORM_HOME=/path/to/python-multiplatform python3 -m pytest tests -q</code></pre>
<p data-lang="en">With a current one (<code>31c092f0</code> or later) <strong>244 pass</strong> and 1 skips (the <code>UI.ipynb</code> test); an older one skips 5 that call a snake_case method on a proxy the binder returned. That is expected and recorded.</p><p data-lang="ko">최신 체크아웃(<code>31c092f0</code> 이후)이면 <strong>230 개가 통과</strong>하고 1 개(<code>UI.ipynb</code> 테스트)를 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 <strong>건너뜁니다</strong>. 예상된 상태이며 기록되어 있습니다.</p>
<p data-lang="en">With a current one (<code>31c092f0</code> or later) <strong>250 pass</strong> and 1 skips (the <code>UI.ipynb</code> test); an older one skips 5 that call a snake_case method on a proxy the binder returned. That is expected and recorded.</p><p data-lang="ko">최신 체크아웃(<code>31c092f0</code> 이후)이면 <strong>230 개가 통과</strong>하고 1 개(<code>UI.ipynb</code> 테스트)를 건너뛰며, 오래된 체크아웃은 바인더가 돌려준 proxy 의 snake_case 메서드를 부르는 5 개를 <strong>건너뜁니다</strong>. 예상된 상태이며 기록되어 있습니다.</p>
</section>

<section>
Expand Down
Loading
Loading