Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions changelog.d/pr476.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added optional ``[native]`` extra to accelerate parsing on CPython via the
``fast-semver-rs-backend`` package. The pure-Python parser remains the default;
the native backend is opt-in and only available on CPython.
21 changes: 21 additions & 0 deletions docs/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,27 @@ Then use the command :command:`uv` to install the package:
uv pip install semver


Optional Native Backend
-----------------------

The ``semver`` package provides an optional native backend that can accelerate version parsing on CPython.
Install it with the ``[native]`` extra:

.. code-block:: bash

pip3 install 'semver[native]'

Or with :command:`uv`:

.. code-block:: bash

uv pip install 'semver[native]'

The native backend uses the ``fast-semver-rs-backend`` package, which is only available
on CPython. On other Python implementations or when the extra is not installed,
``semver`` automatically falls back to its pure-Python parser. No code changes are required.


Linux Distributions
-------------------

Expand Down
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
26 changes: 26 additions & 0 deletions tests/test_optional_native_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations

# selftest marker: keep for fork CI path filter


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)
23 changes: 21 additions & 2 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading