netbox/scripts/packaging/hatch_metadata.py

64 lines
2.4 KiB
Python

"""Hatchling metadata hook: derive a PEP 440 version from netbox/release.yaml.
NetBox stores the release version and any pre-release designation (for example
"beta1") separately in netbox/release.yaml. This hook combines them into a
canonical PEP 440 version so wheels publish correctly, including pre-releases.
"""
import re
from pathlib import Path
from packaging.version import Version
try:
from hatchling.metadata.plugin.interface import MetadataHookInterface
except ModuleNotFoundError:
# hatchling is only installed inside the isolated build environment. Fall back
# so this module (and compute_version) remains importable for unit testing.
MetadataHookInterface = object
def compute_version(version, designation):
"""Return a canonical PEP 440 version from a version and optional designation."""
raw = f"{version}{designation}" if designation else version
return str(Version(raw))
def _read_release_field(text, field):
match = re.search(rf'^{field}:\s*"?([^"\n]+?)"?\s*$', text, re.MULTILINE)
return match.group(1).strip() if match else None
def read_requirements(text):
"""Parse a pinned requirements.txt body into PEP 508 dependency specifiers.
Assumes NetBox's flat "package==version" format (one top-level pin per line).
Blank lines, comments, and pip option lines (starting with "-") are skipped.
"""
dependencies = []
for raw_line in text.splitlines():
line = raw_line.split('#', 1)[0].strip()
if not line or line.startswith('-'):
continue
dependencies.append(line)
return dependencies
class NetBoxMetadataHook(MetadataHookInterface):
def update(self, metadata):
root = Path(self.root)
# Version: derived from release.yaml (version + optional designation).
release_path = root / 'netbox' / 'release.yaml'
text = release_path.read_text()
version = _read_release_field(text, 'version')
if not version:
raise ValueError(f"Unable to read 'version' from {release_path}")
designation = _read_release_field(text, 'designation')
metadata['version'] = compute_version(version, designation)
# Dependencies: requirements.txt is the single pinned source of truth, so the
# published wheel's Requires-Dist matches the tested pins, not loose ranges.
requirements_path = root / 'requirements.txt'
metadata['dependencies'] = read_requirements(requirements_path.read_text())