Skip to content

topmark.version.convert

topmark / version / convert

Convert the subset of PEP 440 versions TopMark emits into SemVer-ish output.

Supported inputs (subset): - X.Y.Z - X.Y.ZaN / X.Y.ZbN / X.Y.ZrcN - X.Y.Z.devN (or with a pre-release, e.g. X.Y.Zrc1.dev2) - optional local version: +local (kept as-is)

Notes: - .postN is recognized only so we can reject it with a clear error message, because it has no clean SemVer equivalent.

convert_pep440_to_semver

convert_pep440_to_semver(pep440_version)

Convert a supported PEP 440 version string to a SemVer-ish string.

Mapping
  • rcN -> -rc.N
  • aN -> -alpha.N
  • bN -> -beta.N
  • devN -> -dev.N (after any pre-release)
  • +local kept as-is

Examples:

  • 1.2.3 -> 1.2.3
  • 1.2.3rc1 -> 1.2.3-rc.1
  • 1.2.3rc1.dev2 -> 1.2.3-rc.1.dev.2
  • 1.2.3.dev4 -> 1.2.3-dev.4
  • 1.2.3+abc.1 -> 1.2.3+abc.1

Parameters:

Name Type Description Default
pep440_version str

Version in PEP 440 format.

required

Returns:

Type Description
str

A SemVer-ish version string.

Raises:

Type Description
ValueError

If the version does not match the supported subset or if it contains .postN.

Source code in src/topmark/version/convert.py
def convert_pep440_to_semver(pep440_version: str) -> str:
    """Convert a supported PEP 440 version string to a SemVer-ish string.

    Mapping:
      - rcN  -> -rc.N
      - aN   -> -alpha.N
      - bN   -> -beta.N
      - devN -> -dev.N (after any pre-release)
      - +local kept as-is

    Examples:
      - 1.2.3        -> 1.2.3
      - 1.2.3rc1     -> 1.2.3-rc.1
      - 1.2.3rc1.dev2 -> 1.2.3-rc.1.dev.2
      - 1.2.3.dev4   -> 1.2.3-dev.4
      - 1.2.3+abc.1  -> 1.2.3+abc.1

    Args:
        pep440_version: Version in PEP 440 format.

    Returns:
        A SemVer-ish version string.

    Raises:
        ValueError: If the version does not match the supported subset or if it contains `.postN`.
    """
    m: re.Match[str] | None = _PEP440_RE.match(pep440_version)
    if not m:
        raise ValueError(f"Not a recognized supported PEP 440 version: {pep440_version!r}")

    if m.group("post") is not None:
        raise ValueError(f"Post-releases are not valid SemVer: {pep440_version!r}")

    major: str = m.group("major")
    minor: str = m.group("minor")
    patch: str = m.group("patch")
    base: str = f"{major}.{minor}.{patch}"

    pre: str = ""
    pre_label: str | None = m.group("pre_label")
    if pre_label is not None:
        lbl: str = {"a": "alpha", "b": "beta", "rc": "rc"}[pre_label]
        pre = f"-{lbl}.{m.group('pre_num')}"

    dev: str = ""
    if m.group("dev") is not None:
        # If we already have a pre-release, dev becomes an extra dot-segment; otherwise it starts
        # the pre-release part.
        dev = f"{'.' if pre else '-'}dev.{m.group('dev')}"

    local: str = f"+{m.group('local')}" if m.group("local") else ""
    return f"{base}{pre}{dev}{local}"