From bb6874bdfd44e1229eb04eaaea3976261bcda161 Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Sat, 6 Nov 2021 15:39:02 -0400 Subject: [PATCH] Added pydantic integration for Version - Added two new dunder methods in Version which comply with the custom field types described [here](https://pydantic-docs.helpmanual.io/usage/types/#classes-with-__get_validators__). - Updated usage documentation with example for using with Pydantic. - Added test case for pydantic integration. - Added pydantic dependency in `tox.ini` for tests cases. - Verified all tests passed for all python versions with `tox`. --- docs/usage.rst | 15 ++++++ src/semver/version.py | 32 +++++++----- tests/test_pydantic.py | 109 +++++++++++++++++++++++++++++++++++++++++ tox.ini | 1 + 4 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 tests/test_pydantic.py diff --git a/docs/usage.rst b/docs/usage.rst index eb4cc25b..08dc534f 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -787,3 +787,18 @@ the original class: Traceback (most recent call last): ... ValueError: '1.2.4': not a valid semantic version tag. Must start with 'v' or 'V' + +Integrating with Pydantic +------------------------------------- + +If you are building a Pydantic model, you can use :class:`~semver.version.Version` directly. +An appropriate :class:`pydantic.ValidationError` will be raised for invalid version numbers. + +.. code-block:: python + + >>> from semver.version import Version + >>> from pydantic import create_model + >>> Model = create_model('Model', version=(Version, ...)) + >>> model = Model(version="3.4.5-pre.2+build.4") + >>> model.version + Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') diff --git a/src/semver/version.py b/src/semver/version.py index 9e02544f..2377eda6 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -1,28 +1,22 @@ """Version handling.""" -import collections import re -from functools import wraps +import collections from typing import ( Any, Dict, - Iterable, - Optional, - SupportsInt, Tuple, Union, - cast, Callable, + Iterable, + Optional, Collection, + SupportsInt, + cast, ) +from functools import wraps -from ._types import ( - VersionTuple, - VersionDict, - VersionIterator, - String, - VersionPart, -) +from ._types import String, VersionDict, VersionPart, VersionTuple, VersionIterator # These types are required here because of circular imports Comparable = Union["Version", Dict[str, VersionPart], Collection[VersionPart], str] @@ -552,6 +546,18 @@ def match(self, match_expr: str) -> bool: return cmp_res in possibilities + @classmethod + def __get_validators__(cls): + """Return a list of validator methods for pydantic models.""" + + yield cls.parse + + @classmethod + def __modify_schema__(cls, field_schema): + """Inject/mutate the pydantic field schema in-place.""" + + field_schema.update(examples=["1.0.2", "2.15.3-alpha", "21.3.15-beta+12345"]) + @classmethod def parse(cls, version: String) -> "Version": """ diff --git a/tests/test_pydantic.py b/tests/test_pydantic.py new file mode 100644 index 00000000..941d8099 --- /dev/null +++ b/tests/test_pydantic.py @@ -0,0 +1,109 @@ +import pytest +import pydantic + +from semver import Version + + +class Schema(pydantic.BaseModel): + """An example schema which contains a semver Version object""" + + name: str + """ Other data which isn't important """ + version: Version + """ Version number auto-parsed by Pydantic """ + + +@pytest.mark.parametrize( + "version,expected", + [ + # no. 1 + ( + "1.2.3-alpha.1.2+build.11.e0f985a", + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "alpha.1.2", + "build": "build.11.e0f985a", + }, + ), + # no. 2 + ( + "1.2.3-alpha-1+build.11.e0f985a", + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "alpha-1", + "build": "build.11.e0f985a", + }, + ), + ( + "0.1.0-0f", + {"major": 0, "minor": 1, "patch": 0, "prerelease": "0f", "build": None}, + ), + ( + "0.0.0-0foo.1", + {"major": 0, "minor": 0, "patch": 0, "prerelease": "0foo.1", "build": None}, + ), + ( + "0.0.0-0foo.1+build.1", + { + "major": 0, + "minor": 0, + "patch": 0, + "prerelease": "0foo.1", + "build": "build.1", + }, + ), + ], +) +def test_should_parse_version(version, expected): + result = Schema(name="test", version=version) + assert result.version == expected + + +@pytest.mark.parametrize( + "version,expected", + [ + # no. 1 + ( + "1.2.3-rc.0+build.0", + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "rc.0", + "build": "build.0", + }, + ), + # no. 2 + ( + "1.2.3-rc.0.0+build.0", + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "rc.0.0", + "build": "build.0", + }, + ), + ], +) +def test_should_parse_zero_prerelease(version, expected): + result = Schema(name="test", version=version) + assert result.version == expected + + +@pytest.mark.parametrize("version", ["01.2.3", "1.02.3", "1.2.03"]) +def test_should_raise_value_error_for_zero_prefixed_versions(version): + with pytest.raises(pydantic.ValidationError): + Schema(name="test", version=version) + + +def test_should_have_schema_examples(): + assert Schema.schema()["properties"]["version"]["examples"] == [ + "1.0.2", + "2.15.3-alpha", + "21.3.15-beta+12345", + ] diff --git a/tox.ini b/tox.ini index ce566562..5479fd44 100644 --- a/tox.ini +++ b/tox.ini @@ -20,6 +20,7 @@ commands = pytest {posargs:} deps = pytest pytest-cov + pydantic setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy