Skip to content

Commit

Permalink
♻️ update SemanticVersion code (#237)
Browse files Browse the repository at this point in the history
* Derive SemanticVersion from semver.Version

This change makes it possible to use SemanticVersion to initialize
models using the __init__ method without type errors.

* Add pattern to SemanticVersion's JSON Schema

This ensures that users of the JSON Schema also get their semantic
versions validated.

The pattern and the new test cases come from https://semver.org.
  • Loading branch information
viccie30 authored Nov 14, 2024
1 parent c0ad1b5 commit f7875f0
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 9 deletions.
12 changes: 8 additions & 4 deletions pydantic_extra_types/semantic_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
) from e


class SemanticVersion:
class SemanticVersion(semver.Version):
"""
Semantic version based on the official [semver thread](https://python-semver.readthedocs.io/en/latest/advanced/combine-pydantic-and-semver.html).
"""
Expand All @@ -27,8 +27,8 @@ def __get_pydantic_core_schema__(
_source_type: Any,
_handler: Callable[[Any], core_schema.CoreSchema],
) -> core_schema.CoreSchema:
def validate_from_str(value: str) -> semver.Version:
return semver.Version.parse(value)
def validate_from_str(value: str) -> SemanticVersion:
return cls.parse(value)

from_str_schema = core_schema.chain_schema(
[
Expand All @@ -52,4 +52,8 @@ def validate_from_str(value: str) -> semver.Version:
def __get_pydantic_json_schema__(
cls, _core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
return handler(core_schema.str_schema())
return handler(
core_schema.str_schema(
pattern=r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
)
)
8 changes: 7 additions & 1 deletion tests/test_json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,13 @@
(
SemanticVersion,
{
'properties': {'x': {'title': 'X', 'type': 'string'}},
'properties': {
'x': {
'title': 'X',
'type': 'string',
'pattern': r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$',
}
},
'required': ['x'],
'title': 'Model',
'type': 'object',
Expand Down
92 changes: 88 additions & 4 deletions tests/test_semantic_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
import semver
from pydantic import BaseModel, ValidationError

from pydantic_extra_types.semantic_version import SemanticVersion
Expand All @@ -12,14 +13,97 @@ class Application(BaseModel):
return Application


@pytest.mark.parametrize('version', ['1.0.0', '1.0.0-alpha.1', '1.0.0-alpha.1+build.1', '1.2.3'])
def test_valid_semantic_version(SemanticVersionObject, version):
application = SemanticVersionObject(version=version)
@pytest.mark.parametrize(
'constructor', [str, semver.Version.parse, SemanticVersion.parse], ids=['str', 'semver.Version', 'SemanticVersion']
)
@pytest.mark.parametrize(
'version',
[
'0.0.4',
'1.2.3',
'10.20.30',
'1.1.2-prerelease+meta',
'1.1.2+meta',
'1.1.2+meta-valid',
'1.0.0-alpha',
'1.0.0-beta',
'1.0.0-alpha.beta',
'1.0.0-alpha.beta.1',
'1.0.0-alpha.1',
'1.0.0-alpha0.valid',
'1.0.0-alpha.0valid',
'1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay',
'1.0.0-rc.1+build.1',
'2.0.0-rc.1+build.123',
'1.2.3-beta',
'10.2.3-DEV-SNAPSHOT',
'1.2.3-SNAPSHOT-123',
'1.0.0',
'2.0.0',
'1.1.7',
'2.0.0+build.1848',
'2.0.1-alpha.1227',
'1.0.0-alpha+beta',
'1.2.3----RC-SNAPSHOT.12.9.1--.12+788',
'1.2.3----R-S.12.9.1--.12+meta',
'1.2.3----RC-SNAPSHOT.12.9.1--.12',
'1.0.0+0.build.1-rc.10000aaa-kk-0.1',
'99999999999999999999999.999999999999999999.99999999999999999',
'1.0.0-0A.is.legal',
],
)
def test_valid_semantic_version(SemanticVersionObject, constructor, version):
application = SemanticVersionObject(version=constructor(version))
assert application.version
assert application.model_dump() == {'version': version}


@pytest.mark.parametrize('invalid_version', ['no dots string', 'with.dots.string', ''])
@pytest.mark.parametrize(
'invalid_version',
[
'',
'1',
'1.2',
'1.2.3-0123',
'1.2.3-0123.0123',
'1.1.2+.123',
'+invalid',
'-invalid',
'-invalid+invalid',
'-invalid.01',
'alpha',
'alpha.beta',
'alpha.beta.1',
'alpha.1',
'alpha+beta',
'alpha_beta',
'alpha.',
'alpha..',
'beta',
'1.0.0-alpha_beta',
'-alpha.',
'1.0.0-alpha..',
'1.0.0-alpha..1',
'1.0.0-alpha...1',
'1.0.0-alpha....1',
'1.0.0-alpha.....1',
'1.0.0-alpha......1',
'1.0.0-alpha.......1',
'01.1.1',
'1.01.1',
'1.1.01',
'1.2',
'1.2.3.DEV',
'1.2-SNAPSHOT',
'1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788',
'1.2-RC-SNAPSHOT',
'-1.0.3-gamma+b7718',
'+justmeta',
'9.8.7+meta+meta',
'9.8.7-whatever+meta+meta',
'99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12',
],
)
def test_invalid_semantic_version(SemanticVersionObject, invalid_version):
with pytest.raises(ValidationError):
SemanticVersionObject(version=invalid_version)

0 comments on commit f7875f0

Please sign in to comment.