Source code for repomatic.versions

# Copyright Kevin Deldycke <[email protected]> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

"""PEP 440 version primitives, shared by the whole package.

Every version this package reads comes from somewhere it does not control (a
lock file, a package index, a git tag), so parsing has to tolerate junk, and
nearly every module compares or normalizes versions somewhere. These helpers
used to live in {mod}`repomatic.release.version_sync`, whose import graph reaches the
GitHub, PyPI and npm clients: the modules below it in that graph each kept a
private two-line copy of the parse rather than close a cycle. A leaf module
with no dependency beyond `packaging` is what lets everyone import the one
copy.
"""

from __future__ import annotations

import re

from packaging.version import InvalidVersion, Version

DEV_SUFFIX_RE = re.compile(r"\.dev\d*$")
"""Match the trailing PEP 440 developmental-release segment of a version."""


[docs] def safe_version(value: str) -> Version | None: """Parse a PEP 440 version, returning `None` for anything unparsable. Collapsing the `try`/`except InvalidVersion` into one helper keeps the callers reading as the filters they are. :param value: A version string. :return: The parsed {class}`~packaging.version.Version`, or `None` when *value* is empty or not PEP 440. """ if not value: return None try: return Version(value) except InvalidVersion: return None
[docs] def is_newer(new: str, old: str) -> bool: """Return `True` when *new* is a strictly higher version than *old*. Unparsable versions compare as not-newer, so a malformed candidate never triggers a bump. """ new_v, old_v = safe_version(new), safe_version(old) if new_v is None or old_v is None: return False return new_v > old_v
[docs] def strip_dev_suffix(version: str) -> str: """Drop any PEP 440 `.devN` segment from *version*. `"5.10.0.dev0"` becomes `"5.10.0"`. A version carrying no developmental segment is returned unchanged, so the call is safe to apply blindly. """ return DEV_SUFFIX_RE.sub("", version)