Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: add optional native parser backend
  • Loading branch information
123 authored and cursoragent committed Sep 2, 2026
commit db92cdbe71b1d6370078d0b6a8b02ad61c28a51c
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ Documentation = "https://python-semver.rtfd.io"
Releases = "https://github.com/python-semver/python-semver/releases"
"Bug Tracker" = "https://github.com/python-semver/python-semver/issues"

[project.optional-dependencies]
native = [
"fast-semver-rs-backend>=0.1.1; platform_python_implementation == 'CPython'",
]


[project.scripts]
pysemver = "semver.cli:main"
Expand Down
13 changes: 13 additions & 0 deletions src/semver/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
VersionPart,
)

try:
from fast_semver_rs_backend import parse_parts as _native_parse_parts
except ImportError: # optional backend is deliberately absent on the default path
_native_parse_parts = None

# These types are required here because of circular imports
Comparable = Union["Version", Dict[str, VersionPart], Collection[VersionPart], str]
Comparator = Callable[["Version", Comparable], bool]
Expand Down Expand Up @@ -662,6 +667,14 @@ def parse(
elif not isinstance(version, String.__args__): # type: ignore
raise TypeError("not expecting type '%s'" % type(version))

if _native_parse_parts is not None and not optional_minor_and_patch:
try:
return cls(*_native_parse_parts(version))
except ValueError:
# The backend uses bounded Rust integers. Falling through keeps
# Python's arbitrary-size integer and exact error contracts.
pass

if optional_minor_and_patch:
match = cls._REGEX_OPTIONAL_MINOR_AND_PATCH.match(version)
else:
Expand Down
23 changes: 23 additions & 0 deletions tests/test_optional_native_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations

import importlib.util

import pytest

from semver import Version
from semver import version as version_module


def test_optional_backend_is_available_without_changing_public_objects() -> None:
if importlib.util.find_spec("fast_semver_rs_backend") is None:
pytest.skip("optional backend is not installed")

assert version_module._native_parse_parts is not None
result = Version.parse("1.2.3-rc.1+build.4")
assert type(result) is Version
assert result.to_tuple() == (1, 2, 3, "rc.1", "build.4")


def test_optional_backend_is_not_used_for_python_only_optional_parts() -> None:
result = Version.parse("7", optional_minor_and_patch=True)
assert result.to_tuple() == (7, 0, 0, None, None)
Loading