easybind 0.2.4__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.
Files changed (38) hide show
  1. {easybind-0.2.4 → easybind-0.2.5}/PKG-INFO +10 -1
  2. {easybind-0.2.4 → easybind-0.2.5}/README.md +9 -0
  3. {easybind-0.2.4 → easybind-0.2.5}/RELEASING.md +4 -0
  4. {easybind-0.2.4 → easybind-0.2.5}/pyproject.toml +7 -0
  5. easybind-0.2.5/pyrightconfig.json +4 -0
  6. easybind-0.2.5/scripts/release_tag.py +16 -0
  7. easybind-0.2.5/scripts/wait_pypi_release.py +16 -0
  8. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/devtools/__init__.py +18 -0
  9. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/devtools/pin_pyproject.py +83 -2
  10. easybind-0.2.5/src/easybind/devtools/release_helpers.py +92 -0
  11. easybind-0.2.5/src/easybind/devtools/release_tag.py +89 -0
  12. easybind-0.2.5/src/easybind/devtools/wait_pypi.py +64 -0
  13. easybind-0.2.4/scripts/release_tag.py +0 -197
  14. {easybind-0.2.4 → easybind-0.2.5}/.clangd +0 -0
  15. {easybind-0.2.4 → easybind-0.2.5}/.github/workflows/publish.yml +0 -0
  16. {easybind-0.2.4 → easybind-0.2.5}/.gitignore +0 -0
  17. {easybind-0.2.4 → easybind-0.2.5}/.vscode/tasks.json +0 -0
  18. {easybind-0.2.4 → easybind-0.2.5}/CMakeLists.txt +0 -0
  19. {easybind-0.2.4 → easybind-0.2.5}/LICENSE +0 -0
  20. {easybind-0.2.4 → easybind-0.2.5}/NOTICE +0 -0
  21. {easybind-0.2.4 → easybind-0.2.5}/cmake/easybind_dependencies.cmake +0 -0
  22. {easybind-0.2.4 → easybind-0.2.5}/cmake/easybind_pip.cmake +0 -0
  23. {easybind-0.2.4 → easybind-0.2.5}/scripts/clangd-update.sh +0 -0
  24. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/__init__.cpp +0 -0
  25. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/bind.hpp +0 -0
  26. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/export.hpp +0 -0
  27. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/module/__init__.cpp +0 -0
  28. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/module/node.cpp +0 -0
  29. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/module/node.hpp +0 -0
  30. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/module/node__init__.cpp +0 -0
  31. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/module/ns_module.hpp +0 -0
  32. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/py.typed +0 -0
  33. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/sample/__init__.cpp +0 -0
  34. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/sample/sample.cpp +0 -0
  35. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/sample/sample.hpp +0 -0
  36. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/sample/test1.py +0 -0
  37. {easybind-0.2.4 → easybind-0.2.5}/src/easybind/sample/test2/__init__.py +0 -0
  38. {easybind-0.2.4 → 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.4
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>
@@ -101,6 +101,13 @@ easybind-pin-pyproject --distribution cppdantic
101
101
  easybind-pin-pyproject --pyproject /path/to/pyproject.toml
102
102
  ```
103
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
+
104
111
  **Library:**
105
112
 
106
113
  ```python
@@ -120,6 +127,8 @@ bump_compatible_pins_in_file("pyproject.toml", "cppdantic", ver)
120
127
 
121
128
  Shorthands **`bump_easybind_compatible_pins`** / **`bump_easybind_compatible_pins_in_file`** remain for **`distribution=\"easybind\"`** only.
122
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
+
123
132
  ## Core idea
124
133
  - Each namespace/module defines a `ModuleNode` and a bind callback.
125
134
  - The module entry point calls `apply_init` to run the callback and recurse.
@@ -72,6 +72,13 @@ easybind-pin-pyproject --distribution cppdantic
72
72
  easybind-pin-pyproject --pyproject /path/to/pyproject.toml
73
73
  ```
74
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
+
75
82
  **Library:**
76
83
 
77
84
  ```python
@@ -91,6 +98,8 @@ bump_compatible_pins_in_file("pyproject.toml", "cppdantic", ver)
91
98
 
92
99
  Shorthands **`bump_easybind_compatible_pins`** / **`bump_easybind_compatible_pins_in_file`** remain for **`distribution=\"easybind\"`** only.
93
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
+
94
103
  ## Core idea
95
104
  - Each namespace/module defines a `ModuleNode` and a bind callback.
96
105
  - The module entry point calls `apply_init` to run the callback and recurse.
@@ -33,10 +33,14 @@ Configure credentials with **API token** (`~/.pypirc` or environment variables)
33
33
 
34
34
  Run from the **easybind** repo root. Requires a **clean** working tree.
35
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
+
36
38
  ## CI upload
37
39
 
38
40
  Pushing a tag matching `v*` triggers `.github/workflows/publish.yml`, which builds and uploads to PyPI.
39
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
+
40
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/)).
41
45
 
42
46
  ## Platforms
@@ -32,8 +32,15 @@ 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
+
35
40
  [project.scripts]
36
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"
37
44
 
38
45
  [project.optional-dependencies]
39
46
  dev = [
@@ -0,0 +1,4 @@
1
+ {
2
+ "extraPaths": ["src"],
3
+ "include": ["src", "scripts"]
4
+ }
@@ -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())
@@ -5,8 +5,18 @@ from easybind.devtools.pin_pyproject import (
5
5
  bump_compatible_pins_in_file,
6
6
  bump_easybind_compatible_pins,
7
7
  bump_easybind_compatible_pins_in_file,
8
+ compatible_pin_versions,
8
9
  fetch_pypi_version,
9
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,
10
20
  )
11
21
 
12
22
  __all__ = [
@@ -14,6 +24,14 @@ __all__ = [
14
24
  "bump_compatible_pins_in_file",
15
25
  "bump_easybind_compatible_pins",
16
26
  "bump_easybind_compatible_pins_in_file",
27
+ "compatible_pin_versions",
28
+ "ensure_clean_worktree",
17
29
  "fetch_pypi_version",
18
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",
19
37
  ]
@@ -6,8 +6,10 @@ import argparse
6
6
  import json
7
7
  import re
8
8
  import sys
9
- import urllib.request
9
+ import time
10
10
  from pathlib import Path
11
+ from urllib.error import HTTPError
12
+ from urllib.request import urlopen
11
13
 
12
14
  # PEP 440 version suitable for ``~=`` RHS in typical pyproject pins (numeric release).
13
15
  _VERSION_RE = re.compile(r"^[0-9]+(?:\.[0-9]+)*$")
@@ -28,11 +30,90 @@ def _pin_pattern(distribution: str) -> re.Pattern[str]:
28
30
  def fetch_pypi_version(package: str = "easybind", *, timeout_s: float = 30.0) -> str:
29
31
  """Return ``info.version`` from ``https://pypi.org/pypi/{package}/json``."""
30
32
  url = f"https://pypi.org/pypi/{package}/json"
31
- with urllib.request.urlopen(url, timeout=timeout_s) as r:
33
+ with urlopen(url, timeout=timeout_s) as r:
32
34
  data = json.load(r)
33
35
  return str(data["info"]["version"])
34
36
 
35
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
+
36
117
  def installed_distribution_version(package: str = "easybind") -> str:
37
118
  """Return ``importlib.metadata.version(package)`` for the active environment."""
38
119
  from importlib.metadata import version
@@ -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())
@@ -1,197 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Create and push the next easybind release tag from the latest ``v*`` git tag.
3
-
4
- Default: bump **patch** (0.2.3 → 0.2.4). Use ``--minor`` or ``--major`` to bump those
5
- segments instead (resets lower segments to 0).
6
-
7
- If there is no ``v*`` tag yet, starts from **v0.0.0** as the notional baseline, then bumps.
8
-
9
- Examples::
10
-
11
- ./scripts/release_tag.py --dry-run
12
- ./scripts/release_tag.py
13
- ./scripts/release_tag.py --minor
14
- ./scripts/release_tag.py --major --no-push
15
-
16
- Requires a clean ``main`` sync (fetch/checkout/pull) unless ``--no-pull``.
17
- """
18
-
19
- from __future__ import annotations
20
-
21
- import argparse
22
- import re
23
- import subprocess
24
- import sys
25
- from pathlib import Path
26
-
27
- _REPO = Path(__file__).resolve().parents[1]
28
- _TAG_RE = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$")
29
-
30
-
31
- def _run(
32
- cmd: list[str],
33
- *,
34
- cwd: Path,
35
- check: bool = True,
36
- ) -> subprocess.CompletedProcess[str]:
37
- return subprocess.run(
38
- cmd,
39
- cwd=cwd,
40
- check=check,
41
- text=True,
42
- capture_output=True,
43
- )
44
-
45
-
46
- def _latest_version_tag(repo: Path) -> str | None:
47
- """Newest ``vMAJOR.MINOR.PATCH`` tag by version tuple (no reliance on git sort)."""
48
- p = _run(["git", "tag", "-l", "v*"], cwd=repo, check=False)
49
- if p.returncode != 0:
50
- return None
51
- tags: list[str] = []
52
- for line in p.stdout.splitlines():
53
- line = line.strip()
54
- if line and _TAG_RE.match(line):
55
- tags.append(line)
56
- if not tags:
57
- return None
58
- return max(tags, key=lambda t: _parse_semver(t))
59
-
60
-
61
- def _parse_semver(tag: str) -> tuple[int, int, int]:
62
- m = _TAG_RE.fullmatch(tag.strip())
63
- if not m:
64
- raise ValueError(f"not a vMAJOR.MINOR.PATCH tag: {tag!r}")
65
- return int(m.group(1)), int(m.group(2)), int(m.group(3))
66
-
67
-
68
- def _bump(
69
- major: int,
70
- minor: int,
71
- patch: int,
72
- *,
73
- level: str,
74
- ) -> tuple[int, int, int]:
75
- if level == "major":
76
- return major + 1, 0, 0
77
- if level == "minor":
78
- return major, minor + 1, 0
79
- return major, minor, patch + 1
80
-
81
-
82
- def _format_tag(t: tuple[int, int, int]) -> str:
83
- return f"v{t[0]}.{t[1]}.{t[2]}"
84
-
85
-
86
- def _ensure_clean(repo: Path) -> None:
87
- p = _run(["git", "status", "--porcelain"], cwd=repo, check=True)
88
- if p.stdout.strip():
89
- raise SystemExit(
90
- "working tree is not clean; commit or stash before tagging.\n"
91
- + p.stdout
92
- )
93
-
94
-
95
- def main() -> int:
96
- ap = argparse.ArgumentParser(description=__doc__)
97
- g = ap.add_mutually_exclusive_group()
98
- g.add_argument(
99
- "--patch",
100
- action="store_const",
101
- dest="level",
102
- const="patch",
103
- help="bump patch (default)",
104
- )
105
- g.add_argument(
106
- "--minor",
107
- action="store_const",
108
- dest="level",
109
- const="minor",
110
- help="bump minor, reset patch to 0",
111
- )
112
- g.add_argument(
113
- "--major",
114
- action="store_const",
115
- dest="level",
116
- const="major",
117
- help="bump major, reset minor and patch to 0",
118
- )
119
- ap.set_defaults(level="patch")
120
- ap.add_argument(
121
- "--dry-run",
122
- action="store_true",
123
- help="print the next tag and commands only; no git writes",
124
- )
125
- ap.add_argument(
126
- "--no-pull",
127
- action="store_true",
128
- help="do not fetch/checkout main/pull (still creates tag unless --dry-run)",
129
- )
130
- ap.add_argument(
131
- "--no-push",
132
- action="store_true",
133
- help="create the annotated tag locally but do not push main or the tag",
134
- )
135
- ap.add_argument(
136
- "--remote",
137
- default="origin",
138
- help="git remote for fetch/push (default: origin)",
139
- )
140
- ap.add_argument(
141
- "--branch",
142
- default="main",
143
- help="branch to update before tagging (default: main)",
144
- )
145
- ns = ap.parse_args()
146
-
147
- repo = _REPO
148
- if not (repo / ".git").exists() and not (repo / ".git").is_file():
149
- print(f"error: no git state at {repo}", file=sys.stderr)
150
- return 2
151
-
152
- latest = _latest_version_tag(repo)
153
- if latest is None:
154
- ma, mi, pa = 0, 0, 0
155
- else:
156
- ma, mi, pa = _parse_semver(latest)
157
-
158
- new_ver = _bump(ma, mi, pa, level=ns.level)
159
- new_tag = _format_tag(new_ver)
160
-
161
- print(f"latest tag: {latest or '(none)'}")
162
- print(f"next tag: {new_tag} (bump {ns.level})")
163
-
164
- cmds: list[list[str]] = []
165
- if not ns.no_pull:
166
- cmds.extend(
167
- [
168
- ["git", "fetch", ns.remote],
169
- ["git", "checkout", ns.branch],
170
- ["git", "pull", ns.remote, ns.branch],
171
- ]
172
- )
173
- cmds.append(["git", "tag", "-a", new_tag, "-m", f"easybind {new_tag.removeprefix('v')}"])
174
- if not ns.no_push:
175
- cmds.append(["git", "push", ns.remote, ns.branch])
176
- cmds.append(["git", "push", ns.remote, new_tag])
177
-
178
- if ns.dry_run:
179
- print("--- dry-run; commands not executed ---")
180
- for c in cmds:
181
- print("+", " ".join(c))
182
- return 0
183
-
184
- _ensure_clean(repo)
185
-
186
- for c in cmds:
187
- subprocess.run(c, cwd=repo, check=True)
188
-
189
- if ns.no_push:
190
- print(f"done: created {new_tag} locally (not pushed)")
191
- else:
192
- print(f"done: pushed {new_tag} (remote={ns.remote}, branch={ns.branch})")
193
- return 0
194
-
195
-
196
- if __name__ == "__main__":
197
- 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