easybind 0.2.5__tar.gz → 0.2.8__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.5 → easybind-0.2.8}/PKG-INFO +7 -2
- {easybind-0.2.5 → easybind-0.2.8}/README.md +5 -1
- {easybind-0.2.5 → easybind-0.2.8}/pyproject.toml +1 -1
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/devtools/__init__.py +8 -0
- easybind-0.2.8/src/easybind/devtools/pin_pyproject.py +454 -0
- easybind-0.2.5/src/easybind/devtools/pin_pyproject.py +0 -251
- {easybind-0.2.5 → easybind-0.2.8}/.clangd +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/.github/workflows/publish.yml +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/.gitignore +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/.vscode/tasks.json +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/CMakeLists.txt +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/LICENSE +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/NOTICE +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/RELEASING.md +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/cmake/easybind_dependencies.cmake +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/cmake/easybind_pip.cmake +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/pyrightconfig.json +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/scripts/clangd-update.sh +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/scripts/release_tag.py +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/scripts/wait_pypi_release.py +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/__init__.cpp +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/bind.hpp +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/devtools/release_helpers.py +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/devtools/release_tag.py +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/devtools/wait_pypi.py +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/export.hpp +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/module/__init__.cpp +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/module/node.cpp +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/module/node.hpp +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/module/node__init__.cpp +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/module/ns_module.hpp +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/py.typed +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/sample/__init__.cpp +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/sample/sample.cpp +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/sample/sample.hpp +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/sample/test1.py +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/src/easybind/sample/test2/__init__.py +0 -0
- {easybind-0.2.5 → easybind-0.2.8}/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.8
|
|
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>
|
|
@@ -21,6 +21,7 @@ Project-URL: Repository, https://github.com/pymergetic/easybind
|
|
|
21
21
|
Project-URL: Issues, https://github.com/pymergetic/easybind/issues
|
|
22
22
|
Requires-Python: >=3.11
|
|
23
23
|
Requires-Dist: nanobind~=2.12.0
|
|
24
|
+
Requires-Dist: packaging>=23
|
|
24
25
|
Provides-Extra: dev
|
|
25
26
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
26
27
|
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
@@ -90,17 +91,21 @@ Use **`easybind.devtools`** or the **`easybind-pin-pyproject`** CLI to rewrite e
|
|
|
90
91
|
- **cppdantic** pinning **easybind** in several tables.
|
|
91
92
|
- A future project pinning **cppdantic** the same way: pass **`--distribution cppdantic`**.
|
|
92
93
|
|
|
93
|
-
**CLI** (defaults: **`distribution=easybind`**, version = latest on PyPI
|
|
94
|
+
**CLI** (defaults: **`distribution=easybind`**, version = latest on PyPI; run from the tree that contains **`pyproject.toml`**):
|
|
94
95
|
|
|
95
96
|
```bash
|
|
96
97
|
easybind-pin-pyproject --dry-run
|
|
97
98
|
easybind-pin-pyproject
|
|
99
|
+
easybind-pin-pyproject --from-github # GitHub OWNER/REPO from PyPI metadata for --distribution
|
|
100
|
+
easybind-pin-pyproject --from-github ORG/REPO # override when PyPI has no GitHub URL
|
|
98
101
|
easybind-pin-pyproject --installed
|
|
99
102
|
easybind-pin-pyproject --version 0.2.3
|
|
100
103
|
easybind-pin-pyproject --distribution cppdantic
|
|
101
104
|
easybind-pin-pyproject --pyproject /path/to/pyproject.toml
|
|
102
105
|
```
|
|
103
106
|
|
|
107
|
+
Use **`GITHUB_TOKEN`** for private GitHub repos or higher API rate limits.
|
|
108
|
+
|
|
104
109
|
**Other devtools CLIs** (after **`pip install`** / **`uv pip install -e .`**, or run **`scripts/…`** shims from a git checkout):
|
|
105
110
|
|
|
106
111
|
```bash
|
|
@@ -61,17 +61,21 @@ Use **`easybind.devtools`** or the **`easybind-pin-pyproject`** CLI to rewrite e
|
|
|
61
61
|
- **cppdantic** pinning **easybind** in several tables.
|
|
62
62
|
- A future project pinning **cppdantic** the same way: pass **`--distribution cppdantic`**.
|
|
63
63
|
|
|
64
|
-
**CLI** (defaults: **`distribution=easybind`**, version = latest on PyPI
|
|
64
|
+
**CLI** (defaults: **`distribution=easybind`**, version = latest on PyPI; run from the tree that contains **`pyproject.toml`**):
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
67
|
easybind-pin-pyproject --dry-run
|
|
68
68
|
easybind-pin-pyproject
|
|
69
|
+
easybind-pin-pyproject --from-github # GitHub OWNER/REPO from PyPI metadata for --distribution
|
|
70
|
+
easybind-pin-pyproject --from-github ORG/REPO # override when PyPI has no GitHub URL
|
|
69
71
|
easybind-pin-pyproject --installed
|
|
70
72
|
easybind-pin-pyproject --version 0.2.3
|
|
71
73
|
easybind-pin-pyproject --distribution cppdantic
|
|
72
74
|
easybind-pin-pyproject --pyproject /path/to/pyproject.toml
|
|
73
75
|
```
|
|
74
76
|
|
|
77
|
+
Use **`GITHUB_TOKEN`** for private GitHub repos or higher API rate limits.
|
|
78
|
+
|
|
75
79
|
**Other devtools CLIs** (after **`pip install`** / **`uv pip install -e .`**, or run **`scripts/…`** shims from a git checkout):
|
|
76
80
|
|
|
77
81
|
```bash
|
|
@@ -8,7 +8,7 @@ dynamic = ["version"]
|
|
|
8
8
|
description = "Self-registering nanobind helpers (import as easybind)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
|
-
dependencies = ["nanobind~=2.12.0"]
|
|
11
|
+
dependencies = ["nanobind~=2.12.0", "packaging>=23"]
|
|
12
12
|
license = "Apache-2.0"
|
|
13
13
|
license-files = ["LICENSE", "NOTICE"]
|
|
14
14
|
authors = [
|
|
@@ -6,8 +6,12 @@ from easybind.devtools.pin_pyproject import (
|
|
|
6
6
|
bump_easybind_compatible_pins,
|
|
7
7
|
bump_easybind_compatible_pins_in_file,
|
|
8
8
|
compatible_pin_versions,
|
|
9
|
+
compatible_release_pin_from_installed_version,
|
|
10
|
+
fetch_pypi_project_json,
|
|
9
11
|
fetch_pypi_version,
|
|
12
|
+
github_owner_repo_from_pypi_distribution,
|
|
10
13
|
installed_distribution_version,
|
|
14
|
+
latest_release_version_from_github,
|
|
11
15
|
pypi_release_exists,
|
|
12
16
|
single_compatible_pin_version,
|
|
13
17
|
wait_pypi_for_compatible_pin,
|
|
@@ -25,9 +29,13 @@ __all__ = [
|
|
|
25
29
|
"bump_easybind_compatible_pins",
|
|
26
30
|
"bump_easybind_compatible_pins_in_file",
|
|
27
31
|
"compatible_pin_versions",
|
|
32
|
+
"compatible_release_pin_from_installed_version",
|
|
28
33
|
"ensure_clean_worktree",
|
|
34
|
+
"fetch_pypi_project_json",
|
|
29
35
|
"fetch_pypi_version",
|
|
36
|
+
"github_owner_repo_from_pypi_distribution",
|
|
30
37
|
"installed_distribution_version",
|
|
38
|
+
"latest_release_version_from_github",
|
|
31
39
|
"latest_v_tag",
|
|
32
40
|
"next_v_tag",
|
|
33
41
|
"pypi_release_exists",
|
|
@@ -0,0 +1,454 @@
|
|
|
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 os
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from urllib.error import HTTPError
|
|
13
|
+
from urllib.request import Request, urlopen
|
|
14
|
+
|
|
15
|
+
# PEP 440 version suitable for ``~=`` RHS in typical pyproject pins (numeric release).
|
|
16
|
+
_VERSION_RE = re.compile(r"^[0-9]+(?:\.[0-9]+)*$")
|
|
17
|
+
|
|
18
|
+
# Release tags: ``vMAJOR.MINOR.PATCH`` (GitHub / git tag style).
|
|
19
|
+
_V_SEMVER_TAG = re.compile(r"^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _semver_tuple(version: str) -> tuple[int, ...]:
|
|
23
|
+
"""Numeric tuple for comparing ``X.Y.Z`` style pins."""
|
|
24
|
+
return tuple(int(p) for p in version.split("."))
|
|
25
|
+
|
|
26
|
+
# Reject pathological / mistaken distribution strings (full re.escape covers the rest).
|
|
27
|
+
_DIST_OK = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]*$")
|
|
28
|
+
|
|
29
|
+
# ``https://github.com/OWNER/REPO`` (and ``…/tree/…``, ``.git``, …).
|
|
30
|
+
_GITHUB_OWNER_REPO = re.compile(
|
|
31
|
+
r"https?://github\.com/([^/]+)/([^/#?]+)",
|
|
32
|
+
re.IGNORECASE,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _pin_pattern(distribution: str) -> re.Pattern[str]:
|
|
37
|
+
if not _DIST_OK.match(distribution):
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"invalid distribution name for pin replacement: {distribution!r} "
|
|
40
|
+
"(expected a PyPI-style name, e.g. easybind, cppdantic, scikit-build-core)"
|
|
41
|
+
)
|
|
42
|
+
return re.compile(rf"({re.escape(distribution)}~=)([0-9]+(?:\.[0-9]+)*)")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def fetch_pypi_project_json(distribution: str, *, timeout_s: float = 30.0) -> dict:
|
|
46
|
+
"""Return the JSON object from ``https://pypi.org/pypi/{distribution}/json``."""
|
|
47
|
+
url = f"https://pypi.org/pypi/{distribution}/json"
|
|
48
|
+
try:
|
|
49
|
+
with urlopen(url, timeout=timeout_s) as r:
|
|
50
|
+
return json.load(r)
|
|
51
|
+
except HTTPError as e:
|
|
52
|
+
if e.code == 404:
|
|
53
|
+
raise ValueError(f"no PyPI project named {distribution!r}") from e
|
|
54
|
+
raise
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def fetch_pypi_version(package: str = "easybind", *, timeout_s: float = 30.0) -> str:
|
|
58
|
+
"""Return ``info.version`` from ``https://pypi.org/pypi/{package}/json``."""
|
|
59
|
+
data = fetch_pypi_project_json(package, timeout_s=timeout_s)
|
|
60
|
+
return str(data["info"]["version"])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def github_owner_repo_from_pypi_distribution(distribution: str, *, timeout_s: float = 30.0) -> str:
|
|
64
|
+
"""Return ``OWNER/REPO`` by scanning PyPI ``home_page`` and ``project_urls`` for ``github.com``.
|
|
65
|
+
|
|
66
|
+
Works for any PyPI name whose metadata includes a ``github.com/OWNER/REPO`` link.
|
|
67
|
+
If nothing matches, raises ``ValueError`` — then pass ``--from-github OWNER/REPO`` explicitly.
|
|
68
|
+
"""
|
|
69
|
+
data = fetch_pypi_project_json(distribution, timeout_s=timeout_s)
|
|
70
|
+
info = data.get("info") or {}
|
|
71
|
+
urls: list[str] = []
|
|
72
|
+
pu = info.get("project_urls")
|
|
73
|
+
if isinstance(pu, dict):
|
|
74
|
+
for key in ("Source", "Repository", "Homepage", "Code"):
|
|
75
|
+
v = pu.get(key)
|
|
76
|
+
if isinstance(v, str) and v.strip():
|
|
77
|
+
urls.append(v.strip())
|
|
78
|
+
for v in pu.values():
|
|
79
|
+
if isinstance(v, str) and v.strip() and v.strip() not in urls:
|
|
80
|
+
urls.append(v.strip())
|
|
81
|
+
hp = info.get("home_page")
|
|
82
|
+
if isinstance(hp, str) and hp.strip():
|
|
83
|
+
urls.append(hp.strip())
|
|
84
|
+
|
|
85
|
+
for u in urls:
|
|
86
|
+
m = _GITHUB_OWNER_REPO.search(u)
|
|
87
|
+
if m:
|
|
88
|
+
repo = m.group(2).removesuffix(".git")
|
|
89
|
+
return f"{m.group(1)}/{repo}"
|
|
90
|
+
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"no github.com URL in PyPI metadata for {distribution!r}; "
|
|
93
|
+
"set project URLs on PyPI or pass --from-github OWNER/REPO"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def pypi_release_exists(package: str, version: str, *, timeout_s: float = 15.0) -> bool:
|
|
98
|
+
"""Return True if ``https://pypi.org/pypi/{package}/{version}/json`` exists (release uploaded)."""
|
|
99
|
+
url = f"https://pypi.org/pypi/{package}/{version}/json"
|
|
100
|
+
try:
|
|
101
|
+
with urlopen(url, timeout=timeout_s) as r:
|
|
102
|
+
return getattr(r, "status", 200) == 200
|
|
103
|
+
except HTTPError as e:
|
|
104
|
+
if e.code == 404:
|
|
105
|
+
return False
|
|
106
|
+
raise
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def compatible_pin_versions(pyproject_toml: str, distribution: str) -> list[str]:
|
|
110
|
+
"""Return every version string from ``{distribution}~=VERSION`` pins (order preserved)."""
|
|
111
|
+
pat = _pin_pattern(distribution)
|
|
112
|
+
return [m.group(2) for m in pat.finditer(pyproject_toml)]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def single_compatible_pin_version(
|
|
116
|
+
pyproject_toml: str,
|
|
117
|
+
distribution: str,
|
|
118
|
+
*,
|
|
119
|
+
empty_pins_suffix: str = "",
|
|
120
|
+
) -> str:
|
|
121
|
+
"""Return the version string if all ``{distribution}~=…`` pins agree; else raise ``ValueError``."""
|
|
122
|
+
vers = compatible_pin_versions(pyproject_toml, distribution)
|
|
123
|
+
if not vers:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"no `{distribution}~=...` pins in pyproject.toml{empty_pins_suffix}"
|
|
126
|
+
)
|
|
127
|
+
uniq = set(vers)
|
|
128
|
+
if len(uniq) != 1:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"{distribution}~= pins disagree: {sorted(uniq)!r}; fix pyproject.toml first"
|
|
131
|
+
)
|
|
132
|
+
return vers[0]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def wait_pypi_for_compatible_pin(
|
|
136
|
+
pyproject_toml: str,
|
|
137
|
+
distribution: str,
|
|
138
|
+
*,
|
|
139
|
+
timeout_s: float = 1800.0,
|
|
140
|
+
interval_s: float = 30.0,
|
|
141
|
+
verbose: bool = False,
|
|
142
|
+
) -> tuple[str, int]:
|
|
143
|
+
"""Poll PyPI until ``pypi_release_exists(distribution, version)`` for the pin.
|
|
144
|
+
|
|
145
|
+
Uses ``single_compatible_pin_version`` on *pyproject_toml*. Returns
|
|
146
|
+
``(resolved_version, attempt_count)``. Raises ``TimeoutError`` if the release
|
|
147
|
+
never appears within *timeout_s*. With *verbose*, log progress to stdout.
|
|
148
|
+
"""
|
|
149
|
+
version = single_compatible_pin_version(pyproject_toml, distribution)
|
|
150
|
+
if verbose:
|
|
151
|
+
print(
|
|
152
|
+
f"waiting for {distribution}=={version} on PyPI "
|
|
153
|
+
f"(timeout {timeout_s:.0f}s, interval {interval_s:.0f}s)...",
|
|
154
|
+
flush=True,
|
|
155
|
+
)
|
|
156
|
+
deadline = time.monotonic() + timeout_s
|
|
157
|
+
attempt = 0
|
|
158
|
+
while True:
|
|
159
|
+
attempt += 1
|
|
160
|
+
if pypi_release_exists(distribution, version):
|
|
161
|
+
return version, attempt
|
|
162
|
+
if time.monotonic() >= deadline:
|
|
163
|
+
raise TimeoutError(
|
|
164
|
+
f"timed out waiting for {distribution}=={version} on PyPI "
|
|
165
|
+
f"after {timeout_s}s ({attempt} attempts)"
|
|
166
|
+
)
|
|
167
|
+
if verbose:
|
|
168
|
+
remaining = int(max(0, deadline - time.monotonic()))
|
|
169
|
+
print(
|
|
170
|
+
f"attempt {attempt}: not yet (retry in {interval_s:.0f}s, ~{remaining}s left)...",
|
|
171
|
+
flush=True,
|
|
172
|
+
)
|
|
173
|
+
time.sleep(interval_s)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def installed_distribution_version(package: str = "easybind") -> str:
|
|
177
|
+
"""Return ``importlib.metadata.version(package)`` for the active environment."""
|
|
178
|
+
from importlib.metadata import version
|
|
179
|
+
|
|
180
|
+
return version(package)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def compatible_release_pin_from_installed_version(pep440: str) -> str:
|
|
184
|
+
"""Map an installed PEP 440 version (e.g. setuptools-scm ``0.2.7.post1.dev0``) to ``MAJOR.MINOR.PATCH`` for ``~=`` pins."""
|
|
185
|
+
from packaging.version import Version
|
|
186
|
+
|
|
187
|
+
v = Version(pep440)
|
|
188
|
+
rel = list(v.release)
|
|
189
|
+
if not rel:
|
|
190
|
+
raise ValueError(f"no release segment in {pep440!r}")
|
|
191
|
+
while len(rel) < 3:
|
|
192
|
+
rel.append(0)
|
|
193
|
+
return f"{rel[0]}.{rel[1]}.{rel[2]}"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _github_request_json(url: str, *, token: str | None, timeout_s: float) -> object:
|
|
197
|
+
req = Request(url, headers={"Accept": "application/vnd.github+json"})
|
|
198
|
+
if token:
|
|
199
|
+
req.add_header("Authorization", f"Bearer {token}")
|
|
200
|
+
with urlopen(req, timeout=timeout_s) as r:
|
|
201
|
+
return json.load(r)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def latest_release_version_from_github(
|
|
205
|
+
owner_repo: str,
|
|
206
|
+
*,
|
|
207
|
+
token: str | None = None,
|
|
208
|
+
timeout_s: float = 30.0,
|
|
209
|
+
) -> str:
|
|
210
|
+
"""Return the highest ``vMAJOR.MINOR.PATCH`` tag on GitHub as ``X.Y.Z`` (no local git clone).
|
|
211
|
+
|
|
212
|
+
Uses ``GET /repos/{owner}/{repo}/tags`` (paginated). Works when the tag exists on GitHub but
|
|
213
|
+
PyPI has not published the wheel yet. *owner_repo* is ``OWNER/REPO``.
|
|
214
|
+
|
|
215
|
+
Set ``GITHUB_TOKEN`` or ``GH_TOKEN`` (or pass *token*) for private repos or higher rate limits.
|
|
216
|
+
"""
|
|
217
|
+
s = owner_repo.strip().strip("/")
|
|
218
|
+
parts = s.split("/")
|
|
219
|
+
if len(parts) != 2 or not parts[0] or not parts[1]:
|
|
220
|
+
raise ValueError(f"expected OWNER/REPO, got {owner_repo!r}")
|
|
221
|
+
|
|
222
|
+
owner, repo = parts[0], parts[1]
|
|
223
|
+
tok = token if token is not None else os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
|
224
|
+
|
|
225
|
+
best: tuple[int, int, int] | None = None
|
|
226
|
+
best_ver: str | None = None
|
|
227
|
+
page = 1
|
|
228
|
+
per_page = 100
|
|
229
|
+
while page <= 100:
|
|
230
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/tags?per_page={per_page}&page={page}"
|
|
231
|
+
try:
|
|
232
|
+
data = _github_request_json(url, token=tok, timeout_s=timeout_s)
|
|
233
|
+
except HTTPError as e:
|
|
234
|
+
detail = ""
|
|
235
|
+
try:
|
|
236
|
+
detail = e.read().decode("utf-8", errors="replace")[:500]
|
|
237
|
+
except Exception:
|
|
238
|
+
pass
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"GitHub API HTTP {e.code} for {owner}/{repo} (set GITHUB_TOKEN for private repos): {detail}"
|
|
241
|
+
) from e
|
|
242
|
+
if not isinstance(data, list) or len(data) == 0:
|
|
243
|
+
break
|
|
244
|
+
for item in data:
|
|
245
|
+
if not isinstance(item, dict):
|
|
246
|
+
continue
|
|
247
|
+
name = item.get("name")
|
|
248
|
+
if not isinstance(name, str):
|
|
249
|
+
continue
|
|
250
|
+
m = _V_SEMVER_TAG.fullmatch(name.strip())
|
|
251
|
+
if not m:
|
|
252
|
+
continue
|
|
253
|
+
t = (int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
|
254
|
+
if best is None or t > best:
|
|
255
|
+
best = t
|
|
256
|
+
best_ver = f"{m.group(1)}.{m.group(2)}.{m.group(3)}"
|
|
257
|
+
if len(data) < per_page:
|
|
258
|
+
break
|
|
259
|
+
page += 1
|
|
260
|
+
|
|
261
|
+
if best_ver is None:
|
|
262
|
+
raise ValueError(f"no vMAJOR.MINOR.PATCH tags found for github.com/{owner}/{repo}")
|
|
263
|
+
return best_ver
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def bump_compatible_pins(pyproject_toml: str, distribution: str, version: str) -> tuple[str, int]:
|
|
267
|
+
"""Replace each ``{distribution}~=X.Y.Z`` with ``{distribution}~={version}``.
|
|
268
|
+
|
|
269
|
+
Returns ``(new_text, replacement_count)``.
|
|
270
|
+
"""
|
|
271
|
+
if not _VERSION_RE.match(version):
|
|
272
|
+
raise ValueError(f"invalid PEP 440 version for pin: {version!r}")
|
|
273
|
+
|
|
274
|
+
pat = _pin_pattern(distribution)
|
|
275
|
+
|
|
276
|
+
def repl(m: re.Match[str]) -> str:
|
|
277
|
+
return f"{m.group(1)}{version}"
|
|
278
|
+
|
|
279
|
+
new_text, n = pat.subn(repl, pyproject_toml)
|
|
280
|
+
return new_text, n
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def bump_compatible_pins_in_file(
|
|
284
|
+
pyproject: Path | str,
|
|
285
|
+
distribution: str,
|
|
286
|
+
version: str,
|
|
287
|
+
*,
|
|
288
|
+
dry_run: bool = False,
|
|
289
|
+
) -> int:
|
|
290
|
+
"""Read ``pyproject.toml``, apply :func:`bump_compatible_pins`, write back unless ``dry_run``.
|
|
291
|
+
|
|
292
|
+
Returns the number of replacements. Raises ``ValueError`` if no matching pin exists.
|
|
293
|
+
"""
|
|
294
|
+
path = Path(pyproject)
|
|
295
|
+
text = path.read_text(encoding="utf-8")
|
|
296
|
+
new_text, n = bump_compatible_pins(text, distribution, version)
|
|
297
|
+
if n == 0:
|
|
298
|
+
raise ValueError(f"no `{distribution}~=...` pin found in {path}")
|
|
299
|
+
if new_text == text:
|
|
300
|
+
return n
|
|
301
|
+
if not dry_run:
|
|
302
|
+
path.write_text(new_text, encoding="utf-8")
|
|
303
|
+
return n
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def bump_easybind_compatible_pins(pyproject_toml: str, version: str) -> tuple[str, int]:
|
|
307
|
+
"""Same as :func:`bump_compatible_pins` with ``distribution=\"easybind\"``."""
|
|
308
|
+
return bump_compatible_pins(pyproject_toml, "easybind", version)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def bump_easybind_compatible_pins_in_file(
|
|
312
|
+
pyproject: Path | str,
|
|
313
|
+
version: str,
|
|
314
|
+
*,
|
|
315
|
+
dry_run: bool = False,
|
|
316
|
+
) -> int:
|
|
317
|
+
"""Same as :func:`bump_compatible_pins_in_file` with ``distribution=\"easybind\"``."""
|
|
318
|
+
return bump_compatible_pins_in_file(pyproject, "easybind", version, dry_run=dry_run)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def main(argv: list[str] | None = None) -> int:
|
|
322
|
+
"""CLI: ``easybind-pin-pyproject`` (see ``--help``)."""
|
|
323
|
+
ap = argparse.ArgumentParser(
|
|
324
|
+
description=(
|
|
325
|
+
"Set every {distribution}~= pin in pyproject.toml "
|
|
326
|
+
"(default distribution: easybind). "
|
|
327
|
+
"Default version is latest on PyPI (can lag tags). "
|
|
328
|
+
"--from-github uses the latest vX.Y.Z GitHub tag for that distribution's repo "
|
|
329
|
+
"(OWNER/REPO from PyPI project URLs if you omit it). "
|
|
330
|
+
"--version / --installed are explicit overrides."
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
ap.add_argument(
|
|
334
|
+
"--distribution",
|
|
335
|
+
"-d",
|
|
336
|
+
default="easybind",
|
|
337
|
+
metavar="NAME",
|
|
338
|
+
help="PyPI distribution name to match in pins (default: easybind), e.g. cppdantic, easybind",
|
|
339
|
+
)
|
|
340
|
+
ap.add_argument(
|
|
341
|
+
"--pyproject",
|
|
342
|
+
type=Path,
|
|
343
|
+
default=None,
|
|
344
|
+
help="path to pyproject.toml (default: ./pyproject.toml under cwd)",
|
|
345
|
+
)
|
|
346
|
+
ap.add_argument(
|
|
347
|
+
"--version",
|
|
348
|
+
metavar="X.Y.Z",
|
|
349
|
+
default=None,
|
|
350
|
+
help="pin to this exact release (use when PyPI does not list it yet, e.g. CI still publishing)",
|
|
351
|
+
)
|
|
352
|
+
ap.add_argument(
|
|
353
|
+
"--installed",
|
|
354
|
+
action="store_true",
|
|
355
|
+
help="use installed version for --distribution, normalized to X.Y.Z (dev/post/local stripped)",
|
|
356
|
+
)
|
|
357
|
+
ap.add_argument(
|
|
358
|
+
"--from-github",
|
|
359
|
+
nargs="?",
|
|
360
|
+
const="",
|
|
361
|
+
default=None,
|
|
362
|
+
metavar="OWNER/REPO",
|
|
363
|
+
help=(
|
|
364
|
+
"pin to highest vMAJOR.MINOR.PATCH tag on GitHub. "
|
|
365
|
+
"If OWNER/REPO is omitted, read github.com/OWNER/REPO from PyPI metadata for --distribution. "
|
|
366
|
+
"Set GITHUB_TOKEN for private repos."
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
ap.add_argument("--dry-run", action="store_true", help="do not write the file")
|
|
370
|
+
ns = ap.parse_args(argv)
|
|
371
|
+
|
|
372
|
+
nsrc = sum(
|
|
373
|
+
1
|
|
374
|
+
for x in (ns.version is not None, ns.installed, ns.from_github is not None)
|
|
375
|
+
if x
|
|
376
|
+
)
|
|
377
|
+
if nsrc > 1:
|
|
378
|
+
print("error: use at most one of --version, --installed, or --from-github", file=sys.stderr)
|
|
379
|
+
return 2
|
|
380
|
+
|
|
381
|
+
pyproject = ns.pyproject if ns.pyproject is not None else Path.cwd() / "pyproject.toml"
|
|
382
|
+
if not pyproject.is_file():
|
|
383
|
+
print(f"error: {pyproject} not found", file=sys.stderr)
|
|
384
|
+
return 2
|
|
385
|
+
|
|
386
|
+
dist = ns.distribution.strip()
|
|
387
|
+
if not dist:
|
|
388
|
+
print("error: empty --distribution", file=sys.stderr)
|
|
389
|
+
return 2
|
|
390
|
+
|
|
391
|
+
ver_source: str
|
|
392
|
+
if ns.installed:
|
|
393
|
+
raw_installed = installed_distribution_version(dist)
|
|
394
|
+
try:
|
|
395
|
+
ver = compatible_release_pin_from_installed_version(raw_installed)
|
|
396
|
+
except ValueError as e:
|
|
397
|
+
print(f"error: {e}", file=sys.stderr)
|
|
398
|
+
return 1
|
|
399
|
+
if raw_installed != ver:
|
|
400
|
+
print(
|
|
401
|
+
f"note: installed {dist}=={raw_installed!r} -> pin ~={ver} (PEP 440 release triple)",
|
|
402
|
+
file=sys.stderr,
|
|
403
|
+
)
|
|
404
|
+
ver_source = "installed"
|
|
405
|
+
elif ns.from_github is not None:
|
|
406
|
+
raw = ns.from_github.strip()
|
|
407
|
+
try:
|
|
408
|
+
if not raw:
|
|
409
|
+
owner_repo = github_owner_repo_from_pypi_distribution(dist)
|
|
410
|
+
else:
|
|
411
|
+
owner_repo = raw
|
|
412
|
+
ver = latest_release_version_from_github(owner_repo)
|
|
413
|
+
except ValueError as e:
|
|
414
|
+
print(f"error: {e}", file=sys.stderr)
|
|
415
|
+
return 1
|
|
416
|
+
ver_source = "github"
|
|
417
|
+
elif ns.version:
|
|
418
|
+
ver = ns.version.strip()
|
|
419
|
+
ver_source = "explicit"
|
|
420
|
+
else:
|
|
421
|
+
ver = fetch_pypi_version(dist)
|
|
422
|
+
ver_source = "pypi"
|
|
423
|
+
|
|
424
|
+
if not _VERSION_RE.match(ver):
|
|
425
|
+
print(f"error: bad version string: {ver!r}", file=sys.stderr)
|
|
426
|
+
return 2
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
n = bump_compatible_pins_in_file(pyproject, dist, ver, dry_run=ns.dry_run)
|
|
430
|
+
except ValueError as e:
|
|
431
|
+
print(f"error: {e}", file=sys.stderr)
|
|
432
|
+
return 1
|
|
433
|
+
|
|
434
|
+
action = "would update" if ns.dry_run else "updated"
|
|
435
|
+
print(f"{action} {n} {dist}~= pin(s) to ~={ver} in {pyproject}")
|
|
436
|
+
|
|
437
|
+
# PyPI / venv often lag a v* tag on GitHub.
|
|
438
|
+
if ns.dry_run and ver_source in ("pypi", "installed"):
|
|
439
|
+
try:
|
|
440
|
+
or_ = github_owner_repo_from_pypi_distribution(dist)
|
|
441
|
+
gh_ver = latest_release_version_from_github(or_)
|
|
442
|
+
if _semver_tuple(gh_ver) > _semver_tuple(ver):
|
|
443
|
+
print(
|
|
444
|
+
f"hint: github.com/{or_} has v{gh_ver} but {ver_source} gave {ver}. "
|
|
445
|
+
"Re-run with: easybind-pin-pyproject --dry-run --from-github",
|
|
446
|
+
)
|
|
447
|
+
except ValueError:
|
|
448
|
+
pass
|
|
449
|
+
|
|
450
|
+
return 0
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
if __name__ == "__main__":
|
|
454
|
+
raise SystemExit(main())
|
|
@@ -1,251 +0,0 @@
|
|
|
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())
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|