Skip to content

Commit 4ecdef1

Browse files
authored
Merge pull request #63 from thisisthepy/feat/first-release
Feat: Prepare the first PyPI release, 0.1.0a1, published by tag through trusted publishing
2 parents 8e16c28 + d2b9202 commit 4ecdef1

13 files changed

Lines changed: 232 additions & 55 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env python3
2+
"""Fail unless a release tag is `v` + the version in `pyproject.toml`.
3+
4+
python3 .github/scripts/check_release_version.py "$GITHUB_REF_NAME" # exit 0 = match, 1 = not
5+
6+
`publish-pypi.yml` runs this before building, so a tag cannot publish a version it does not name.
7+
A full ref (`refs/tags/v0.1.0a1`) is accepted too. Standard library only.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import sys
13+
import tomllib
14+
from pathlib import Path
15+
16+
REPO = Path(__file__).resolve().parents[2]
17+
18+
19+
def main(argv: list[str]) -> int:
20+
if len(argv) != 2:
21+
print("usage: check_release_version.py <tag>", file=sys.stderr)
22+
return 2
23+
tag = argv[1].removeprefix("refs/tags/")
24+
with open(REPO / "pyproject.toml", "rb") as f:
25+
version = tomllib.load(f)["project"]["version"]
26+
expected = f"v{version}"
27+
if tag != expected:
28+
print(f"error: tag {tag!r} does not match pyproject.toml, which expects {expected!r}.\n"
29+
f"Fix the version in pyproject.toml or tag the right commit.", file=sys.stderr)
30+
return 1
31+
print(f"ok: tag {tag} matches pyproject.toml version {version}")
32+
return 0
33+
34+
35+
if __name__ == "__main__":
36+
sys.exit(main(sys.argv))

‎.github/workflows/publish-pypi.yml‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Publish to PyPI
2+
3+
# A pushed `v*` tag releases that commit. The tag must be `v` + the version in pyproject.toml. The
4+
# upload uses PyPI trusted publishing: no token is stored anywhere; PyPI trusts this workflow and
5+
# the `pypi` environment, whose deployment policy allows `v*` tags only.
6+
on:
7+
push:
8+
tags: ['v*']
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
build:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
with:
19+
fetch-depth: 1
20+
- uses: actions/setup-python@v5
21+
with:
22+
python-version: '3.13'
23+
- run: python -m pip install build twine
24+
- name: Check the tag against pyproject.toml
25+
run: python .github/scripts/check_release_version.py "$GITHUB_REF_NAME"
26+
- name: Build the sdist and the wheel
27+
run: python -m build
28+
- run: twine check dist/*
29+
# Importing needs no binder: names resolve lazily, on first use.
30+
- name: Smoke-install the wheel in a fresh venv
31+
run: |
32+
python -m venv .smoke
33+
.smoke/bin/pip install dist/*.whl
34+
.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()"
35+
- uses: actions/upload-artifact@v4
36+
with:
37+
name: dist
38+
path: dist/
39+
if-no-files-found: error
40+
41+
publish:
42+
needs: build
43+
runs-on: ubuntu-latest
44+
environment:
45+
name: pypi
46+
url: https://pypi.org/p/pythonx-compose
47+
permissions:
48+
id-token: write
49+
steps:
50+
- uses: actions/download-artifact@v4
51+
with:
52+
name: dist
53+
path: dist/
54+
- uses: pypa/gh-action-pypi-publish@release/v1
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ permissions:
1818
contents: read
1919

2020
concurrency:
21-
group: tests-${{ github.ref }}
21+
group: test-${{ github.ref }}
2222
cancel-in-progress: true
2323

2424
jobs:

‎AGENTS.md‎

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

223223
- The distribution name is `pythonx-compose` (`pyproject.toml`); the import package is
224224
`pythonx.compose`.
225+
- **Release.** After the maintainer approves, tag the commit to release `v<version>` (the
226+
`pyproject.toml` version, e.g. `v0.1.0a1`) and push the tag; `.github/workflows/publish-pypi.yml`
227+
builds, checks and publishes through PyPI trusted publishing (environment `pypi`). Never upload
228+
by hand, and never store a PyPI token.
225229
- `.pyi` stubs, `py.typed` and the manifest must end up **inside the wheel**. A `package-data`
226230
pattern is not proof — build the wheel and list it before claiming something ships.
227231

‎PROJECT.md‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ Compose(`androidx.compose.*`)를 파이썬에서 쓰기 위한 pip 패키지 **`
3535

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

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

125135
- **선언형 앱 루트, 갱신 함수 없음.** 노트북의 `main.App.update(...)`·`getValue()`/`setValue()` 는

‎README.md‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
English | [한국어](docs/locale/README_ko.md)
1+
English | [한국어](https://github.com/thisisthepy/pythonx-compose/blob/main/docs/locale/README_ko.md)
22

33
<div align="center">
44

55
# pythonx-compose
66

77
**Write Compose user interfaces in Python — with Compose's own widgets, spelled the Python way.**
88

9-
[![License: MIT](https://img.shields.io/badge/license-MIT-7c4dff.svg)](LICENSE)
10-
[![Python](https://img.shields.io/badge/python-3.11%2B-7c4dff.svg)](pyproject.toml)
11-
[![pip](https://img.shields.io/badge/pip-pythonx--compose-7c4dff.svg)](pyproject.toml)
9+
[![License: MIT](https://img.shields.io/badge/license-MIT-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/LICENSE)
10+
[![Python](https://img.shields.io/badge/python-3.11%2B-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/pyproject.toml)
11+
[![pip](https://img.shields.io/badge/pip-pythonx--compose-7c4dff.svg)](https://github.com/thisisthepy/pythonx-compose/blob/main/pyproject.toml)
1212
[![Status](https://img.shields.io/badge/status-pre--alpha-lightgrey.svg)](#-status)
1313

14-
[Guide](docs/guide/index.html) · [Getting started](docs/guide/getting-started.html) · [Concepts](docs/guide/concepts.html) · [Status](docs/guide/status.html)
14+
[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)
1515

1616
</div>
1717

@@ -56,7 +56,7 @@ proofs are still pending. See [Status](#-status).</sub>
5656
- **`@Composable` stays.** Screens are decorated Python functions.
5757
- **A real package.** `pythonx/` is ordinary Python source that imports `androidx.compose.*` and
5858
reshapes it. The binder never renames anything — renaming is this package's job.
59-
- **One manifest.** [`pythonx-map.toml`](pythonx/compose/pythonx-map.toml) says which `pythonx.compose.*` module
59+
- **One manifest.** [`pythonx-map.toml`](https://github.com/thisisthepy/pythonx-compose/blob/main/pythonx/compose/pythonx-map.toml) says which `pythonx.compose.*` module
6060
stands for which Kotlin package. It lives inside the package and ships in the wheel. The runtime
6161
and the `.pyi` generator read the same file, so what your editor completes is what the interpreter
6262
resolves.
@@ -86,7 +86,10 @@ Each module is a real file whose `__init__.py` calls one re-export rule; names r
8686
## 🚀 Quick start
8787

8888
> [!NOTE]
89-
> `pythonx-compose` is **pre-alpha** and not yet published to PyPI.
89+
> `pythonx-compose` is **pre-alpha**. The first alpha, `0.1.0a1`, is released to PyPI when the
90+
> maintainer tags it; a pre-release needs `pip install --pre pythonx-compose`.
91+
92+
From a clone, to run the tests:
9093

9194
```bash
9295
git clone https://github.com/thisisthepy/pythonx-compose
@@ -164,12 +167,12 @@ standalone desktop toolkit.
164167
| Colour schemes | ⏳ planned |
165168
| `remember_saveable`, coroutine scopes | ⏳ planned |
166169

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

169172
## 📖 Documentation
170173

171-
- **Guide** — [`docs/guide/`](docs/guide/index.html), bilingual (English / 한국어)
172-
- **Korean README** — [`docs/locale/README_ko.md`](docs/locale/README_ko.md)
174+
- **Guide** — [`docs/guide/`](https://thisisthepy.github.io/pythonx-compose/), bilingual (English / 한국어)
175+
- **Korean README** — [`docs/locale/README_ko.md`](https://github.com/thisisthepy/pythonx-compose/blob/main/docs/locale/README_ko.md)
173176

174177
## 🔌 Ecosystem
175178

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

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

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

198201
## License
199202

200-
[MIT](LICENSE) © 2023–2024 BREW (b-re-w), Jong-uk Lee (rnoro5122)
203+
[MIT](https://github.com/thisisthepy/pythonx-compose/blob/main/LICENSE) © 2023–2024 BREW (b-re-w), Jong-uk Lee (rnoro5122)

‎docs/SPEC.md‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ Run from a worktree with `python -m pytest tests -q -rs` (pytest 8, mypy 2.4, CP
2121

2222
| Environment | Result |
2323
|---|---|
24-
| No `PythonMultiplatform` checkout found | **133 passed, 112 skipped**, 167 subtests passed |
25-
| 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 |
24+
| No `PythonMultiplatform` checkout found | **139 passed, 112 skipped**, 167 subtests passed |
25+
| 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 |
2626
| an older checkout, without `describe_member` (python-multiplatform #54) | the same, with 7 more skipped |
2727
| an older one, without `describe(module, name)` (python-multiplatform #36) | with 13 more skipped again |
2828
| 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
3636
`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
3737
pass.
3838

39-
Of the 133 that pass without a checkout, 73 check the type stubs and the wheel (S1.2); most of the
39+
Of the 139 that pass without a checkout, 73 check the type stubs and the wheel (S1.2); most of the
4040
rest assert **absence** (a retired token, a deleted file, a docstring that exists). Those are listed
4141
in §9 and are not counted as features.
4242

@@ -46,13 +46,22 @@ in §9 and are not counted as features.
4646

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

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

5353
- Shipped and tested: `tests/test_wheel.py` builds the wheel from a copy of the sources and finds
5454
`pythonx/compose/pythonx-map.toml`, the re-export rule, and an `__init__.py` for every mapped
5555
module (issue #13). The manifest lives inside the package directory so `package-data` carries it.
56+
- PyPI metadata: description, `README.md` as the long description (its links are absolute, since
57+
pypi.org cannot resolve relative ones), `license = "MIT"` with `LICENSE`, the two authors as
58+
`LICENSE` names them (no emails), keywords, classifiers (Alpha, Python 3.11-3.13, Typed) and
59+
project URLs. `build-system.requires` is `setuptools>=77`, which understands that license form.
60+
- Released by pushing a tag `v<version>` (`.github/workflows/publish-pypi.yml`): the workflow checks
61+
the tag equals `v` + the `pyproject.toml` version (`.github/scripts/check_release_version.py`,
62+
`tests/test_release_version.py`), builds the sdist and wheel, runs `twine check`, smoke-installs
63+
the wheel in a fresh venv, then uploads through PyPI trusted publishing (environment `pypi`, no
64+
token). Nothing is uploaded by hand.
5665
- Typed: the wheel carries `py.typed` and the generated `.pyi` stubs (S1.2, issue #12).
5766

5867
### S1.2 Type stubs ship in the wheel — `implemented`

‎docs/guide/getting-started.html‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
<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>
3131

3232
<div class="wrap doc narrow">
33-
<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>
33+
<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>
3434

3535
<section class="flush">
3636
<h2><span data-lang="en">1. Get the source</span><span data-lang="ko">1. 소스 받기</span></h2>
@@ -43,9 +43,9 @@ <h2><span data-lang="en">1. Get the source</span><span data-lang="ko">1. 소스
4343
<section>
4444
<h2><span data-lang="en">2. Run the tests</span><span data-lang="ko">2. 테스트 실행</span></h2>
4545
<pre><code class="language-bash">python3 -m pytest tests -q</code></pre>
46-
<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>
46+
<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>
4747
<pre><code class="language-bash">PYTHONMULTIPLATFORM_HOME=/path/to/python-multiplatform python3 -m pytest tests -q</code></pre>
48-
<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>
48+
<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>
4949
</section>
5050

5151
<section>

0 commit comments

Comments
 (0)