easybind 0.2.3__tar.gz → 0.2.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {easybind-0.2.3 → easybind-0.2.5}/PKG-INFO +47 -1
- {easybind-0.2.3 → easybind-0.2.5}/README.md +46 -0
- {easybind-0.2.3 → easybind-0.2.5}/RELEASING.md +17 -0
- {easybind-0.2.3 → easybind-0.2.5}/pyproject.toml +10 -0
- easybind-0.2.5/pyrightconfig.json +4 -0
- easybind-0.2.5/scripts/release_tag.py +16 -0
- easybind-0.2.5/scripts/wait_pypi_release.py +16 -0
- easybind-0.2.5/src/easybind/devtools/__init__.py +37 -0
- easybind-0.2.5/src/easybind/devtools/pin_pyproject.py +251 -0
- easybind-0.2.5/src/easybind/devtools/release_helpers.py +92 -0
- easybind-0.2.5/src/easybind/devtools/release_tag.py +89 -0
- easybind-0.2.5/src/easybind/devtools/wait_pypi.py +64 -0
- {easybind-0.2.3 → easybind-0.2.5}/.clangd +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/.github/workflows/publish.yml +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/.gitignore +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/.vscode/tasks.json +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/CMakeLists.txt +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/LICENSE +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/NOTICE +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/cmake/easybind_dependencies.cmake +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/cmake/easybind_pip.cmake +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/scripts/clangd-update.sh +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/src/easybind/__init__.cpp +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/src/easybind/bind.hpp +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/src/easybind/export.hpp +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/src/easybind/module/__init__.cpp +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/src/easybind/module/node.cpp +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/src/easybind/module/node.hpp +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/src/easybind/module/node__init__.cpp +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/src/easybind/module/ns_module.hpp +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/src/easybind/py.typed +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/src/easybind/sample/__init__.cpp +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/src/easybind/sample/sample.cpp +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/src/easybind/sample/sample.hpp +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/src/easybind/sample/test1.py +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/src/easybind/sample/test2/__init__.py +0 -0
- {easybind-0.2.3 → easybind-0.2.5}/tests/test_sample.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: easybind
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Self-registering nanobind helpers (import as easybind)
|
|
5
5
|
Keywords: easybind,nanobind,bindings,pymergetic
|
|
6
6
|
Author-Email: PymergeticOS Maintainers <raudzus@pymergetic.com>
|
|
@@ -83,6 +83,52 @@ easybind_fetch_magic_enum() # if you include easybind headers that need magic_
|
|
|
83
83
|
|
|
84
84
|
When developing **inside** this repository, `easybind_add_extension(...)` is defined in the top-level `CMakeLists.txt` (not shipped in the wheel).
|
|
85
85
|
|
|
86
|
+
## Bumping `{distribution}~=…` pins (downstream projects)
|
|
87
|
+
|
|
88
|
+
Use **`easybind.devtools`** or the **`easybind-pin-pyproject`** CLI to rewrite every compatible-release line **`{name}~=X.Y.Z`** in a **`pyproject.toml`** for any PyPI **distribution** name **`name`** (not only **easybind**). Examples:
|
|
89
|
+
|
|
90
|
+
- **cppdantic** pinning **easybind** in several tables.
|
|
91
|
+
- A future project pinning **cppdantic** the same way: pass **`--distribution cppdantic`**.
|
|
92
|
+
|
|
93
|
+
**CLI** (defaults: **`distribution=easybind`**, version = latest on PyPI for that distribution; run from the tree that contains **`pyproject.toml`**):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
easybind-pin-pyproject --dry-run
|
|
97
|
+
easybind-pin-pyproject
|
|
98
|
+
easybind-pin-pyproject --installed
|
|
99
|
+
easybind-pin-pyproject --version 0.2.3
|
|
100
|
+
easybind-pin-pyproject --distribution cppdantic
|
|
101
|
+
easybind-pin-pyproject --pyproject /path/to/pyproject.toml
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Other devtools CLIs** (after **`pip install`** / **`uv pip install -e .`**, or run **`scripts/…`** shims from a git checkout):
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
easybind-release-tag --dry-run # next v* tag + git push (easybind repo)
|
|
108
|
+
easybind-wait-pypi # poll PyPI until pins resolve (downstream CI)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Library:**
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from easybind.devtools import (
|
|
115
|
+
bump_compatible_pins_in_file,
|
|
116
|
+
fetch_pypi_version,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Pin all easybind~= lines to latest PyPI easybind
|
|
120
|
+
ver = fetch_pypi_version("easybind")
|
|
121
|
+
bump_compatible_pins_in_file("pyproject.toml", "easybind", ver)
|
|
122
|
+
|
|
123
|
+
# Pin all cppdantic~= lines to latest PyPI cppdantic (e.g. downstream of cppdantic)
|
|
124
|
+
ver = fetch_pypi_version("cppdantic")
|
|
125
|
+
bump_compatible_pins_in_file("pyproject.toml", "cppdantic", ver)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Shorthands **`bump_easybind_compatible_pins`** / **`bump_easybind_compatible_pins_in_file`** remain for **`distribution=\"easybind\"`** only.
|
|
129
|
+
|
|
130
|
+
**CI (downstream):** **`easybind-wait-pypi`** (or **`scripts/wait_pypi_release.py`**) wraps **`wait_pypi_for_compatible_pin`** in **`easybind.devtools`** — poll PyPI until the pinned version exists. From a checkout without install, set **`PYTHONPATH`** to this repo’s **`src`** or use the shim script.
|
|
131
|
+
|
|
86
132
|
## Core idea
|
|
87
133
|
- Each namespace/module defines a `ModuleNode` and a bind callback.
|
|
88
134
|
- The module entry point calls `apply_init` to run the callback and recurse.
|
|
@@ -54,6 +54,52 @@ easybind_fetch_magic_enum() # if you include easybind headers that need magic_
|
|
|
54
54
|
|
|
55
55
|
When developing **inside** this repository, `easybind_add_extension(...)` is defined in the top-level `CMakeLists.txt` (not shipped in the wheel).
|
|
56
56
|
|
|
57
|
+
## Bumping `{distribution}~=…` pins (downstream projects)
|
|
58
|
+
|
|
59
|
+
Use **`easybind.devtools`** or the **`easybind-pin-pyproject`** CLI to rewrite every compatible-release line **`{name}~=X.Y.Z`** in a **`pyproject.toml`** for any PyPI **distribution** name **`name`** (not only **easybind**). Examples:
|
|
60
|
+
|
|
61
|
+
- **cppdantic** pinning **easybind** in several tables.
|
|
62
|
+
- A future project pinning **cppdantic** the same way: pass **`--distribution cppdantic`**.
|
|
63
|
+
|
|
64
|
+
**CLI** (defaults: **`distribution=easybind`**, version = latest on PyPI for that distribution; run from the tree that contains **`pyproject.toml`**):
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
easybind-pin-pyproject --dry-run
|
|
68
|
+
easybind-pin-pyproject
|
|
69
|
+
easybind-pin-pyproject --installed
|
|
70
|
+
easybind-pin-pyproject --version 0.2.3
|
|
71
|
+
easybind-pin-pyproject --distribution cppdantic
|
|
72
|
+
easybind-pin-pyproject --pyproject /path/to/pyproject.toml
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Other devtools CLIs** (after **`pip install`** / **`uv pip install -e .`**, or run **`scripts/…`** shims from a git checkout):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
easybind-release-tag --dry-run # next v* tag + git push (easybind repo)
|
|
79
|
+
easybind-wait-pypi # poll PyPI until pins resolve (downstream CI)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Library:**
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from easybind.devtools import (
|
|
86
|
+
bump_compatible_pins_in_file,
|
|
87
|
+
fetch_pypi_version,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Pin all easybind~= lines to latest PyPI easybind
|
|
91
|
+
ver = fetch_pypi_version("easybind")
|
|
92
|
+
bump_compatible_pins_in_file("pyproject.toml", "easybind", ver)
|
|
93
|
+
|
|
94
|
+
# Pin all cppdantic~= lines to latest PyPI cppdantic (e.g. downstream of cppdantic)
|
|
95
|
+
ver = fetch_pypi_version("cppdantic")
|
|
96
|
+
bump_compatible_pins_in_file("pyproject.toml", "cppdantic", ver)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Shorthands **`bump_easybind_compatible_pins`** / **`bump_easybind_compatible_pins_in_file`** remain for **`distribution=\"easybind\"`** only.
|
|
100
|
+
|
|
101
|
+
**CI (downstream):** **`easybind-wait-pypi`** (or **`scripts/wait_pypi_release.py`**) wraps **`wait_pypi_for_compatible_pin`** in **`easybind.devtools`** — poll PyPI until the pinned version exists. From a checkout without install, set **`PYTHONPATH`** to this repo’s **`src`** or use the shim script.
|
|
102
|
+
|
|
57
103
|
## Core idea
|
|
58
104
|
- Each namespace/module defines a `ModuleNode` and a bind callback.
|
|
59
105
|
- The module entry point calls `apply_init` to run the callback and recurse.
|
|
@@ -20,10 +20,27 @@ twine upload dist/* # or TestPyPI: twine upload --repository testpypi dist/*
|
|
|
20
20
|
|
|
21
21
|
Configure credentials with **API token** (`~/.pypirc` or environment variables) or use **trusted publishing** (OIDC) from GitHub Actions.
|
|
22
22
|
|
|
23
|
+
## Helper: next semver tag + push
|
|
24
|
+
|
|
25
|
+
`scripts/release_tag.py` picks the latest **`vMAJOR.MINOR.PATCH`** tag, bumps **patch** by default (or **`--minor`** / **`--major`**), then **`fetch` / `checkout` / `pull`** (unless **`--no-pull`**), creates an **annotated** tag, and **`push`**es branch + tag (unless **`--no-push`** or **`--dry-run`**).
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
./scripts/release_tag.py --dry-run
|
|
29
|
+
./scripts/release_tag.py
|
|
30
|
+
./scripts/release_tag.py --minor
|
|
31
|
+
./scripts/release_tag.py --major
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Run from the **easybind** repo root. Requires a **clean** working tree.
|
|
35
|
+
|
|
36
|
+
**Tag ≠ PyPI success:** the tag appears on GitHub immediately; the **Publish** workflow can still fail (build, auditwheel, twine). Fix the branch, then tag again or re-run the workflow — PyPI may not have that version until CI goes green.
|
|
37
|
+
|
|
23
38
|
## CI upload
|
|
24
39
|
|
|
25
40
|
Pushing a tag matching `v*` triggers `.github/workflows/publish.yml`, which builds and uploads to PyPI.
|
|
26
41
|
|
|
42
|
+
**Downstream:** projects that pin **easybind** can use **`scripts/wait_pypi_release.py`** in their own CI (see docstring): it waits until the pinned release exists on PyPI before building — useful when the consumer tag is pushed immediately after the easybind tag.
|
|
43
|
+
|
|
27
44
|
Create a **repository secret** `PYPI_API_TOKEN` with a PyPI API token scoped to this project (or switch the workflow to [trusted publishing](https://docs.pypi.org/trusted-publishers/)).
|
|
28
45
|
|
|
29
46
|
## Platforms
|
|
@@ -32,6 +32,16 @@ Homepage = "https://github.com/pymergetic/easybind"
|
|
|
32
32
|
Repository = "https://github.com/pymergetic/easybind"
|
|
33
33
|
Issues = "https://github.com/pymergetic/easybind/issues"
|
|
34
34
|
|
|
35
|
+
# Scripts under scripts/ add src to sys.path at runtime; pyright/pylance need this too.
|
|
36
|
+
# See also pyrightconfig.json in this directory (nearest config for files under external/easybind/).
|
|
37
|
+
[tool.pyright]
|
|
38
|
+
extraPaths = ["src"]
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
easybind-pin-pyproject = "easybind.devtools.pin_pyproject:main"
|
|
42
|
+
easybind-release-tag = "easybind.devtools.release_tag:main"
|
|
43
|
+
easybind-wait-pypi = "easybind.devtools.wait_pypi:main"
|
|
44
|
+
|
|
35
45
|
[project.optional-dependencies]
|
|
36
46
|
dev = [
|
|
37
47
|
"pytest>=7.0.0",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shim: run from a git checkout without installing (adds ``src`` to ``sys.path``)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
_SRC = Path(__file__).resolve().parents[1] / "src"
|
|
10
|
+
if str(_SRC) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(_SRC))
|
|
12
|
+
|
|
13
|
+
from easybind.devtools.release_tag import main # noqa: E402
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shim: run from a git checkout without installing (adds ``src`` to ``sys.path``)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
_SRC = Path(__file__).resolve().parents[1] / "src"
|
|
10
|
+
if str(_SRC) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(_SRC))
|
|
12
|
+
|
|
13
|
+
from easybind.devtools.wait_pypi import main # noqa: E402
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Developer helpers (PyPI version lookup, bumping ``{dist}~=…`` pins in ``pyproject.toml``)."""
|
|
2
|
+
|
|
3
|
+
from easybind.devtools.pin_pyproject import (
|
|
4
|
+
bump_compatible_pins,
|
|
5
|
+
bump_compatible_pins_in_file,
|
|
6
|
+
bump_easybind_compatible_pins,
|
|
7
|
+
bump_easybind_compatible_pins_in_file,
|
|
8
|
+
compatible_pin_versions,
|
|
9
|
+
fetch_pypi_version,
|
|
10
|
+
installed_distribution_version,
|
|
11
|
+
pypi_release_exists,
|
|
12
|
+
single_compatible_pin_version,
|
|
13
|
+
wait_pypi_for_compatible_pin,
|
|
14
|
+
)
|
|
15
|
+
from easybind.devtools.release_helpers import (
|
|
16
|
+
ensure_clean_worktree,
|
|
17
|
+
latest_v_tag,
|
|
18
|
+
next_v_tag,
|
|
19
|
+
tag_push_commands,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"bump_compatible_pins",
|
|
24
|
+
"bump_compatible_pins_in_file",
|
|
25
|
+
"bump_easybind_compatible_pins",
|
|
26
|
+
"bump_easybind_compatible_pins_in_file",
|
|
27
|
+
"compatible_pin_versions",
|
|
28
|
+
"ensure_clean_worktree",
|
|
29
|
+
"fetch_pypi_version",
|
|
30
|
+
"installed_distribution_version",
|
|
31
|
+
"latest_v_tag",
|
|
32
|
+
"next_v_tag",
|
|
33
|
+
"pypi_release_exists",
|
|
34
|
+
"single_compatible_pin_version",
|
|
35
|
+
"tag_push_commands",
|
|
36
|
+
"wait_pypi_for_compatible_pin",
|
|
37
|
+
]
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Bump ``{distribution}~=…`` compatible-release pins in a ``pyproject.toml`` string or file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from urllib.error import HTTPError
|
|
12
|
+
from urllib.request import urlopen
|
|
13
|
+
|
|
14
|
+
# PEP 440 version suitable for ``~=`` RHS in typical pyproject pins (numeric release).
|
|
15
|
+
_VERSION_RE = re.compile(r"^[0-9]+(?:\.[0-9]+)*$")
|
|
16
|
+
|
|
17
|
+
# Reject pathological / mistaken distribution strings (full re.escape covers the rest).
|
|
18
|
+
_DIST_OK = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]*$")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _pin_pattern(distribution: str) -> re.Pattern[str]:
|
|
22
|
+
if not _DIST_OK.match(distribution):
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f"invalid distribution name for pin replacement: {distribution!r} "
|
|
25
|
+
"(expected a PyPI-style name, e.g. easybind, cppdantic, scikit-build-core)"
|
|
26
|
+
)
|
|
27
|
+
return re.compile(rf"({re.escape(distribution)}~=)([0-9]+(?:\.[0-9]+)*)")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def fetch_pypi_version(package: str = "easybind", *, timeout_s: float = 30.0) -> str:
|
|
31
|
+
"""Return ``info.version`` from ``https://pypi.org/pypi/{package}/json``."""
|
|
32
|
+
url = f"https://pypi.org/pypi/{package}/json"
|
|
33
|
+
with urlopen(url, timeout=timeout_s) as r:
|
|
34
|
+
data = json.load(r)
|
|
35
|
+
return str(data["info"]["version"])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def pypi_release_exists(package: str, version: str, *, timeout_s: float = 15.0) -> bool:
|
|
39
|
+
"""Return True if ``https://pypi.org/pypi/{package}/{version}/json`` exists (release uploaded)."""
|
|
40
|
+
url = f"https://pypi.org/pypi/{package}/{version}/json"
|
|
41
|
+
try:
|
|
42
|
+
with urlopen(url, timeout=timeout_s) as r:
|
|
43
|
+
return getattr(r, "status", 200) == 200
|
|
44
|
+
except HTTPError as e:
|
|
45
|
+
if e.code == 404:
|
|
46
|
+
return False
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def compatible_pin_versions(pyproject_toml: str, distribution: str) -> list[str]:
|
|
51
|
+
"""Return every version string from ``{distribution}~=VERSION`` pins (order preserved)."""
|
|
52
|
+
pat = _pin_pattern(distribution)
|
|
53
|
+
return [m.group(2) for m in pat.finditer(pyproject_toml)]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def single_compatible_pin_version(
|
|
57
|
+
pyproject_toml: str,
|
|
58
|
+
distribution: str,
|
|
59
|
+
*,
|
|
60
|
+
empty_pins_suffix: str = "",
|
|
61
|
+
) -> str:
|
|
62
|
+
"""Return the version string if all ``{distribution}~=…`` pins agree; else raise ``ValueError``."""
|
|
63
|
+
vers = compatible_pin_versions(pyproject_toml, distribution)
|
|
64
|
+
if not vers:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"no `{distribution}~=...` pins in pyproject.toml{empty_pins_suffix}"
|
|
67
|
+
)
|
|
68
|
+
uniq = set(vers)
|
|
69
|
+
if len(uniq) != 1:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"{distribution}~= pins disagree: {sorted(uniq)!r}; fix pyproject.toml first"
|
|
72
|
+
)
|
|
73
|
+
return vers[0]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def wait_pypi_for_compatible_pin(
|
|
77
|
+
pyproject_toml: str,
|
|
78
|
+
distribution: str,
|
|
79
|
+
*,
|
|
80
|
+
timeout_s: float = 1800.0,
|
|
81
|
+
interval_s: float = 30.0,
|
|
82
|
+
verbose: bool = False,
|
|
83
|
+
) -> tuple[str, int]:
|
|
84
|
+
"""Poll PyPI until ``pypi_release_exists(distribution, version)`` for the pin.
|
|
85
|
+
|
|
86
|
+
Uses ``single_compatible_pin_version`` on *pyproject_toml*. Returns
|
|
87
|
+
``(resolved_version, attempt_count)``. Raises ``TimeoutError`` if the release
|
|
88
|
+
never appears within *timeout_s*. With *verbose*, log progress to stdout.
|
|
89
|
+
"""
|
|
90
|
+
version = single_compatible_pin_version(pyproject_toml, distribution)
|
|
91
|
+
if verbose:
|
|
92
|
+
print(
|
|
93
|
+
f"waiting for {distribution}=={version} on PyPI "
|
|
94
|
+
f"(timeout {timeout_s:.0f}s, interval {interval_s:.0f}s)...",
|
|
95
|
+
flush=True,
|
|
96
|
+
)
|
|
97
|
+
deadline = time.monotonic() + timeout_s
|
|
98
|
+
attempt = 0
|
|
99
|
+
while True:
|
|
100
|
+
attempt += 1
|
|
101
|
+
if pypi_release_exists(distribution, version):
|
|
102
|
+
return version, attempt
|
|
103
|
+
if time.monotonic() >= deadline:
|
|
104
|
+
raise TimeoutError(
|
|
105
|
+
f"timed out waiting for {distribution}=={version} on PyPI "
|
|
106
|
+
f"after {timeout_s}s ({attempt} attempts)"
|
|
107
|
+
)
|
|
108
|
+
if verbose:
|
|
109
|
+
remaining = int(max(0, deadline - time.monotonic()))
|
|
110
|
+
print(
|
|
111
|
+
f"attempt {attempt}: not yet (retry in {interval_s:.0f}s, ~{remaining}s left)...",
|
|
112
|
+
flush=True,
|
|
113
|
+
)
|
|
114
|
+
time.sleep(interval_s)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def installed_distribution_version(package: str = "easybind") -> str:
|
|
118
|
+
"""Return ``importlib.metadata.version(package)`` for the active environment."""
|
|
119
|
+
from importlib.metadata import version
|
|
120
|
+
|
|
121
|
+
return version(package)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def bump_compatible_pins(pyproject_toml: str, distribution: str, version: str) -> tuple[str, int]:
|
|
125
|
+
"""Replace each ``{distribution}~=X.Y.Z`` with ``{distribution}~={version}``.
|
|
126
|
+
|
|
127
|
+
Returns ``(new_text, replacement_count)``.
|
|
128
|
+
"""
|
|
129
|
+
if not _VERSION_RE.match(version):
|
|
130
|
+
raise ValueError(f"invalid PEP 440 version for pin: {version!r}")
|
|
131
|
+
|
|
132
|
+
pat = _pin_pattern(distribution)
|
|
133
|
+
|
|
134
|
+
def repl(m: re.Match[str]) -> str:
|
|
135
|
+
return f"{m.group(1)}{version}"
|
|
136
|
+
|
|
137
|
+
new_text, n = pat.subn(repl, pyproject_toml)
|
|
138
|
+
return new_text, n
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def bump_compatible_pins_in_file(
|
|
142
|
+
pyproject: Path | str,
|
|
143
|
+
distribution: str,
|
|
144
|
+
version: str,
|
|
145
|
+
*,
|
|
146
|
+
dry_run: bool = False,
|
|
147
|
+
) -> int:
|
|
148
|
+
"""Read ``pyproject.toml``, apply :func:`bump_compatible_pins`, write back unless ``dry_run``.
|
|
149
|
+
|
|
150
|
+
Returns the number of replacements. Raises ``ValueError`` if no matching pin exists.
|
|
151
|
+
"""
|
|
152
|
+
path = Path(pyproject)
|
|
153
|
+
text = path.read_text(encoding="utf-8")
|
|
154
|
+
new_text, n = bump_compatible_pins(text, distribution, version)
|
|
155
|
+
if n == 0:
|
|
156
|
+
raise ValueError(f"no `{distribution}~=...` pin found in {path}")
|
|
157
|
+
if new_text == text:
|
|
158
|
+
return n
|
|
159
|
+
if not dry_run:
|
|
160
|
+
path.write_text(new_text, encoding="utf-8")
|
|
161
|
+
return n
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def bump_easybind_compatible_pins(pyproject_toml: str, version: str) -> tuple[str, int]:
|
|
165
|
+
"""Same as :func:`bump_compatible_pins` with ``distribution=\"easybind\"``."""
|
|
166
|
+
return bump_compatible_pins(pyproject_toml, "easybind", version)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def bump_easybind_compatible_pins_in_file(
|
|
170
|
+
pyproject: Path | str,
|
|
171
|
+
version: str,
|
|
172
|
+
*,
|
|
173
|
+
dry_run: bool = False,
|
|
174
|
+
) -> int:
|
|
175
|
+
"""Same as :func:`bump_compatible_pins_in_file` with ``distribution=\"easybind\"``."""
|
|
176
|
+
return bump_compatible_pins_in_file(pyproject, "easybind", version, dry_run=dry_run)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def main(argv: list[str] | None = None) -> int:
|
|
180
|
+
"""CLI: ``easybind-pin-pyproject`` (see ``--help``)."""
|
|
181
|
+
ap = argparse.ArgumentParser(
|
|
182
|
+
description=(
|
|
183
|
+
"Set every {distribution}~= pin in pyproject.toml "
|
|
184
|
+
"(default distribution: easybind; default version: latest on PyPI)."
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
ap.add_argument(
|
|
188
|
+
"--distribution",
|
|
189
|
+
"-d",
|
|
190
|
+
default="easybind",
|
|
191
|
+
metavar="NAME",
|
|
192
|
+
help="PyPI distribution name to match in pins (default: easybind), e.g. cppdantic, easybind",
|
|
193
|
+
)
|
|
194
|
+
ap.add_argument(
|
|
195
|
+
"--pyproject",
|
|
196
|
+
type=Path,
|
|
197
|
+
default=None,
|
|
198
|
+
help="path to pyproject.toml (default: ./pyproject.toml under cwd)",
|
|
199
|
+
)
|
|
200
|
+
ap.add_argument(
|
|
201
|
+
"--version",
|
|
202
|
+
metavar="X.Y.Z",
|
|
203
|
+
default=None,
|
|
204
|
+
help="pin to this version (default: PyPI latest for --distribution, unless --installed)",
|
|
205
|
+
)
|
|
206
|
+
ap.add_argument(
|
|
207
|
+
"--installed",
|
|
208
|
+
action="store_true",
|
|
209
|
+
help="use the version of --distribution installed in the current environment",
|
|
210
|
+
)
|
|
211
|
+
ap.add_argument("--dry-run", action="store_true", help="do not write the file")
|
|
212
|
+
ns = ap.parse_args(argv)
|
|
213
|
+
|
|
214
|
+
if ns.version and ns.installed:
|
|
215
|
+
print("error: use only one of --version or --installed", file=sys.stderr)
|
|
216
|
+
return 2
|
|
217
|
+
|
|
218
|
+
pyproject = ns.pyproject if ns.pyproject is not None else Path.cwd() / "pyproject.toml"
|
|
219
|
+
if not pyproject.is_file():
|
|
220
|
+
print(f"error: {pyproject} not found", file=sys.stderr)
|
|
221
|
+
return 2
|
|
222
|
+
|
|
223
|
+
dist = ns.distribution.strip()
|
|
224
|
+
if not dist:
|
|
225
|
+
print("error: empty --distribution", file=sys.stderr)
|
|
226
|
+
return 2
|
|
227
|
+
|
|
228
|
+
if ns.installed:
|
|
229
|
+
ver = installed_distribution_version(dist)
|
|
230
|
+
elif ns.version:
|
|
231
|
+
ver = ns.version.strip()
|
|
232
|
+
else:
|
|
233
|
+
ver = fetch_pypi_version(dist)
|
|
234
|
+
|
|
235
|
+
if not _VERSION_RE.match(ver):
|
|
236
|
+
print(f"error: bad version string: {ver!r}", file=sys.stderr)
|
|
237
|
+
return 2
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
n = bump_compatible_pins_in_file(pyproject, dist, ver, dry_run=ns.dry_run)
|
|
241
|
+
except ValueError as e:
|
|
242
|
+
print(f"error: {e}", file=sys.stderr)
|
|
243
|
+
return 1
|
|
244
|
+
|
|
245
|
+
action = "would update" if ns.dry_run else "updated"
|
|
246
|
+
print(f"{action} {n} {dist}~= pin(s) to ~={ver} in {pyproject}")
|
|
247
|
+
return 0
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Plan the next ``vMAJOR.MINOR.PATCH`` tag and ``git fetch`` / ``tag`` / ``push`` commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
_TAG_RE = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _run(cmd: list[str], *, cwd: Path, check: bool = True) -> subprocess.CompletedProcess[str]:
|
|
13
|
+
return subprocess.run(cmd, cwd=cwd, check=check, text=True, capture_output=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def latest_v_tag(repo: Path) -> str | None:
|
|
17
|
+
"""Newest ``v*`` semver tag by version tuple."""
|
|
18
|
+
p = _run(["git", "tag", "-l", "v*"], cwd=repo, check=False)
|
|
19
|
+
if p.returncode != 0:
|
|
20
|
+
return None
|
|
21
|
+
tags: list[str] = []
|
|
22
|
+
for line in p.stdout.splitlines():
|
|
23
|
+
line = line.strip()
|
|
24
|
+
if line and _TAG_RE.match(line):
|
|
25
|
+
tags.append(line)
|
|
26
|
+
if not tags:
|
|
27
|
+
return None
|
|
28
|
+
return max(tags, key=lambda t: _parse_semver(t))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_semver(tag: str) -> tuple[int, int, int]:
|
|
32
|
+
m = _TAG_RE.fullmatch(tag.strip())
|
|
33
|
+
if not m:
|
|
34
|
+
raise ValueError(f"not a vMAJOR.MINOR.PATCH tag: {tag!r}")
|
|
35
|
+
return int(m.group(1)), int(m.group(2)), int(m.group(3))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _bump(major: int, minor: int, patch: int, *, level: str) -> tuple[int, int, int]:
|
|
39
|
+
if level == "major":
|
|
40
|
+
return major + 1, 0, 0
|
|
41
|
+
if level == "minor":
|
|
42
|
+
return major, minor + 1, 0
|
|
43
|
+
return major, minor, patch + 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _format_tag(t: tuple[int, int, int]) -> str:
|
|
47
|
+
return f"v{t[0]}.{t[1]}.{t[2]}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def next_v_tag(repo: Path, *, level: str) -> tuple[str | None, str]:
|
|
51
|
+
"""Return ``(latest_tag_or_none, new_tag)`` for the given bump *level*."""
|
|
52
|
+
latest = latest_v_tag(repo)
|
|
53
|
+
if latest is None:
|
|
54
|
+
ma, mi, pa = 0, 0, 0
|
|
55
|
+
else:
|
|
56
|
+
ma, mi, pa = _parse_semver(latest)
|
|
57
|
+
new_tag = _format_tag(_bump(ma, mi, pa, level=level))
|
|
58
|
+
return latest, new_tag
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def tag_push_commands(
|
|
62
|
+
new_tag: str,
|
|
63
|
+
*,
|
|
64
|
+
remote: str,
|
|
65
|
+
branch: str,
|
|
66
|
+
no_pull: bool,
|
|
67
|
+
no_push: bool,
|
|
68
|
+
tag_message: str,
|
|
69
|
+
) -> list[list[str]]:
|
|
70
|
+
"""Commands: optional fetch/checkout/pull, annotated tag, optional push branch + tag."""
|
|
71
|
+
cmds: list[list[str]] = []
|
|
72
|
+
if not no_pull:
|
|
73
|
+
cmds.extend(
|
|
74
|
+
[
|
|
75
|
+
["git", "fetch", remote],
|
|
76
|
+
["git", "checkout", branch],
|
|
77
|
+
["git", "pull", remote, branch],
|
|
78
|
+
]
|
|
79
|
+
)
|
|
80
|
+
cmds.append(["git", "tag", "-a", new_tag, "-m", tag_message])
|
|
81
|
+
if not no_push:
|
|
82
|
+
cmds.append(["git", "push", remote, branch])
|
|
83
|
+
cmds.append(["git", "push", remote, new_tag])
|
|
84
|
+
return cmds
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def ensure_clean_worktree(repo: Path) -> None:
|
|
88
|
+
p = _run(["git", "status", "--porcelain"], cwd=repo, check=True)
|
|
89
|
+
if p.stdout.strip():
|
|
90
|
+
raise SystemExit(
|
|
91
|
+
"working tree is not clean; commit or stash before tagging.\n" + p.stdout
|
|
92
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Next semver ``v*`` tag and ``git fetch`` / ``tag`` / ``push`` (easybind repository)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from easybind.devtools.release_helpers import (
|
|
11
|
+
ensure_clean_worktree,
|
|
12
|
+
next_v_tag,
|
|
13
|
+
tag_push_commands,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main(argv: list[str] | None = None, *, repo: Path | None = None) -> int:
|
|
18
|
+
"""Entry point for ``easybind-release-tag`` and ``scripts/release_tag.py``.
|
|
19
|
+
|
|
20
|
+
*repo* defaults to the current working directory (run from the repository root).
|
|
21
|
+
"""
|
|
22
|
+
ap = argparse.ArgumentParser(
|
|
23
|
+
description=(
|
|
24
|
+
"Create and push the next easybind release tag from the latest ``v*`` git tag.\n\n"
|
|
25
|
+
"Default: bump **patch**. Use ``--minor`` / ``--major`` for other segments.\n"
|
|
26
|
+
"If there is no ``v*`` tag yet, starts from **v0.0.0**, then bumps.\n\n"
|
|
27
|
+
"**GitHub vs PyPI:** the tag appears immediately; PyPI upload can still fail."
|
|
28
|
+
),
|
|
29
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
30
|
+
)
|
|
31
|
+
g = ap.add_mutually_exclusive_group()
|
|
32
|
+
g.add_argument("--patch", action="store_const", dest="level", const="patch", help="bump patch (default)")
|
|
33
|
+
g.add_argument("--minor", action="store_const", dest="level", const="minor", help="bump minor, reset patch")
|
|
34
|
+
g.add_argument("--major", action="store_const", dest="level", const="major", help="bump major")
|
|
35
|
+
ap.set_defaults(level="patch")
|
|
36
|
+
ap.add_argument("--dry-run", action="store_true", help="print commands only; no git writes")
|
|
37
|
+
ap.add_argument("--no-pull", action="store_true", help="skip fetch/checkout/pull")
|
|
38
|
+
ap.add_argument("--no-push", action="store_true", help="tag locally only; do not push")
|
|
39
|
+
ap.add_argument("--remote", default="origin", help="git remote (default: origin)")
|
|
40
|
+
ap.add_argument("--branch", default="main", help="branch to update (default: main)")
|
|
41
|
+
ap.add_argument(
|
|
42
|
+
"--repo",
|
|
43
|
+
type=Path,
|
|
44
|
+
default=None,
|
|
45
|
+
help="git repository root (default: current working directory)",
|
|
46
|
+
)
|
|
47
|
+
ns = ap.parse_args(argv)
|
|
48
|
+
|
|
49
|
+
root = repo if repo is not None else ns.repo
|
|
50
|
+
if root is None:
|
|
51
|
+
root = Path.cwd()
|
|
52
|
+
root = root.resolve()
|
|
53
|
+
if not (root / ".git").exists() and not (root / ".git").is_file():
|
|
54
|
+
print(f"error: no git state at {root}", file=sys.stderr)
|
|
55
|
+
return 2
|
|
56
|
+
|
|
57
|
+
latest, new_tag = next_v_tag(root, level=ns.level)
|
|
58
|
+
msg = f"easybind {new_tag.removeprefix('v')}"
|
|
59
|
+
cmds = tag_push_commands(
|
|
60
|
+
new_tag,
|
|
61
|
+
remote=ns.remote,
|
|
62
|
+
branch=ns.branch,
|
|
63
|
+
no_pull=ns.no_pull,
|
|
64
|
+
no_push=ns.no_push,
|
|
65
|
+
tag_message=msg,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
print(f"latest tag: {latest or '(none)'}")
|
|
69
|
+
print(f"next tag: {new_tag} (bump {ns.level})")
|
|
70
|
+
|
|
71
|
+
if ns.dry_run:
|
|
72
|
+
print("--- dry-run; commands not executed ---")
|
|
73
|
+
for c in cmds:
|
|
74
|
+
print("+", " ".join(c))
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
ensure_clean_worktree(root)
|
|
78
|
+
for c in cmds:
|
|
79
|
+
subprocess.run(c, cwd=root, check=True)
|
|
80
|
+
|
|
81
|
+
if ns.no_push:
|
|
82
|
+
print(f"done: created {new_tag} locally (not pushed)")
|
|
83
|
+
else:
|
|
84
|
+
print(f"done: pushed {new_tag} (remote={ns.remote}, branch={ns.branch})")
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Poll PyPI until a ``{distribution}~=…`` pin is published."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from easybind.devtools.pin_pyproject import wait_pypi_for_compatible_pin
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv: list[str] | None = None) -> int:
|
|
13
|
+
"""Entry point for ``easybind-wait-pypi`` and ``scripts/wait_pypi_release.py``."""
|
|
14
|
+
ap = argparse.ArgumentParser(
|
|
15
|
+
description=(
|
|
16
|
+
"Poll PyPI until a release exists for the version pinned as "
|
|
17
|
+
"``{distribution}~=X.Y.Z`` in pyproject.toml."
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
ap.add_argument(
|
|
21
|
+
"--pyproject",
|
|
22
|
+
type=Path,
|
|
23
|
+
default=Path("pyproject.toml"),
|
|
24
|
+
help="path to pyproject.toml (default: ./pyproject.toml)",
|
|
25
|
+
)
|
|
26
|
+
ap.add_argument(
|
|
27
|
+
"--distribution",
|
|
28
|
+
default="easybind",
|
|
29
|
+
help="distribution name in ``{name}~=…`` pins (default: easybind)",
|
|
30
|
+
)
|
|
31
|
+
ap.add_argument("--timeout", type=int, default=1800, metavar="SEC", help="max wait (default: 1800)")
|
|
32
|
+
ap.add_argument("--interval", type=int, default=30, metavar="SEC", help="seconds between checks (default: 30)")
|
|
33
|
+
ns = ap.parse_args(argv)
|
|
34
|
+
|
|
35
|
+
pp = ns.pyproject.resolve()
|
|
36
|
+
if not pp.is_file():
|
|
37
|
+
print(f"error: not a file: {pp}", file=sys.stderr)
|
|
38
|
+
return 2
|
|
39
|
+
|
|
40
|
+
text = pp.read_text(encoding="utf-8")
|
|
41
|
+
try:
|
|
42
|
+
version, attempts = wait_pypi_for_compatible_pin(
|
|
43
|
+
text,
|
|
44
|
+
ns.distribution,
|
|
45
|
+
timeout_s=float(ns.timeout),
|
|
46
|
+
interval_s=float(ns.interval),
|
|
47
|
+
verbose=True,
|
|
48
|
+
)
|
|
49
|
+
except ValueError as e:
|
|
50
|
+
raise SystemExit(str(e)) from None
|
|
51
|
+
except TimeoutError as e:
|
|
52
|
+
print(
|
|
53
|
+
f"error: {e}. "
|
|
54
|
+
"Confirm the dependency’s release workflow finished and PyPI has the artifacts.",
|
|
55
|
+
file=sys.stderr,
|
|
56
|
+
)
|
|
57
|
+
return 1
|
|
58
|
+
|
|
59
|
+
print(f"ok: {ns.distribution} {version} is on PyPI (attempt {attempts})", flush=True)
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
raise SystemExit(main())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|