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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ worktree so a test can see it. Read it in place.
- `docs/INTENT.md` and `docs/SPEC.md` are derived from it. When they disagree with the notebook or
with the user, the notebook and the user win.
- `tests/test_pythonx_map.py` reads the notebook's imports. Where the notebook is absent (any
worktree, any CI checkout) that test checks nothing and still passes. Run it in the main checkout
before claiming the manifest covers the notebook.
worktree, any CI checkout) that test **skips**, saying so. Run it in the main checkout before
claiming the manifest covers the notebook.

## 12. Renaming happens here, in real Python, never in the binder

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

```
python3 -m pytest tests -q
PythonMultiplatform 체크아웃 없음 40 passed, 45 skipped
PYTHONMULTIPLATFORM_HOME 지정 81 passed, 4 skipped (4 개는 python-multiplatform #17 대기)
PythonMultiplatform 체크아웃 없음 43 passed, 45 skipped
PYTHONMULTIPLATFORM_HOME 지정 84 passed, 4 skipped (4 개는 python-multiplatform #17 대기)
```

건너뛰는 4 개는 바인더가 돌려준 proxy 위의 camelCase 확장을 snake_case 이름으로 부르는 테스트다.
바인더의 member resolver(python-multiplatform #17)가 들어오면 실행된다. 건너뜀은 통과가 아니다.
체크아웃 없이 통과하는 40 개 중 다수는 *부재*(옛 토큰·삭제된 파일이 없음)를 확인하는 것이라 기능 진척으로 세지
체크아웃 없이 통과하는 43 개 중 다수는 *부재*(옛 토큰·삭제된 파일이 없음)를 확인하는 것이라 기능 진척으로 세지
않는다.

### 마일스톤
Expand All @@ -65,12 +65,13 @@ GitHub 마일스톤과 같은 내용이다. 날짜는 2026-10-03 에 정했고,
```
pythonx/compose/ import 패키지 (현재 대부분 독스트링만 있는 모듈)
runtime/ @Composable
pythonx-map.toml pythonx ↔ androidx 매핑 매니페스트 (wheel 에 함께 실림)
_reexport.py 매니페스트 모듈 전체에 적용되는 재노출 규칙 하나
ui/ modifier.py (빈 Modifier 등록 지점), alignment.py
layout/ arrangement.py
material3/ icon.py, color_scheme.py (호출 불가 기록용), 빈 파일 28 개
lite/ 2024 JPype 프로토타입 (은퇴, 바이너리 97 개 추적 중)
native/ 서브모듈 → thisisthepy/swing-graalvm-demo
pythonx-map.toml pythonx ↔ androidx 매핑 매니페스트
tests/ pytest (unittest 스타일)
test/ 2023–2024 Kotlin Multiplatform 샘플 (pycomposeui) — 처리 미정
docs/INTENT.md docs/SPEC.md docs/locale/ docs/guide/
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def Greeting():
- **`@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-map.toml) says which `pythonx.compose.*` module
- **One manifest.** [`pythonx-map.toml`](pythonx/compose/pythonx-map.toml) says which `pythonx.compose.*` module
stands for which Kotlin package. The runtime and the `.pyi` generator read the same file, so what
your editor completes is what the interpreter resolves.

Expand Down Expand Up @@ -95,7 +95,7 @@ assert Screen() == "drawn" and Screen.__name__ == "Screen"
```python
import tomllib # the manifest: the one place the mapping is written down

with open("pythonx-map.toml", "rb") as f:
with open("pythonx/compose/pythonx-map.toml", "rb") as f:
manifest = tomllib.load(f)

manifest["modules"]["pythonx.compose.layout"]
Expand Down
18 changes: 9 additions & 9 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ Run from a worktree with `python3 -m pytest tests -q` (pytest 8, CPython 3.13):

| Environment | Result |
|---|---|
| No `PythonMultiplatform` checkout found | **40 passed, 45 skipped**, 44 subtests passed |
| `PYTHONMULTIPLATFORM_HOME` → the `PythonMultiplatform` checkout | **81 passed, 4 skipped**, 44 subtests passed |
| No `PythonMultiplatform` checkout found | **43 passed, 45 skipped**, 44 subtests passed |
| `PYTHONMULTIPLATFORM_HOME` → the `PythonMultiplatform` checkout | **84 passed, 4 skipped**, 44 subtests passed |

Without a checkout, the 45 tests that install the binder's layers through `tests/adapter.py` skip.
With one, all pass except 4 that skip because they call a camelCase extension on a binder proxy by
its snake_case name, which needs python-multiplatform #17 (§3). A skip is not a pass.

Of the 40 that pass without a checkout, most assert **absence** (a retired token, a deleted file, a docstring that
Of the 43 that pass without a checkout, most 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 @@ -36,14 +36,14 @@ exists). Those are listed 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, and
`pyproject.toml` declares `name = "pythonx-compose"`, version `0.0.1`, setuptools build,
`requires-python >= 3.11` (the runtime reads the manifest with `tomllib`), and
`packages.find include = ["pythonx.compose*"]`.

- Configured: `package-data` carries `*.pyi`, `py.typed` and `pythonx-map.toml` for every package
(`tests/test_pythonx_map.py::TestManifestShipsWithThePackage::test_the_wheel_is_configured_to_carry_it`).
- Not yet true: no `.pyi` and no `py.typed` exist in the tree, and `pythonx-map.toml` sits at the
repository root — outside every package directory — so the `package-data` pattern cannot pick it
up. No test builds a wheel and looks inside it.
- 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.
- Not yet true: no `.pyi` and no `py.typed` exist (S1.2, issue #12).

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

Expand Down
4 changes: 2 additions & 2 deletions docs/guide/getting-started.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ <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>40 passed, 45 skipped</strong>. The 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>40 passed, 45 skipped</strong> 가 정상입니다. 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <a href="https://github.com/thisisthepy/python-multiplatform">python-multiplatform</a> 체크아웃이 있거나 다음이 필요합니다:</p>
<p data-lang="en">Expect <strong>43 passed, 45 skipped</strong>. The 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>43 passed, 45 skipped</strong> 가 정상입니다. 건너뛴 테스트는 바인더의 적응 계층을 구동하며, 옆에 <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 it, all but 4 pass; those 4 <strong>skip</strong> until the binder can resolve a snake_case method on a proxy it returned (python-multiplatform #17). That is expected and recorded.</p><p data-lang="ko">지정하면 4 개를 뺀 전부가 통과합니다. 그 4 개는 바인더가 돌려준 proxy 의 snake_case 메서드를 바인더가 풀 수 있을 때까지(python-multiplatform #17) <strong>건너뜁니다</strong>. 예상된 상태이며 기록되어 있습니다.</p>
</section>
Expand All @@ -61,7 +61,7 @@ <h2><span data-lang="en">3. What you can call today</span><span data-lang="ko">3
assert Screen() == &quot;drawn&quot; and Screen.__name__ == &quot;Screen&quot;</code></pre>
<pre><code class="language-python">import tomllib

with open(&quot;pythonx-map.toml&quot;, &quot;rb&quot;) as f:
with open(&quot;pythonx/compose/pythonx-map.toml&quot;, &quot;rb&quot;) as f:
manifest = tomllib.load(f)

manifest[&quot;modules&quot;][&quot;pythonx.compose.layout&quot;]
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/status.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
<thead><tr><th><span data-lang="en">State</span><span data-lang="ko">상태</span></th><th><span data-lang="en">Item</span><span data-lang="ko">항목</span></th></tr></thead>
<tbody><tr><td><span class="chip ok"><span data-lang="en">implemented</span><span data-lang="ko">구현</span></span></td><td><span data-lang="en">Mapping manifest <code>pythonx-map.toml</code></span><span data-lang="ko">매핑 매니페스트 <code>pythonx-map.toml</code></span></td></tr><tr><td><span class="chip ok"><span data-lang="en">implemented</span><span data-lang="ko">구현</span></span></td><td><span data-lang="en"><code>@Composable</code> identity decorator</span><span data-lang="ko"><code>@Composable</code> 항등 데코레이터</span></td></tr><tr><td><span class="chip partial"><span data-lang="en">partial</span><span data-lang="ko">부분</span></span></td><td><span data-lang="en">Distribution as <code>pythonx-compose</code> — configured; no stubs yet, manifest outside the package directory</span><span data-lang="ko"><code>pythonx-compose</code> 배포 — 설정됨; 스텁 없음, 매니페스트가 패키지 디렉터리 밖</span></td></tr><tr><td><span class="chip ok"><span data-lang="en">implemented</span><span data-lang="ko">구현</span></span></td><td><span data-lang="en"><code>Modifier</code> chains, overload dispatch, <code>Dp</code> as a number</span><span data-lang="ko"><code>Modifier</code> 체인, 오버로드 디스패치, 숫자 <code>Dp</code></span></td></tr><tr><td><span class="chip partial"><span data-lang="en">partial</span><span data-lang="ko">부분</span></span></td><td><span data-lang="en">Snake-case names, keywords and signatures — module level done; methods on a returned proxy wait for python-multiplatform #17</span><span data-lang="ko">snake_case 이름·키워드·시그니처 — 모듈 수준 완료, 반환된 proxy 의 메서드는 python-multiplatform #17 대기</span></td></tr><tr><td><span class="chip partial"><span data-lang="en">partial</span><span data-lang="ko">부분</span></span></td><td><span data-lang="en">Material 3 widgets — re-exported by rule; render proofs per widget pending (#9)</span><span data-lang="ko">Material 3 위젯 — 규칙으로 재노출됨; 위젯별 렌더 증거 대기(#9)</span></td></tr><tr><td><span class="chip partial"><span data-lang="en">partial</span><span data-lang="ko">부분</span></span></td><td><span data-lang="en"><code>Icon</code>, colour schemes — importable, not callable</span><span data-lang="ko"><code>Icon</code>, 색 스킴 — import 가능, 호출 불가</span></td></tr><tr><td><span class="chip partial"><span data-lang="en">partial</span><span data-lang="ko">부분</span></span></td><td><span data-lang="en"><code>Alignment</code> / <code>Arrangement</code> constants — read without parentheses; grouped spelling pending (#9)</span><span data-lang="ko"><code>Alignment</code> / <code>Arrangement</code> 상수 — 괄호 없이 읽음; 묶음 표기 대기(#9)</span></td></tr><tr><td><span class="chip ok"><span data-lang="en">implemented</span><span data-lang="ko">구현</span></span></td><td><span data-lang="en"><code>pythonx</code> as a real on-disk package re-exporting <code>androidx.compose.*</code> by one rule</span><span data-lang="ko"><code>androidx.compose.*</code> 를 규칙 하나로 재노출하는 실제 디스크 패키지 <code>pythonx</code></span></td></tr><tr><td><span class="chip planned"><span data-lang="en">planned</span><span data-lang="ko">계획</span></span></td><td><span data-lang="en"><code>.pyi</code> stubs in the wheel</span><span data-lang="ko">wheel 안의 <code>.pyi</code> 스텁</span></td></tr><tr><td><span class="chip planned"><span data-lang="en">planned</span><span data-lang="ko">계획</span></span></td><td><span data-lang="en"><code>remember_saveable</code>, <code>DefaultIcons</code>, coroutine scopes</span><span data-lang="ko"><code>remember_saveable</code>, <code>DefaultIcons</code>, 코루틴 스코프</span></td></tr></tbody>
</table></div>
<p data-lang="en" class="muted">Test baseline: <code>40 passed, 45 skipped</code> without a python-multiplatform checkout; <code>81 passed, 4 skipped</code> with one.</p><p data-lang="ko" class="muted">테스트 기준선: python-multiplatform 체크아웃이 없으면 <code>40 passed, 45 skipped</code>, 있으면 <code>81 passed, 4 skipped</code>.</p>
<p data-lang="en" class="muted">Test baseline: <code>43 passed, 45 skipped</code> without a python-multiplatform checkout; <code>84 passed, 4 skipped</code> with one.</p><p data-lang="ko" class="muted">테스트 기준선: python-multiplatform 체크아웃이 없으면 <code>43 passed, 45 skipped</code>, 있으면 <code>84 passed, 4 skipped</code>.</p>
</section>
<section>
<h2>FAQ</h2>
Expand Down
4 changes: 2 additions & 2 deletions docs/locale/README_ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def Greeting():
- **`@Composable` 은 그대로.** 화면은 데코레이터가 붙은 파이썬 함수입니다.
- **실제 패키지.** `pythonx/` 는 `androidx.compose.*` 를 import 해 재구성하는 평범한 파이썬
소스입니다. 바인더는 아무 이름도 바꾸지 않습니다 — 이름을 바꾸는 것은 이 패키지의 일입니다.
- **하나의 매니페스트.** [`pythonx-map.toml`](../../pythonx-map.toml) 이 어떤 `pythonx.compose.*`
- **하나의 매니페스트.** [`pythonx-map.toml`](../../pythonx/compose/pythonx-map.toml) 이 어떤 `pythonx.compose.*`
모듈이 어떤 Kotlin 패키지에 대응하는지 적습니다. 런타임과 `.pyi` 생성기가 같은 파일을 읽으므로,
편집기가 자동완성하는 이름과 인터프리터가 해석하는 이름이 어긋나지 않습니다.

Expand Down Expand Up @@ -94,7 +94,7 @@ assert Screen() == "drawn" and Screen.__name__ == "Screen"
```python
import tomllib # 매니페스트: 대응 관계가 적힌 유일한 곳

with open("pythonx-map.toml", "rb") as f:
with open("pythonx/compose/pythonx-map.toml", "rb") as f:
manifest = tomllib.load(f)

manifest["modules"]["pythonx.compose.layout"]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ dependencies = [
#dependencies-kotlin = [
# "org.jetbrains.compose:compose-ui:0.4.0",
#]
requires-python = ">= 3.8"
requires-python = ">= 3.11" # tomllib, which reads the manifest at run time
#requires-kotlin = ">= 1.4.32"

[tool.setuptools.packages.find]
Expand Down
2 changes: 1 addition & 1 deletion pythonx/compose/_reexport.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
ROOT_MODULE = "python_multiplatform"
BINDING_MODULE = "python_multiplatform.binding"

_MANIFEST = Path(__file__).resolve().parents[2] / "pythonx-map.toml"
_MANIFEST = Path(__file__).resolve().parent / "pythonx-map.toml"

_LOWER_UPPER = re.compile(r"([a-z0-9])([A-Z])")
_ACRONYM_WORD = re.compile(r"([A-Z]+)([A-Z][a-z])")
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion tests/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def read_manifest() -> dict:
"""
import tomllib

path = pathlib.Path(__file__).resolve().parents[1] / "pythonx-map.toml"
path = pathlib.Path(__file__).resolve().parents[1] / "pythonx" / "compose" / "pythonx-map.toml"
with path.open("rb") as handle:
return tomllib.load(handle)

Expand Down
4 changes: 3 additions & 1 deletion tests/test_pythonx_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from pathlib import Path

REPO = Path(__file__).resolve().parents[1]
MAP = REPO / "pythonx-map.toml"
MAP = REPO / "pythonx" / "compose" / "pythonx-map.toml"
NOTEBOOK = REPO / "UI.ipynb"


Expand Down Expand Up @@ -60,6 +60,8 @@ def test_the_wheel_is_configured_to_carry_it(self) -> None:

class TestEveryNotebookImportIsMapped(unittest.TestCase):
def test_the_specification_resolves(self) -> None:
if not NOTEBOOK.is_file():
self.skipTest("UI.ipynb is not here (it lives only in the maintainer's main checkout)")
modules = _manifest()["modules"]
missing = sorted(name for name in _notebook_imports() if name not in modules)
self.assertEqual([], missing, "UI.ipynb imports a module the manifest does not map")
Expand Down
78 changes: 78 additions & 0 deletions tests/test_wheel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""What the wheel actually carries -- built, opened and listed, not inferred from `package-data`.

AGENTS.md section 15: a pattern in `pyproject.toml` is not proof that a file ships. This builds the
wheel from a copy of the sources (so no `build/` or `*.egg-info` lands in the checkout) and reads its
file list.

Needs `setuptools` and `wheel` importable in the running interpreter; without them the test skips,
and a skip is not a pass.
"""

from __future__ import annotations

import importlib.util
import shutil
import subprocess
import sys
import tempfile
import tomllib
import unittest
import zipfile
from pathlib import Path

REPO = Path(__file__).resolve().parents[1]
SCRATCH = REPO / ".tmp"


def _build_wheel(out: Path) -> Path:
source = out / "src"
shutil.copytree(
REPO, source,
ignore=shutil.ignore_patterns(".git", ".tmp", ".worktrees", "build", "*.egg-info",
"__pycache__", "UI.ipynb", "test", "docs", "tools"),
)
wheels = out / "wheels"
subprocess.run(
[sys.executable, "-m", "pip", "wheel", str(source), "--no-deps", "--no-build-isolation",
"-q", "-w", str(wheels)],
check=True, capture_output=True, text=True,
)
(wheel,) = wheels.glob("*.whl")
return wheel


class TheWheel(unittest.TestCase):

@classmethod
def setUpClass(cls):
for needed in ("setuptools", "wheel"):
if importlib.util.find_spec(needed) is None:
raise unittest.SkipTest(f"{needed} is not installed; the wheel cannot be built here")
SCRATCH.mkdir(exist_ok=True)
cls._dir = tempfile.TemporaryDirectory(dir=SCRATCH)
wheel = _build_wheel(Path(cls._dir.name))
with zipfile.ZipFile(wheel) as archive:
cls.names = set(archive.namelist())

@classmethod
def tearDownClass(cls):
cls._dir.cleanup()

def test_the_manifest_is_inside_the_package(self):
self.assertIn("pythonx/compose/pythonx-map.toml", self.names)

def test_every_mapped_module_is_a_file_in_the_wheel(self):
with (REPO / "pythonx" / "compose" / "pythonx-map.toml").open("rb") as handle:
modules = tomllib.load(handle)["modules"]
missing = sorted(
name for name in modules
if name.replace(".", "/") + "/__init__.py" not in self.names
)
self.assertEqual([], missing, "a mapped module with no file cannot be imported once installed")

def test_the_re_export_rule_ships(self):
self.assertIn("pythonx/compose/_reexport.py", self.names)


if __name__ == "__main__":
unittest.main()