Remove unused _append_ops_call_method, fix _property_data_from_member call signature, replace sys.version_info guard with hasattr check for __buffer__ protocol, fix implicit string concatenations, add msgbus overrides for 4.0-4.4, add conformance tests, and add publish script. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
226 lines
6.4 KiB
Python
226 lines
6.4 KiB
Python
"""Publish stubs to PyPI with automatic version bumping.
|
|
|
|
Fetches the latest published version from PyPI, bumps the revision,
|
|
asks for confirmation, then builds and uploads.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tomllib
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
from typing import cast
|
|
|
|
SCRIPT_DIR = Path(__file__).parent
|
|
DIST_DIR = SCRIPT_DIR / "dist"
|
|
PACKAGE_NAME = "blender-python-stubs"
|
|
|
|
PYPI_JSON_URL = "https://pypi.org/pypi/{package}/json"
|
|
TEST_PYPI_JSON_URL = "https://test.pypi.org/pypi/{package}/json"
|
|
TEST_PYPI_UPLOAD_URL = "https://test.pypi.org/legacy/"
|
|
|
|
|
|
def fetch_latest_revision(blender_version: str, use_test_pypi: bool) -> int | None:
|
|
"""Fetch the latest revision for a Blender version from PyPI.
|
|
|
|
Returns the revision number (the 4th component of X.Y.Z.R) or None
|
|
if no matching version is found.
|
|
"""
|
|
base_url = TEST_PYPI_JSON_URL if use_test_pypi else PYPI_JSON_URL
|
|
url = base_url.format(package=PACKAGE_NAME)
|
|
|
|
try:
|
|
req = urllib.request.Request(url, headers={"Accept": "application/json"})
|
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
data: dict[str, object] = json.loads(resp.read())
|
|
except urllib.error.HTTPError as e:
|
|
if e.code == 404:
|
|
return None
|
|
raise
|
|
|
|
raw_releases = data.get("releases")
|
|
if not isinstance(raw_releases, dict):
|
|
return None
|
|
|
|
releases = cast(dict[str, object], raw_releases)
|
|
|
|
# Find versions matching this Blender version (e.g., 5.1.0.*)
|
|
prefix = blender_version + "."
|
|
max_revision = -1
|
|
for version_str in releases:
|
|
if not version_str.startswith(prefix):
|
|
continue
|
|
parts = version_str.split(".")
|
|
if len(parts) != 4:
|
|
continue
|
|
try:
|
|
revision = int(parts[3])
|
|
except ValueError:
|
|
continue
|
|
if revision > max_revision:
|
|
max_revision = revision
|
|
|
|
return max_revision if max_revision >= 0 else None
|
|
|
|
|
|
def read_generated_version(version_dir: Path) -> str:
|
|
"""Read the version from a generated dist pyproject.toml."""
|
|
pyproject_path = version_dir / "pyproject.toml"
|
|
if not pyproject_path.exists():
|
|
print(f"No pyproject.toml found in {version_dir}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
with pyproject_path.open("rb") as f:
|
|
data = tomllib.load(f)
|
|
|
|
version = data.get("project", {}).get("version", "")
|
|
if not isinstance(version, str) or not version:
|
|
print(f"No version found in {pyproject_path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
return version
|
|
|
|
|
|
def update_version_in_pyproject(version_dir: Path, new_version: str) -> None:
|
|
"""Update the version in the generated pyproject.toml."""
|
|
pyproject_path = version_dir / "pyproject.toml"
|
|
content = pyproject_path.read_text()
|
|
|
|
# Replace the version line
|
|
lines: list[str] = []
|
|
for line in content.splitlines():
|
|
if line.startswith("version = "):
|
|
lines.append(f'version = "{new_version}"')
|
|
else:
|
|
lines.append(line)
|
|
|
|
pyproject_path.write_text("\n".join(lines) + "\n")
|
|
|
|
|
|
def publish_version(
|
|
blender_version: str,
|
|
publish_url: str,
|
|
revision: int | None,
|
|
yes: bool,
|
|
) -> None:
|
|
"""Publish stubs for a single Blender version."""
|
|
# Parse blender_version into the full version from the generated stubs
|
|
version_dir = DIST_DIR / blender_version
|
|
if not version_dir.is_dir():
|
|
print(
|
|
f"No generated stubs for {blender_version}. Run 'poe generate {blender_version}' first.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
|
|
current_version = read_generated_version(version_dir)
|
|
parts = current_version.split(".")
|
|
if len(parts) != 4:
|
|
print(
|
|
f"Unexpected version format: {current_version}",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
|
|
base_version = ".".join(parts[:3]) # e.g., "5.1.0"
|
|
|
|
use_test_pypi = "test.pypi" in publish_url
|
|
|
|
if revision is not None:
|
|
new_revision = revision
|
|
else:
|
|
# Fetch latest from PyPI
|
|
pypi_label = "Test PyPI" if use_test_pypi else "PyPI"
|
|
print(f"Fetching latest version from {pypi_label}...")
|
|
latest = fetch_latest_revision(base_version, use_test_pypi)
|
|
|
|
if latest is not None:
|
|
new_revision = latest + 1
|
|
print(f" Latest published: {base_version}.{latest}")
|
|
else:
|
|
new_revision = 0
|
|
print(f" No published version found for {base_version}")
|
|
|
|
new_version = f"{base_version}.{new_revision}"
|
|
|
|
if not yes:
|
|
answer = input(f"Publish {new_version}? [y/N] ")
|
|
if answer.lower() not in ("y", "yes"):
|
|
print("Aborted.")
|
|
sys.exit(0)
|
|
|
|
# Update pyproject.toml with the new version
|
|
update_version_in_pyproject(version_dir, new_version)
|
|
print(f" Version set to {new_version}")
|
|
|
|
# Clean previous build artifacts
|
|
build_dist = version_dir / "dist"
|
|
if build_dist.exists():
|
|
import shutil
|
|
|
|
shutil.rmtree(build_dist)
|
|
|
|
# Build
|
|
print(" Building...")
|
|
result = subprocess.run(
|
|
["uv", "build", str(version_dir)],
|
|
cwd=str(SCRIPT_DIR),
|
|
)
|
|
if result.returncode != 0:
|
|
print("Build failed.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Publish
|
|
print(f" Publishing to {publish_url}...")
|
|
result = subprocess.run(
|
|
[
|
|
"uv",
|
|
"publish",
|
|
str(build_dist / "*"),
|
|
"--publish-url",
|
|
publish_url,
|
|
],
|
|
cwd=str(SCRIPT_DIR),
|
|
)
|
|
if result.returncode != 0:
|
|
print("Publish failed.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print(f" Published {new_version}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="Publish Blender type stubs to PyPI")
|
|
parser.add_argument(
|
|
"version",
|
|
help="Blender version to publish (e.g., 5.1)",
|
|
)
|
|
parser.add_argument(
|
|
"--revision",
|
|
type=int,
|
|
default=None,
|
|
help="Override revision number instead of auto-detecting",
|
|
)
|
|
parser.add_argument(
|
|
"--publish-url",
|
|
default=TEST_PYPI_UPLOAD_URL,
|
|
help="PyPI upload URL (defaults to Test PyPI)",
|
|
)
|
|
parser.add_argument(
|
|
"-y",
|
|
"--yes",
|
|
action="store_true",
|
|
help="Skip confirmation prompt",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
publish_version(
|
|
args.version,
|
|
args.publish_url,
|
|
args.revision,
|
|
args.yes,
|
|
)
|