Skip to content

Commit 8c7a056

Browse files
committed
Prepare for pydanticv2
1 parent 8aa70f0 commit 8c7a056

File tree

5 files changed

+471
-364
lines changed

5 files changed

+471
-364
lines changed

fhir_py_types/ast.py

+29-61
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,13 @@ def format_identifier(
7979
def uppercamelcase(s: str) -> str:
8080
return s[:1].upper() + s[1:]
8181

82-
return (
83-
identifier + uppercamelcase(type_.code)
84-
if is_polymorphic(definition)
85-
else identifier
86-
)
82+
if is_polymorphic(definition):
83+
# TODO: it's fast hack
84+
if type_.code[0].islower():
85+
return identifier + uppercamelcase(clear_primitive_id(type_.code))
86+
return identifier + uppercamelcase(type_.code)
87+
88+
return identifier
8789

8890

8991
def remap_type(
@@ -94,8 +96,8 @@ def remap_type(
9496
case "Resource":
9597
# Different contexts use 'Resource' type to refer to any
9698
# resource differentiated by its 'resourceType' (tagged union).
97-
# 'AnyResource' is not defined by the spec but rather
98-
# generated as a union of all defined resource types.
99+
# 'AnyResource' is defined in header as a special type
100+
# that dynamically replaced with a right type in run-time
99101
type_ = replace(type_, code="AnyResource")
100102

101103
if is_polymorphic(definition):
@@ -106,6 +108,12 @@ def remap_type(
106108
# with a custom validator that will enforce single required property rule.
107109
type_ = replace(type_, required=False)
108110

111+
if is_primitive_type(type_):
112+
# Primitive types defined from the small letter (like code)
113+
# and it might overlap with model fields
114+
# e.g. QuestionnaireItem has attribute code and linkId has type code
115+
type_ = replace(type_, code=make_primitive_id(type_.code))
116+
109117
return type_
110118

111119

@@ -116,9 +124,8 @@ def zip_identifier_type(
116124

117125
for t in [remap_type(definition, t) for t in definition.type]:
118126
result.append((format_identifier(definition, identifier, t), t))
119-
if (
120-
definition.kind != StructureDefinitionKind.PRIMITIVE
121-
and is_primitive_type(t)
127+
if definition.kind != StructureDefinitionKind.PRIMITIVE and is_primitive_type(
128+
t
122129
):
123130
result.append(
124131
(
@@ -201,11 +208,6 @@ def define_class_object(
201208
keywords=[],
202209
type_params=[],
203210
),
204-
ast.Call(
205-
ast.Attribute(value=ast.Name(definition.id), attr="update_forward_refs"),
206-
args=[],
207-
keywords=[],
208-
),
209211
]
210212

211213

@@ -214,48 +216,22 @@ def define_class(definition: StructureDefinition) -> Iterable[ast.stmt | ast.exp
214216

215217

216218
def define_alias(definition: StructureDefinition) -> Iterable[ast.stmt]:
217-
return type_annotate(definition, definition.id, AnnotationForm.TypeAlias)
218-
219-
220-
def define_tagged_union(
221-
name: str, components: Iterable[StructureDefinition], distinct_by: str
222-
) -> ast.stmt:
223-
annotation = functools.reduce(
224-
lambda acc, n: ast.BinOp(left=acc, right=n, op=ast.BitOr()),
225-
(cast(ast.expr, ast.Name(d.id)) for d in components),
219+
# Primitive types are renamed to another name to avoid overlapping with model fields
220+
return type_annotate(
221+
definition, make_primitive_id(definition.id), AnnotationForm.TypeAlias
226222
)
227223

228-
return ast.Assign(
229-
targets=[ast.Name(name)],
230-
value=ast.Subscript(
231-
value=ast.Name("Annotated_"),
232-
slice=ast.Tuple(
233-
elts=[
234-
annotation,
235-
ast.Call(
236-
ast.Name("Field"),
237-
args=[ast.Constant(...)],
238-
keywords=[
239-
ast.keyword(
240-
arg="discriminator", value=ast.Constant(distinct_by)
241-
),
242-
],
243-
),
244-
]
245-
),
246-
),
247-
)
248224

225+
def make_primitive_id(name: str) -> str:
226+
if name in ("str", "int", "float", "bool"):
227+
return name
228+
return f"{name}Type"
249229

250-
def select_tagged_resources(
251-
definitions: Iterable[StructureDefinition], key: str
252-
) -> Iterable[StructureDefinition]:
253-
return (
254-
definition
255-
for definition in definitions
256-
if definition.kind == StructureDefinitionKind.RESOURCE
257-
and key in definition.elements
258-
)
230+
231+
def clear_primitive_id(name: str) -> str:
232+
if name.endswith("Type"):
233+
return name[:-4]
234+
return name
259235

260236

261237
def select_nested_definitions(
@@ -301,14 +277,6 @@ def build_ast(
301277
f"Unsupported definition {definition.id} of kind {definition.kind}, skipping"
302278
)
303279

304-
resources = list(select_tagged_resources(structure_definitions, key="resourceType"))
305-
if resources:
306-
typedefinitions.append(
307-
define_tagged_union(
308-
name="AnyResource", components=resources, distinct_by="resourceType"
309-
)
310-
)
311-
312280
return sorted(
313281
typedefinitions,
314282
# Defer any postprocessing until after the structure tree is defined.

fhir_py_types/header.py.tpl

+91-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,100 @@
1-
from typing import List as List_, Optional as Optional_, Literal as Literal_, Annotated as Annotated_
1+
import warnings
2+
from typing import List as List_, Optional as Optional_, Literal as Literal_
23

3-
from pydantic import BaseModel as BaseModel_, Field, Extra
4+
from pydantic import PydanticDeprecatedSince20
5+
from pydantic import (
6+
BaseModel as BaseModel_,
7+
Field,
8+
Extra,
9+
field_validator,
10+
ValidationError,
11+
)
12+
from pydantic_core import PydanticCustomError
13+
14+
15+
class AnyResource(BaseModel_):
16+
class Config:
17+
extra = Extra.allow
18+
19+
resourceType: str
420

521

622
class BaseModel(BaseModel_):
723
class Config:
824
extra = Extra.forbid
925
validate_assignment = True
10-
allow_population_by_field_name = True
26+
populate_by_name = True
27+
defer_build = True
28+
29+
def model_dump(self, *args, **kwargs):
30+
by_alias = kwargs.pop("by_alias", True)
31+
return super().model_dump(*args, **kwargs, by_alias=by_alias)
1132

1233
def dict(self, *args, **kwargs):
13-
by_alias = kwargs.pop('by_alias', True)
14-
return super().dict(*args, **kwargs, by_alias=by_alias)
34+
warnings.warn(
35+
"The `dict` method is deprecated; use `model_dump` instead.",
36+
category=PydanticDeprecatedSince20,
37+
)
38+
by_alias = kwargs.pop("by_alias", True)
39+
return super().model_dump(*args, **kwargs, by_alias=by_alias)
40+
41+
@field_validator("*")
42+
@classmethod
43+
def validate(cls, value):
44+
if isinstance(value, list):
45+
return [
46+
_init_any_resource(v, index=index) for index, v in enumerate(value)
47+
]
48+
return _init_any_resource(value)
49+
50+
51+
def _init_any_resource(value, index=None):
52+
if isinstance(value, AnyResource):
53+
try:
54+
klass = globals().get(value.resourceType)
55+
except PydanticCustomError as exc:
56+
raise ValidationError.from_exception_data(
57+
"ImportError",
58+
[
59+
{
60+
"loc": [index, "resourceType"],
61+
"type": "value_error",
62+
"msg": f"{value.resourceType} resource is not found",
63+
"input": [value],
64+
"ctx": {
65+
"error": f"{value.resourceType} resource is not found"
66+
},
67+
}
68+
],
69+
) from exc
70+
71+
if (
72+
not issubclass(klass, BaseModel)
73+
or "resourceType" not in klass.__fields__
74+
):
75+
raise ValidationError.from_exception_data(
76+
"ImportError",
77+
[
78+
{
79+
"loc": [index, "resourceType"],
80+
"type": "value_error",
81+
"msg": f"{value.resourceType} is not a resource",
82+
"input": [value],
83+
"ctx": {
84+
"error": f"{value.resourceType} is not a resource"
85+
},
86+
}
87+
],
88+
)
89+
90+
try:
91+
return klass(**value.model_dump())
92+
except ValidationError as exc:
93+
raise ValidationError.from_exception_data(
94+
exc.title,
95+
[{**error, "loc": [index, *error["loc"]]} for error in exc.errors()]
96+
if index is not None
97+
else exc.errors(),
98+
) from exc
99+
100+
return value

0 commit comments

Comments
 (0)