Source code for mafw.devtools.versions

#  Copyright 2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""Shared version parsing and formatting primitives for ``devtools``.

PEP 440 distribution versions, Python interpreter minors, and version-bearing
filenames have distinct syntax. This module centralizes their parsing while
leaving caller-specific policy and error messages at their boundaries.

:author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from pathlib import Path
from typing import Final

from packaging.version import Version

PYTHON_MINOR_PATTERN: Final[re.Pattern[str]] = re.compile(r'(?P<major>\d+)\.(?P<minor>\d+)')
"""Exact syntax for a Python ``major.minor`` selector."""

STABLE_TAG_PATTERN: Final[re.Pattern[str]] = re.compile(r'v(?P<major>\d+)\.(?P<minor>\d+)\.(?P<micro>\d+)')
"""Exact syntax for a stable project tag; callers must use ``fullmatch``."""

PYLOCK_REFERENCE_PATTERN: Final[re.Pattern[str]] = re.compile(r'pylock\.py(?P<python_version>3_\d+)_ref\.toml')
"""Exact syntax for a Python reference lock filename; use ``fullmatch``."""


[docs] @dataclass(frozen=True, slots=True, order=True) class PythonMinorVersion: """A structural Python ``major.minor`` value for numeric ordering.""" major: int minor: int
[docs] @classmethod def parse(cls, value: str) -> PythonMinorVersion: """Parse an exact ``major.minor`` token without applying project policy.""" match = PYTHON_MINOR_PATTERN.fullmatch(value) if match is None: raise ValueError(f'Invalid Python minor version: {value!r}') return cls(int(match.group('major')), int(match.group('minor')))
def __str__(self) -> str: """Render the numeric components without leading zeroes.""" return f'{self.major}.{self.minor}'
[docs] def parse_pep440_version(value: str) -> Version: """Parse *value* as a PEP 440 version using :class:`packaging.version.Version`.""" return Version(value)
[docs] def pylock_version_tag(python_version: str) -> str: """Convert a dotted Python minor token to the filename-safe underscore form.""" PythonMinorVersion.parse(python_version) return python_version.replace('.', '_')
[docs] def build_pylock_reference_filename(python_version: str) -> str: """Build the reference lock filename for a Python minor token.""" return f'pylock.py{pylock_version_tag(python_version)}_ref.toml'
[docs] def parse_pylock_reference_filename(file_name: str) -> tuple[str, str] | None: """Parse an exact pylock reference filename, accepting a path-like input.""" base = Path(file_name).name match = PYLOCK_REFERENCE_PATTERN.fullmatch(base) if match is None: return None python_version = match.group('python_version').replace('_', '.') return python_version, base