Skip to content

Commit e0301f8

Browse files
committed
Add a pydantic-like enforcement example
1 parent 9036d55 commit e0301f8

1 file changed

Lines changed: 75 additions & 0 deletions

File tree

tests/test_model_like.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Demonstrate how something like Pydantic might use this library to
2+
# enforce type annotations.
3+
4+
import pytest
5+
6+
from typing import _GenericAlias
7+
8+
import typemap_extensions as typing
9+
10+
from typemap.type_eval import eval_typing, flatten_class
11+
12+
13+
class BaseModel:
14+
def __init__(self, *, _alias=None, **kwargs):
15+
to_eval = _alias or type(self)
16+
# This is somewhat careless, a real implementation would
17+
# probably do more checking and produce better errors
18+
# Also, it would do caching.
19+
20+
# * eval_typing evaluates the class, substituting in type variables
21+
# * flatten_class generates a merged class that has all the
22+
# annotations from the whole mro present
23+
ocls = flatten_class(eval_typing(to_eval))
24+
annos = ocls.__annotations__
25+
26+
for k, v in kwargs.items():
27+
# A real implementation would also have to do more here,
28+
# like handle containers, etc!
29+
if not isinstance(v, annos[k]):
30+
raise TypeError(
31+
f'Invalid type for {k} - '
32+
f'got {type(v)} but needed {annos[k]}'
33+
)
34+
setattr(self, k, v)
35+
36+
@classmethod
37+
def __class_getitem__(cls, args):
38+
# Return an _BaseModelAlias instead of a _GenericAlias
39+
res = super().__class_getitem__(args)
40+
return _BaseModelAlias(res.__origin__, res.__args__)
41+
42+
def __repr__(self):
43+
# Just debugging output
44+
return f'{type(self).__name__}(**{self.__dict__})'
45+
46+
47+
class _BaseModelAlias(_GenericAlias, _root=True):
48+
def __call__(self, *args, **kwargs):
49+
return self.__origin__(*args, **kwargs, _alias=self)
50+
51+
52+
#######
53+
54+
55+
class Foo[T](BaseModel):
56+
x: int
57+
y: T
58+
59+
60+
def test_model_like_1():
61+
Foo[str](x=0, y='yes') # OK
62+
with pytest.raises(TypeError):
63+
Foo[str](x=0, y=False) # error
64+
65+
66+
class MyModel[T](BaseModel):
67+
x: T
68+
y: int if typing.IsAssignable[T, int] else str
69+
70+
71+
def test_model_like_2():
72+
MyModel[int](x=1, y=1) # OK
73+
MyModel[float](x=1.0, y="x") # OK
74+
with pytest.raises(TypeError):
75+
MyModel[float](x=1.0, y=1) # error, second arg must be str

0 commit comments

Comments
 (0)