1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131 | """Versioned model example.
Library versions:
pydantic<2
semver~=3.0.0 # additional
"""
import warnings
from typing import Optional, Tuple, Type
from pydantic import BaseModel, validator
from semver import Version # type: ignore
def _chk_between(v, lo=None, hi=None):
if v is None:
return
if (hi is not None) and (v > hi):
raise ValueError(f"Default version higher than maximum: {v} > {hi}")
if (lo is not None) and (v < lo):
raise ValueError(f"Default version lower than minimum: {v} < {lo}")
def _get_minmax_robust(
cls: Type["VersionedModel"],
) -> Tuple[Optional[Version], Optional[Version]]:
min_, max_ = None, None
for supcls in cls.mro():
Config = getattr(supcls, "Config", None)
if Config is not None:
if min_ is None:
min_ = getattr(Config, "min_version", None)
if max_ is None:
max_ = getattr(Config, "max_version", None)
return min_, max_
class VersionedModel(BaseModel):
"""Versioned model behavior."""
version: Version
class Config:
"""Pydantic configuration."""
# Allow SemVer
arbitrary_types_allowed = True
json_encoders = {Version: lambda x: str(x)}
# Version limits
min_version = Version(0, 0, 0)
max_version = None
@validator("version", pre=True)
def _check_version(cls: Type["VersionedModel"], v) -> Version: # type: ignore
"""Set version from a string, then check within the limits."""
if not isinstance(v, Version):
v = Version.parse(v)
min_, max_ = _get_minmax_robust(cls)
_chk_between(v, lo=min_, hi=max_)
return v
def __init_subclass__(cls) -> None:
"""Set config values."""
# Check Config class types
Config = getattr(cls, "Config", None)
if Config is not None:
# check one field
minv = getattr(Config, "min_version", None)
if minv is not None:
if not isinstance(minv, Version):
setattr(Config, "min_version", Version.parse(minv))
# check other field
maxv = getattr(Config, "max_version", None)
if maxv is not None:
if not isinstance(maxv, Version):
setattr(Config, "max_version", Version.parse(maxv))
# Check ranges
min_, max_ = _get_minmax_robust(cls)
if (min_ is not None) and (max_ is not None) and (min_ > max_):
raise ValueError(f"Minimum version higher than maximum: {min_!r} > {max_!r}")
# Check the default value of the "version" field
fld = cls.__fields__["version"]
d = fld.default
if d is None:
pass
else:
_chk_between(d, lo=min_, hi=max_)
warnings.warn(
f"Recommended to have `version` be required, but set default {d!r}",
UserWarning,
)
if not issubclass(fld.type_, Version):
raise TypeError(f"Field type for `version` must be Version, got {fld.type_!r}")
if __name__ == "__main__":
from pydantic_yaml import parse_yaml_raw_as
class FooBar(VersionedModel):
"""Foobar model."""
foo: str
class Config:
"""Pydantic configuration."""
max_version = Version(1)
fb = parse_yaml_raw_as(
FooBar,
"""
version: 0.2.0
foo: bar
""",
)
try:
parse_yaml_raw_as(
FooBar,
"""
version: 1.2.0 # higher than v1!
foo: bar
""",
)
except Exception as e:
print(e)
|