Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
50707b7
Feat: Delete the Modifier shell, and leave behind only the one fact t…
b-re-w Aug 15, 2026
a6ecbca
Feat: Delete the Text wrapper the binder made redundant, and inventor…
b-re-w Aug 16, 2026
7858d0a
Feat: Delete the five wrappers a render proof retired, and split icon…
b-re-w Aug 16, 2026
7d6c0a1
Feat: Replace the 2024 Composable hierarchy with the decorator, and f…
b-re-w Aug 16, 2026
0856d08
Refactor: Retire the text field wrapper, and stop the two unreachable…
b-re-w Aug 16, 2026
f7e21f8
Docs: Say what the ui package's import is for, and stop the readme cl…
b-re-w Aug 18, 2026
9ad3d21
Refactor: Remove the Chaquopy code these two files could never run, a…
b-re-w Aug 18, 2026
91e1714
Docs: Record that the layer now supplies these constants, and how the…
b-re-w Aug 18, 2026
0f51723
Refactor: Retire the rest of the 2024 tree, and make its replacement …
b-re-w Aug 19, 2026
ded9208
Docs: Name the distribution what it ships as, and say what goes in th…
b-re-w Aug 24, 2026
0f577b8
Feat: Ship the manifest this package owes, and stop depending on the …
b-re-w Aug 24, 2026
ab6c450
Revert: Untrack UI.ipynb, which was swept in by a `git add -A`
b-re-w Aug 24, 2026
f8bfad5
Docs: Restructure into intent, spec, agent rules and a bilingual guide
b-re-w Oct 1, 2026
f5ec437
Docs: Push every commit right away
b-re-w Oct 2, 2026
6ddd6d2
Fix: Let release-sync satisfy the main guard without a PAT
b-re-w Oct 2, 2026
2ec0fdf
Docs: Land every feature through an issue and a pull request
b-re-w Oct 2, 2026
4d0c2eb
Merge pull request #2 from thisisthepy/work/issue-pr-rule
b-re-w Oct 2, 2026
d0d293a
Fix: Let main always fast-forward to the generated release
b-re-w Oct 2, 2026
51f77b4
Merge pull request #3 from thisisthepy/work/release-main-parent
b-re-w Oct 2, 2026
ee04d50
Docs: Close the issue after the develop merge
b-re-w Oct 2, 2026
10e6be8
Merge pull request #4 from thisisthepy/work/close-issue-rule
b-re-w Oct 2, 2026
b152752
Docs: Read layout constants as attributes, the way the binder now han…
b-re-w Oct 2, 2026
3c1f760
Merge pull request #6 from thisisthepy/work/constant-spelling
b-re-w Oct 2, 2026
4081938
Docs: Record the milestones and the reasoning behind each date
b-re-w Oct 2, 2026
6b15709
Merge pull request #15 from thisisthepy/work/milestones
b-re-w Oct 2, 2026
e0d3cd5
Test: Import pythonx from disk, and install the binder under its own …
b-re-w Oct 2, 2026
c0bb33f
Merge pull request #16 from thisisthepy/work/real-pythonx
b-re-w Oct 2, 2026
a46896f
Docs: Record the notebook decisions, and move M2 a week earlier
b-re-w Oct 2, 2026
1d1c38b
Merge pull request #18 from thisisthepy/work/intent-decisions
b-re-w Oct 2, 2026
98529cb
Feat: Re-export every manifest module by one naming rule over the bin…
b-re-w Oct 2, 2026
aad44e1
Merge pull request #21 from thisisthepy/work/reexport
b-re-w Oct 2, 2026
4029c62
Feat: Ship the manifest inside the package, and prove it by building …
b-re-w Oct 2, 2026
ec62c17
Merge pull request #22 from thisisthepy/work/ship-manifest
b-re-w Oct 2, 2026
d3c7da2
Feat: Convert the binder's Kotlin-named stubs to pythonx stubs by the…
b-re-w Oct 2, 2026
20f791b
Docs: Give each baseline row its own subtest count
b-re-w Oct 2, 2026
cbd2d34
Merge pull request #23 from thisisthepy/work/stubs
b-re-w Oct 2, 2026
cfb7994
Feat: Reach snake_case methods on proxies the binder returns
b-re-w Oct 2, 2026
8e21018
Merge pull request #24 from thisisthepy/work/member-names
b-re-w Oct 2, 2026
8b2ac01
Fix: Emit stub overloads fewest parameters first
b-re-w Oct 2, 2026
da0f2db
Merge pull request #26 from thisisthepy/work/overload-order
b-re-w Oct 2, 2026
95c559a
Feat: Serve Kotlin objects like Arrangement and Alignment through pyt…
b-re-w Oct 2, 2026
35e70a1
Merge pull request #27 from thisisthepy/work/layout-constants
b-re-w Oct 2, 2026
37feac0
Feat: Answer for Column, Row and Spacer from material3, as the notebo…
b-re-w Oct 2, 2026
d6c486f
Merge pull request #28 from thisisthepy/work/material3-layout
b-re-w Oct 2, 2026
469dec3
Docs: Record how the notebook's convenience spellings are written
b-re-w Oct 2, 2026
7beabf8
Merge pull request #30 from thisisthepy/work/notebook-spellings
b-re-w Oct 2, 2026
cadda45
Chore: Delete the dead material3 records, the empty placeholders and …
b-re-w Oct 2, 2026
a980b66
Merge pull request #32 from thisisthepy/work/delete-dead-material3
b-re-w Oct 2, 2026
f9696a9
Ci: Run the test suite on GitHub Actions, with and without the binder
b-re-w Oct 2, 2026
61c3ee1
Merge pull request #35 from thisisthepy/work/ci
b-re-w Oct 2, 2026
c27737c
Docs: Describe the Pythonic surface that now exists, and only that
b-re-w Oct 2, 2026
cdf6392
Docs: Subagents do not run heavy local builds
b-re-w Oct 2, 2026
6cb360f
Merge pull request #37 from thisisthepy/work/subagent-build-rule
b-re-w Oct 2, 2026
1786488
Merge pull request #36 from thisisthepy/work/guide-surface
b-re-w Oct 2, 2026
fb90af2
Fix: Import a pythonx submodule by from-import even with no binder in…
b-re-w Oct 2, 2026
5f1ff93
Merge pull request #39 from thisisthepy/work/submodule-fallback
b-re-w Oct 2, 2026
8cc6e5d
Feat: Ship Pythonic stubs for Compose 1.11.1, generated by the runtim…
b-re-w Oct 3, 2026
8eb1325
Test: Create the git-ignored scratch directory before using it
b-re-w Oct 3, 2026
3aecfea
Merge pull request #40 from thisisthepy/work/real-stubs
b-re-w Oct 3, 2026
661d726
Fix: Serve a Kotlin object as a namespace when the binder lists it it…
b-re-w Oct 3, 2026
f9ac4ea
Merge pull request #42 from thisisthepy/work/binder-51
b-re-w Oct 3, 2026
de55428
Ci: Run the suite daily against the binder, and open an issue when it…
b-re-w Oct 3, 2026
49f02d6
Merge pull request #44 from thisisthepy/work/ci-daily
b-re-w Oct 3, 2026
7204436
Feat: Read Alignment.Horizontal.End as the notebook writes it
b-re-w Oct 3, 2026
91f015e
Merge pull request #45 from thisisthepy/work/grouped-alignment
b-re-w Oct 3, 2026
544641c
Feat: Declare the app root with @app, and hold UI state in Compose state
b-re-w Oct 3, 2026
7d9c8f8
Merge pull request #46 from thisisthepy/work/app-root
b-re-w Oct 3, 2026
d1c21a5
Test: Exercise app_root and state() through the binder's real state path
b-re-w Oct 3, 2026
6be07dc
Docs: Point the pinned int refusal at the binder issue that reverses it
b-re-w Oct 3, 2026
47befcb
Merge pull request #47 from thisisthepy/work/state-binder-path
b-re-w Oct 3, 2026
383c1d8
Feat: Take snake_case keyword arguments on proxy methods too
b-re-w Oct 3, 2026
9fb9545
Merge pull request #48 from thisisthepy/work/method-keywords
b-re-w Oct 3, 2026
6d186f5
Feat: Regenerate the stubs from the binder's #53 output, typing objec…
b-re-w Oct 3, 2026
7ae24ac
Merge pull request #49 from thisisthepy/work/stubs-53
b-re-w Oct 3, 2026
74f7e53
Feat: Serve DefaultIcons from material3 as Icons.Default
b-re-w Oct 3, 2026
17929cb
Docs: Record the notebook's DefaultIcons.Add() against the current sp…
b-re-w Oct 3, 2026
0d5f3be
Merge pull request #50 from thisisthepy/work/default-icons
b-re-w Oct 3, 2026
645f622
Test: Round-trip numbers through state() now that the binder boxes them
b-re-w Oct 3, 2026
3061f67
Merge pull request #52 from thisisthepy/work/box-scalars
b-re-w Oct 3, 2026
224e4e6
Feat: Type SpaceBetween and the default icons from the binder's newer…
b-re-w Oct 3, 2026
8c93493
Merge pull request #53 from thisisthepy/work/stubs-a5028618
b-re-w Oct 3, 2026
8e9d764
Feat: TextField(state=...) with Compose owning the text, so IME compo…
b-re-w Oct 3, 2026
e6db167
Merge pull request #55 from thisisthepy/work/textfield
b-re-w Oct 3, 2026
4d91c4e
Chore: Drop CLAUDE.md; Claude Code reads AGENTS.md directly
b-re-w Oct 3, 2026
37a4c57
Merge pull request #57 from thisisthepy/work/drop-claude-md
b-re-w Oct 3, 2026
1cf898f
Chore: Name branches feat/<topic> and delete them once merged
b-re-w Oct 3, 2026
15b6fe7
Merge pull request #59 from thisisthepy/feat/branch-naming
b-re-w Oct 3, 2026
177b4a3
Chore: Restructure into a standard Python package: one tests/, no tools/
b-re-w Oct 3, 2026
8e16c28
Merge pull request #61 from thisisthepy/feat/restructure
b-re-w Oct 3, 2026
d2b9202
Feat: Prepare the first PyPI release, 0.1.0a1, published by tag throu…
b-re-w Oct 3, 2026
4ecdef1
Merge pull request #63 from thisisthepy/feat/first-release
b-re-w Oct 3, 2026
805e481
Chore: Protect main by repository settings alone, and drop the source…
b-re-w Oct 3, 2026
58fbe0f
Merge pull request #65 from thisisthepy/feat/main-protection-settings
b-re-w Oct 3, 2026
c959c5c
Ci: Publish to PyPI when a GitHub Release is published, not on a tag …
b-re-w Oct 3, 2026
19e3d84
Merge pull request #67 from thisisthepy/feat/publish-on-release
b-re-w Oct 3, 2026
c737b25
Release: sync from 19e3d846b1a68f9317b73dc186f7c380acbe5933 19e3d84
github-actions[bot] Oct 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
177 changes: 177 additions & 0 deletions .github/scripts/check_guide.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
"""Check the bilingual guide site under `docs/guide/`.

This is the guide's test. It fails when:

* an HTML page does not parse (unbalanced or mis-nested tags);
* a relative link or asset (`href`, `src`) points at a file that does not exist;
* a page shows a string in one language but not the other -- every element carrying
`data-lang="en"` must be paired, in the same parent, with one carrying `data-lang="ko"`,
and the reverse;
* visible text sits outside any `data-lang` element (it would show in both languages), unless it
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 .github/scripts/check_guide.py # exit status 0 = clean, 1 = findings printed

Standard library only, so it runs anywhere the repository's tests run.
"""

from __future__ import annotations

import sys
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import urlparse

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

VOID = {
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source",
"track", "wbr", "path", "circle", "rect", "line", "polyline", "polygon", "ellipse", "use",
"stop",
}
ALLOWED_EXTERNAL = ("fonts.googleapis.com", "fonts.gstatic.com")
# Links that leave the site on purpose (sibling repositories, the license, the source).
ALLOWED_LINK_HOSTS = ("github.com",)
# Text that reads the same in both languages and may appear without a `data-lang` pair.
NEUTRAL_TEXT = {"FAQ", "pythonx-compose", "GitHub", "Kotlin", "Python", "→", "←", "·"}
# Elements whose text is code, markup, or a diagram rather than prose.
EXEMPT_TAGS = {"code", "pre", "kbd", "svg", "script", "style", "title"}
REQUIRED_PAGES = (
"index.html",
"getting-started.html",
"concepts.html",
"guide-composables.html",
"guide-modifiers.html",
"guide-manifest.html",
"ecosystem.html",
"status.html",
)


class Page(HTMLParser):
def __init__(self, path: Path) -> None:
super().__init__(convert_charrefs=True)
self.path = path
self.stack: list[tuple[str, int]] = []
self.errors: list[str] = []
self.refs: list[tuple[str, str, int]] = [] # (attr, value, line)
# Per open element: list of language markers of its direct children.
self.children_langs: list[list[str]] = [[]]
# Per open element: does it (or an ancestor) make its text language-neutral or bilingual?
self.covered: list[bool] = [False]

def handle_starttag(self, tag, attrs):
line = self.getpos()[0]
attrs = dict(attrs)
for attr in ("href", "src"):
if attrs.get(attr):
self.refs.append((attr, attrs[attr], line))
lang = attrs.get("data-lang")
if lang is not None:
if lang not in ("en", "ko"):
self.errors.append(f"{line}: data-lang={lang!r} is neither en nor ko")
self.children_langs[-1].append(lang)
if tag in VOID:
return
self.stack.append((tag, line))
self.children_langs.append([])
self.covered.append(
self.covered[-1]
or lang is not None
or tag in EXEMPT_TAGS
or attrs.get("translate") == "no"
)

def handle_startendtag(self, tag, attrs):
# `<path ... />` and friends: record refs and language, never push.
line = self.getpos()[0]
attrs = dict(attrs)
for attr in ("href", "src"):
if attrs.get(attr):
self.refs.append((attr, attrs[attr], line))
lang = attrs.get("data-lang")
if lang is not None:
self.children_langs[-1].append(lang)

def handle_endtag(self, tag):
line = self.getpos()[0]
if tag in VOID:
return
if not self.stack:
self.errors.append(f"{line}: </{tag}> closes nothing")
return
open_tag, open_line = self.stack.pop()
langs = self.children_langs.pop()
self.covered.pop()
if open_tag != tag:
self.errors.append(f"{line}: </{tag}> closes <{open_tag}> opened on line {open_line}")
en, ko = langs.count("en"), langs.count("ko")
if en != ko:
self.errors.append(
f"{open_line}: <{open_tag}> has {en} English and {ko} Korean children -- "
"a string exists in one language only"
)

def handle_data(self, data):
text = data.strip()
if not text or self.covered[-1]:
return
words = [w for w in text.replace("·", " ").split() if any(ch.isalpha() for ch in w)]
if words and text not in NEUTRAL_TEXT:
self.errors.append(f"{self.getpos()[0]}: untranslated text outside data-lang: {text[:60]!r}")

def close(self):
super().close()
for tag, line in self.stack:
self.errors.append(f"{line}: <{tag}> is never closed")


def check_ref(page: Path, attr: str, value: str) -> str | None:
parsed = urlparse(value)
if parsed.scheme in ("http", "https"):
host = parsed.netloc
if attr == "src" or (attr == "href" and value.endswith(".css")):
if not host.endswith(ALLOWED_EXTERNAL):
return f"external resource {value} (only Google Fonts is allowed)"
return None
if host.endswith(ALLOWED_EXTERNAL + ALLOWED_LINK_HOSTS):
return None
return f"external link to an unexpected host: {value}"
if parsed.scheme in ("mailto", "data") or value.startswith("#"):
return None
target = (page.parent / parsed.path).resolve()
if not target.exists():
return f"{attr}={value!r} does not resolve ({target.relative_to(REPO) if target.is_relative_to(REPO) else target})"
return None


def main() -> int:
findings: list[str] = []
if not GUIDE.is_dir():
print(f"FAIL: {GUIDE.relative_to(REPO)} does not exist")
return 1
for name in REQUIRED_PAGES:
if not (GUIDE / name).is_file():
findings.append(f"{name}: required page is missing")
pages = sorted(GUIDE.rglob("*.html"))
for page in pages:
parser = Page(page)
parser.feed(page.read_text(encoding="utf-8"))
parser.close()
rel = page.relative_to(REPO)
findings += [f"{rel}:{e}" for e in parser.errors]
for attr, value, line in parser.refs:
problem = check_ref(page, attr, value)
if problem:
findings.append(f"{rel}:{line}: {problem}")
for finding in findings:
print(f"FAIL: {finding}")
print(f"{len(pages)} pages checked, {len(findings)} findings")
return 1 if findings else 0


if __name__ == "__main__":
sys.exit(main())
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))
49 changes: 49 additions & 0 deletions .github/scripts/release/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Release flow

develop --push--> release (auto) --PR--> main

`release` is CI-generated and disposable, not a long-lived branch. It may not exist
between runs, and any existing `release` (e.g. a stale one from an older workflow) is
ignored and overwritten.

1. Every push to `develop` triggers `.github/workflows/release-sync.yml`, which runs
`.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.
The push uses `--force-with-lease` pinned to the remote sha the run observed.
2. The workflow opens (or updates) a pull request `release` -> `main` with the default
`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).
4. On push to `main`, `pages.yml` deploys `docs/guide/` to GitHub Pages.

## What is dropped on release

- every `*.md` at the repository root except `README.md` (including `AGENTS.md`, `PROJECT.md`)
- every `*.md` directly inside `docs/`

Kept: everything else, including Markdown in `docs/<subfolder>/` and nested `README.md` files.
Only Markdown is ever dropped.

## Local use

.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

.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.
35 changes: 35 additions & 0 deletions .github/scripts/release/protect-main.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/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
Loading