easybind 0.2.7__tar.gz → 0.2.9__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 (37) hide show
  1. {easybind-0.2.7 → easybind-0.2.9}/PKG-INFO +6 -2
  2. {easybind-0.2.7 → easybind-0.2.9}/README.md +4 -1
  3. {easybind-0.2.7 → easybind-0.2.9}/pyproject.toml +1 -1
  4. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/devtools/__init__.py +6 -0
  5. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/devtools/pin_pyproject.py +124 -13
  6. {easybind-0.2.7 → easybind-0.2.9}/.clangd +0 -0
  7. {easybind-0.2.7 → easybind-0.2.9}/.github/workflows/publish.yml +0 -0
  8. {easybind-0.2.7 → easybind-0.2.9}/.gitignore +0 -0
  9. {easybind-0.2.7 → easybind-0.2.9}/.vscode/tasks.json +0 -0
  10. {easybind-0.2.7 → easybind-0.2.9}/CMakeLists.txt +0 -0
  11. {easybind-0.2.7 → easybind-0.2.9}/LICENSE +0 -0
  12. {easybind-0.2.7 → easybind-0.2.9}/NOTICE +0 -0
  13. {easybind-0.2.7 → easybind-0.2.9}/RELEASING.md +0 -0
  14. {easybind-0.2.7 → easybind-0.2.9}/cmake/easybind_dependencies.cmake +0 -0
  15. {easybind-0.2.7 → easybind-0.2.9}/cmake/easybind_pip.cmake +0 -0
  16. {easybind-0.2.7 → easybind-0.2.9}/pyrightconfig.json +0 -0
  17. {easybind-0.2.7 → easybind-0.2.9}/scripts/clangd-update.sh +0 -0
  18. {easybind-0.2.7 → easybind-0.2.9}/scripts/release_tag.py +0 -0
  19. {easybind-0.2.7 → easybind-0.2.9}/scripts/wait_pypi_release.py +0 -0
  20. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/__init__.cpp +0 -0
  21. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/bind.hpp +0 -0
  22. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/devtools/release_helpers.py +0 -0
  23. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/devtools/release_tag.py +0 -0
  24. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/devtools/wait_pypi.py +0 -0
  25. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/export.hpp +0 -0
  26. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/module/__init__.cpp +0 -0
  27. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/module/node.cpp +0 -0
  28. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/module/node.hpp +0 -0
  29. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/module/node__init__.cpp +0 -0
  30. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/module/ns_module.hpp +0 -0
  31. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/py.typed +0 -0
  32. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/sample/__init__.cpp +0 -0
  33. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/sample/sample.cpp +0 -0
  34. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/sample/sample.hpp +0 -0
  35. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/sample/test1.py +0 -0
  36. {easybind-0.2.7 → easybind-0.2.9}/src/easybind/sample/test2/__init__.py +0 -0
  37. {easybind-0.2.7 → easybind-0.2.9}/tests/test_sample.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easybind
3
- Version: 0.2.7
3
+ Version: 0.2.9
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"
@@ -95,7 +96,8 @@ Use **`easybind.devtools`** or the **`easybind-pin-pyproject`** CLI to rewrite e
95
96
  ```bash
96
97
  easybind-pin-pyproject --dry-run
97
98
  easybind-pin-pyproject
98
- easybind-pin-pyproject --from-github pymergetic/easybind # latest v* tag via GitHub API (PyPI may lag)
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
99
101
  easybind-pin-pyproject --installed
100
102
  easybind-pin-pyproject --version 0.2.3
101
103
  easybind-pin-pyproject --distribution cppdantic
@@ -104,6 +106,8 @@ easybind-pin-pyproject --pyproject /path/to/pyproject.toml
104
106
 
105
107
  Use **`GITHUB_TOKEN`** for private GitHub repos or higher API rate limits.
106
108
 
109
+ **`--from-github`:** GitHub’s tags API is sometimes a few seconds behind right after you push a new `v*` tag. If **`easybind-pin-pyproject --dry-run --from-github`** still shows the previous release, wait briefly and run again (or pin with **`--version`** until the API catches up).
110
+
107
111
  **Other devtools CLIs** (after **`pip install`** / **`uv pip install -e .`**, or run **`scripts/…`** shims from a git checkout):
108
112
 
109
113
  ```bash
@@ -66,7 +66,8 @@ Use **`easybind.devtools`** or the **`easybind-pin-pyproject`** CLI to rewrite e
66
66
  ```bash
67
67
  easybind-pin-pyproject --dry-run
68
68
  easybind-pin-pyproject
69
- easybind-pin-pyproject --from-github pymergetic/easybind # latest v* tag via GitHub API (PyPI may lag)
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
70
71
  easybind-pin-pyproject --installed
71
72
  easybind-pin-pyproject --version 0.2.3
72
73
  easybind-pin-pyproject --distribution cppdantic
@@ -75,6 +76,8 @@ easybind-pin-pyproject --pyproject /path/to/pyproject.toml
75
76
 
76
77
  Use **`GITHUB_TOKEN`** for private GitHub repos or higher API rate limits.
77
78
 
79
+ **`--from-github`:** GitHub’s tags API is sometimes a few seconds behind right after you push a new `v*` tag. If **`easybind-pin-pyproject --dry-run --from-github`** still shows the previous release, wait briefly and run again (or pin with **`--version`** until the API catches up).
80
+
78
81
  **Other devtools CLIs** (after **`pip install`** / **`uv pip install -e .`**, or run **`scripts/…`** shims from a git checkout):
79
82
 
80
83
  ```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,7 +6,10 @@ 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,
11
14
  latest_release_version_from_github,
12
15
  pypi_release_exists,
@@ -26,8 +29,11 @@ __all__ = [
26
29
  "bump_easybind_compatible_pins",
27
30
  "bump_easybind_compatible_pins_in_file",
28
31
  "compatible_pin_versions",
32
+ "compatible_release_pin_from_installed_version",
29
33
  "ensure_clean_worktree",
34
+ "fetch_pypi_project_json",
30
35
  "fetch_pypi_version",
36
+ "github_owner_repo_from_pypi_distribution",
31
37
  "installed_distribution_version",
32
38
  "latest_release_version_from_github",
33
39
  "latest_v_tag",
@@ -18,9 +18,20 @@ _VERSION_RE = re.compile(r"^[0-9]+(?:\.[0-9]+)*$")
18
18
  # Release tags: ``vMAJOR.MINOR.PATCH`` (GitHub / git tag style).
19
19
  _V_SEMVER_TAG = re.compile(r"^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
20
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
+
21
26
  # Reject pathological / mistaken distribution strings (full re.escape covers the rest).
22
27
  _DIST_OK = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]*$")
23
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
+
24
35
 
25
36
  def _pin_pattern(distribution: str) -> re.Pattern[str]:
26
37
  if not _DIST_OK.match(distribution):
@@ -31,14 +42,58 @@ def _pin_pattern(distribution: str) -> re.Pattern[str]:
31
42
  return re.compile(rf"({re.escape(distribution)}~=)([0-9]+(?:\.[0-9]+)*)")
32
43
 
33
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
+
34
57
  def fetch_pypi_version(package: str = "easybind", *, timeout_s: float = 30.0) -> str:
35
58
  """Return ``info.version`` from ``https://pypi.org/pypi/{package}/json``."""
36
- url = f"https://pypi.org/pypi/{package}/json"
37
- with urlopen(url, timeout=timeout_s) as r:
38
- data = json.load(r)
59
+ data = fetch_pypi_project_json(package, timeout_s=timeout_s)
39
60
  return str(data["info"]["version"])
40
61
 
41
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
+
42
97
  def pypi_release_exists(package: str, version: str, *, timeout_s: float = 15.0) -> bool:
43
98
  """Return True if ``https://pypi.org/pypi/{package}/{version}/json`` exists (release uploaded)."""
44
99
  url = f"https://pypi.org/pypi/{package}/{version}/json"
@@ -125,6 +180,19 @@ def installed_distribution_version(package: str = "easybind") -> str:
125
180
  return version(package)
126
181
 
127
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
+
128
196
  def _github_request_json(url: str, *, token: str | None, timeout_s: float) -> object:
129
197
  req = Request(url, headers={"Accept": "application/vnd.github+json"})
130
198
  if token:
@@ -142,7 +210,10 @@ def latest_release_version_from_github(
142
210
  """Return the highest ``vMAJOR.MINOR.PATCH`` tag on GitHub as ``X.Y.Z`` (no local git clone).
143
211
 
144
212
  Uses ``GET /repos/{owner}/{repo}/tags`` (paginated). Works when the tag exists on GitHub but
145
- PyPI has not published the wheel yet. *owner_repo* is ``OWNER/REPO`` (e.g. ``pymergetic/easybind``).
213
+ PyPI has not published the wheel yet. *owner_repo* is ``OWNER/REPO``.
214
+
215
+ The tags endpoint can lag a few seconds right after you push a new tag (eventual consistency /
216
+ caching). If the version looks stale, wait and call again.
146
217
 
147
218
  Set ``GITHUB_TOKEN`` or ``GH_TOKEN`` (or pass *token*) for private repos or higher rate limits.
148
219
  """
@@ -256,9 +327,11 @@ def main(argv: list[str] | None = None) -> int:
256
327
  description=(
257
328
  "Set every {distribution}~= pin in pyproject.toml "
258
329
  "(default distribution: easybind). "
259
- "Default version is latest on PyPI (can lag CI). "
260
- "Use --from-github OWNER/REPO for the latest vX.Y.Z tag on GitHub (no local git); "
261
- "use --version when you know the number; --installed uses the env's installed wheel."
330
+ "Default version is latest on PyPI (can lag tags). "
331
+ "--from-github uses the latest vX.Y.Z GitHub tag for that distribution's repo "
332
+ "(OWNER/REPO from PyPI project URLs if you omit it); the GitHub tags API can lag "
333
+ "briefly after a new tag push. "
334
+ "--version / --installed are explicit overrides."
262
335
  )
263
336
  )
264
337
  ap.add_argument(
@@ -283,15 +356,19 @@ def main(argv: list[str] | None = None) -> int:
283
356
  ap.add_argument(
284
357
  "--installed",
285
358
  action="store_true",
286
- help="use importlib.metadata.version for --distribution in this env",
359
+ help="use installed version for --distribution, normalized to X.Y.Z (dev/post/local stripped)",
287
360
  )
288
361
  ap.add_argument(
289
362
  "--from-github",
290
- metavar="OWNER/REPO",
363
+ nargs="?",
364
+ const="",
291
365
  default=None,
366
+ metavar="OWNER/REPO",
292
367
  help=(
293
- "pin to highest vMAJOR.MINOR.PATCH tag from GitHub API (e.g. pymergetic/easybind). "
294
- "Use when the tag exists but PyPI does not yet. Set GITHUB_TOKEN for private repos."
368
+ "pin to highest vMAJOR.MINOR.PATCH tag on GitHub. "
369
+ "If OWNER/REPO is omitted, read github.com/OWNER/REPO from PyPI metadata for --distribution. "
370
+ "The tags list can lag a few seconds after you push a new tag; re-run if the pin looks stale. "
371
+ "Set GITHUB_TOKEN for private repos."
295
372
  ),
296
373
  )
297
374
  ap.add_argument("--dry-run", action="store_true", help="do not write the file")
@@ -316,18 +393,38 @@ def main(argv: list[str] | None = None) -> int:
316
393
  print("error: empty --distribution", file=sys.stderr)
317
394
  return 2
318
395
 
396
+ ver_source: str
319
397
  if ns.installed:
320
- ver = installed_distribution_version(dist)
398
+ raw_installed = installed_distribution_version(dist)
399
+ try:
400
+ ver = compatible_release_pin_from_installed_version(raw_installed)
401
+ except ValueError as e:
402
+ print(f"error: {e}", file=sys.stderr)
403
+ return 1
404
+ if raw_installed != ver:
405
+ print(
406
+ f"note: installed {dist}=={raw_installed!r} -> pin ~={ver} (PEP 440 release triple)",
407
+ file=sys.stderr,
408
+ )
409
+ ver_source = "installed"
321
410
  elif ns.from_github is not None:
411
+ raw = ns.from_github.strip()
322
412
  try:
323
- ver = latest_release_version_from_github(ns.from_github.strip())
413
+ if not raw:
414
+ owner_repo = github_owner_repo_from_pypi_distribution(dist)
415
+ else:
416
+ owner_repo = raw
417
+ ver = latest_release_version_from_github(owner_repo)
324
418
  except ValueError as e:
325
419
  print(f"error: {e}", file=sys.stderr)
326
420
  return 1
421
+ ver_source = "github"
327
422
  elif ns.version:
328
423
  ver = ns.version.strip()
424
+ ver_source = "explicit"
329
425
  else:
330
426
  ver = fetch_pypi_version(dist)
427
+ ver_source = "pypi"
331
428
 
332
429
  if not _VERSION_RE.match(ver):
333
430
  print(f"error: bad version string: {ver!r}", file=sys.stderr)
@@ -341,6 +438,20 @@ def main(argv: list[str] | None = None) -> int:
341
438
 
342
439
  action = "would update" if ns.dry_run else "updated"
343
440
  print(f"{action} {n} {dist}~= pin(s) to ~={ver} in {pyproject}")
441
+
442
+ # PyPI / venv often lag a v* tag on GitHub.
443
+ if ns.dry_run and ver_source in ("pypi", "installed"):
444
+ try:
445
+ or_ = github_owner_repo_from_pypi_distribution(dist)
446
+ gh_ver = latest_release_version_from_github(or_)
447
+ if _semver_tuple(gh_ver) > _semver_tuple(ver):
448
+ print(
449
+ f"hint: github.com/{or_} has v{gh_ver} but {ver_source} gave {ver}. "
450
+ "Re-run with: easybind-pin-pyproject --dry-run --from-github",
451
+ )
452
+ except ValueError:
453
+ pass
454
+
344
455
  return 0
345
456
 
346
457
 
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