diff --git a/pydantic_extra_types/semantic_version.py b/pydantic_extra_types/semantic_version.py index f0945fd..79aa64e 100644 --- a/pydantic_extra_types/semantic_version.py +++ b/pydantic_extra_types/semantic_version.py @@ -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). """ @@ -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( [ @@ -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-]+)*))?$' + ) + ) diff --git a/tests/test_json_schema.py b/tests/test_json_schema.py index efeade3..0580279 100644 --- a/tests/test_json_schema.py +++ b/tests/test_json_schema.py @@ -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', diff --git a/tests/test_semantic_version.py b/tests/test_semantic_version.py index 4df76a3..5e4d6a2 100644 --- a/tests/test_semantic_version.py +++ b/tests/test_semantic_version.py @@ -1,4 +1,5 @@ import pytest +import semver from pydantic import BaseModel, ValidationError from pydantic_extra_types.semantic_version import SemanticVersion @@ -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)