-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #85 from epics-containers/pydantic2
Replace APISchema with Pydantic2
- Loading branch information
Showing
43 changed files
with
9,715 additions
and
5,935 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
Intro | ||
===== | ||
|
||
These example scripts are used to investigate an issue with error reporting in ibek. | ||
|
||
They also serve as a good minimal example of how to do object references within a pydantic 2 model. | ||
|
||
The incrementing numeric suffix represents a progression from the most simple possible example of a pydantic model with a reference to a more complex example that more closely resembles ibek's approach which dynamically creates the Entity classes. | ||
|
||
In the yaml subfolder is a support module yaml and IOC yaml that will make ibek load a very similar model to that described in these test scripts. | ||
|
||
Issue under investigation | ||
========================= | ||
|
||
The issue is that when an object refers to another object then the error reported is that the offending object's id cannot be found. This masks the underlying schema issue which is what should be reported first. The custom field validator created in make_entity_model seems to be throwing the error before the schema validation issue is reported. | ||
|
||
At present for the incorrect schema in entity e1 ibek reports: | ||
|
||
``` | ||
KeyError: 'object one not found in []' | ||
``` | ||
|
||
And test_refs4.py reports | ||
|
||
``` | ||
Extra inputs are not permitted [type=extra_forbidden, input_value='bad argument', input_type=str] | ||
``` | ||
|
||
The latter is the useful error that points you at the root cause. | ||
|
||
Resolution | ||
========== | ||
|
||
The simplest test_refs1.py has been updated to demo the issue (forgot that | ||
entity "one" already existed in model1!). | ||
|
||
I've posted a discussion on the subject here | ||
https://github.com/pydantic/pydantic/discussions/6731 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
from __future__ import annotations | ||
|
||
from typing import Dict, List, Optional | ||
|
||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator | ||
|
||
id_to_entity: Dict[str, Entity] = {} | ||
|
||
|
||
class Entity(BaseModel): | ||
name: str = Field(..., description="The name of this entity") | ||
value: str = Field(..., description="The value of this entity") | ||
ref: Optional[str] = Field( | ||
default=None, description="Reference another Entity name" | ||
) | ||
model_config = ConfigDict(extra="forbid") | ||
|
||
@model_validator(mode="after") # type: ignore | ||
def add_ibek_attributes(cls, entity: Entity): | ||
id_to_entity[entity.name] = entity | ||
|
||
return entity | ||
|
||
@field_validator("ref", mode="after") | ||
def lookup_instance(cls, id): | ||
try: | ||
return id_to_entity[id] | ||
except KeyError: | ||
raise KeyError(f"object {id} not found in {list(id_to_entity)}") | ||
|
||
|
||
class Entities(BaseModel): | ||
entities: List[Entity] = Field(..., description="The entities in this model") | ||
|
||
|
||
model1 = Entities( | ||
**{ | ||
"entities": [ | ||
{"name": "one", "value": "OneValue"}, | ||
{"name": "two", "value": "TwoValue", "ref": "one"}, | ||
] | ||
} | ||
) | ||
|
||
# demonstrate that entity two has a reference to entity one | ||
assert model1.entities[1].ref.value == "OneValue" | ||
|
||
# this should throw an error because entity one_again has illegal arguments | ||
# BUT the error shown is: | ||
# KeyError: "object one_again not found in ['one', 'two']" | ||
# which masks the underlying schema violation error that should look like: | ||
# Extra inputs are not permitted [type=extra_forbidden, input_value='bad argument', | ||
model2 = Entities( | ||
**{ | ||
"entities": [ | ||
{"name": "one_again", "value": "OneValue", "illegal": "bad argument"}, | ||
{"name": "two_again", "value": "TwoValue", "ref": "one_again"}, | ||
] | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
from __future__ import annotations | ||
|
||
from typing import Dict, List, Optional | ||
|
||
from pydantic import ( | ||
BaseModel, | ||
ConfigDict, | ||
Field, | ||
create_model, | ||
field_validator, | ||
model_validator, | ||
) | ||
|
||
id_to_entity: Dict[str, Entity] = {} | ||
|
||
|
||
class Entity(BaseModel): | ||
name: str = Field(..., description="The name of this entity") | ||
value: str = Field(..., description="The value of this entity") | ||
ref: Optional[str] = Field( | ||
default=None, description="Reference another Entity name" | ||
) | ||
model_config = ConfigDict(extra="forbid") | ||
|
||
@model_validator(mode="after") # type: ignore | ||
def add_ibek_attributes(cls, entity: Entity): | ||
id_to_entity[entity.name] = entity | ||
|
||
return entity | ||
|
||
|
||
@field_validator("ref", mode="after") | ||
def lookup_instance(cls, id): | ||
try: | ||
return id_to_entity[id] | ||
except KeyError: | ||
raise KeyError(f"object {id} not found in {list(id_to_entity)}") | ||
|
||
|
||
validators = {"Entity": lookup_instance} | ||
|
||
# add validator to the Entity class using create model | ||
Entity2 = create_model( | ||
"Entity", | ||
__validators__=validators, | ||
__base__=Entity, | ||
) # type: ignore | ||
|
||
args = {"entities": (List[Entity2], None)} | ||
Entities = create_model( | ||
"Entities", **args, __config__=ConfigDict(extra="forbid") | ||
) # type: ignore | ||
|
||
|
||
model1 = Entities( | ||
**{ | ||
"entities": [ | ||
{"name": "one", "value": "OneValue"}, | ||
{"name": "two", "value": "TwoValue", "ref": "one"}, | ||
] | ||
} | ||
) | ||
|
||
# demonstrate that entity one has a reference to entity two | ||
assert model1.entities[1].ref.value == "OneValue" | ||
|
||
# this should throw an error because entity one has illegal arguments | ||
model2 = Entities( | ||
**{ | ||
"entities": [ | ||
{"name": "one", "value": "OneValue", "illegal": "bad argument"}, | ||
{"name": "two", "value": "TwoValue", "ref": "one"}, | ||
] | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
from __future__ import annotations | ||
|
||
from typing import Dict, Literal, Optional, Sequence, Union | ||
|
||
from pydantic import ( | ||
BaseModel, | ||
ConfigDict, | ||
Field, | ||
RootModel, | ||
create_model, | ||
field_validator, | ||
model_validator, | ||
) | ||
|
||
id_to_entity: Dict[str, Entity] = {} | ||
|
||
|
||
class Entity(BaseModel): | ||
type: str = Field(description="The type of this entity") | ||
name: str = Field(..., description="The name of this entity") | ||
value: str = Field(..., description="The value of this entity") | ||
ref: Optional[str] = Field( | ||
default=None, description="Reference another Entity name" | ||
) | ||
model_config = ConfigDict(extra="forbid") | ||
|
||
@model_validator(mode="after") # type: ignore | ||
def add_ibek_attributes(cls, entity: Entity): | ||
id_to_entity[entity.name] = entity | ||
|
||
return entity | ||
|
||
|
||
class Entity1(Entity): | ||
type: Literal["e1"] = Field(description="The type of this entity") | ||
|
||
|
||
class Entity2(Entity): | ||
type: Literal["e2"] = Field(description="The type of this entity") | ||
|
||
|
||
@field_validator("ref", mode="after") | ||
def lookup_instance(cls, id): | ||
try: | ||
return id_to_entity[id] | ||
except KeyError: | ||
raise KeyError(f"object {id} not found in {list(id_to_entity)}") | ||
|
||
|
||
validators = {"Entity": lookup_instance} | ||
|
||
# add validator to the Entity classes using create model | ||
EntityOne = create_model( | ||
"EntityOne", | ||
__validators__=validators, | ||
__base__=Entity1, | ||
) # type: ignore | ||
|
||
EntityTwo = create_model( | ||
"EntityTwo", | ||
__validators__=validators, | ||
__base__=Entity2, | ||
) # type: ignore | ||
|
||
entity_models = (EntityOne, EntityTwo) | ||
|
||
|
||
class EntityModel(RootModel): | ||
root: Union[entity_models] = Field(discriminator="type") # type: ignore | ||
|
||
|
||
class Entities(BaseModel): | ||
model_config = ConfigDict(extra="forbid") | ||
entities: Sequence[EntityModel] = Field( # type: ignore | ||
description="List of entities classes we want to create" | ||
) | ||
|
||
|
||
model1 = Entities( | ||
**{ | ||
"entities": [ | ||
{"type": "e1", "name": "one", "value": "OneValue"}, | ||
{"type": "e2", "name": "two", "value": "TwoValue", "ref": "one"}, | ||
] | ||
} | ||
) | ||
|
||
# demonstrate that entity one has a reference to entity two | ||
assert model1.entities[1].root.ref.value == "OneValue" | ||
|
||
# this should throw an error because entity one has illegal arguments | ||
model2 = Entities( | ||
**{ | ||
"entities": [ | ||
{"type": "e2", "name": "two", "value": "TwoValue", "ref": "one"}, | ||
{ | ||
"type": "e1", | ||
"name": "one", | ||
"value": "OneValue", | ||
"illegal": "bad argument", | ||
}, | ||
] | ||
} | ||
) |
Oops, something went wrong.