Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Replace APISchema with Pydantic2 #85

Merged
merged 30 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
df0d471
switched Support to Pydantic2 (intermediate breaking change)
gilesknap Jul 11, 2023
05879b0
first pydantic pass: ioc.py
gilesknap Jul 11, 2023
4112595
ioc.py pydantic working except objects
gilesknap Jul 11, 2023
d5dde2d
partially working id tracking
gilesknap Jul 12, 2023
c2cca52
add TODO comments on existing issues
gilesknap Jul 12, 2023
8285b3c
pydantiic test_ioc_build test working but test_ioc_schema is not
gilesknap Jul 13, 2023
5b5b7a5
great progress
gilesknap Jul 13, 2023
1edcea8
values expansion working
gilesknap Jul 14, 2023
c66c237
id lookup working!
gilesknap Jul 14, 2023
0ba40e0
utils in values render fixed
gilesknap Jul 14, 2023
3ab0625
fix some tests
gilesknap Jul 14, 2023
f34b4d6
fix more tests
gilesknap Jul 14, 2023
995783b
all tests working
gilesknap Jul 14, 2023
31647ea
update ibek-defs
gilesknap Jul 14, 2023
c406526
update TODOs
gilesknap Jul 14, 2023
899f7fe
fix dependencies
gilesknap Jul 14, 2023
e21035c
add check of defaults to pydantic tests
gilesknap Jul 14, 2023
ab4e25c
fix lint
gilesknap Jul 14, 2023
a8f88dc
remove back quotes in docstrings
gilesknap Jul 14, 2023
72188cf
try to fix docs
gilesknap Jul 14, 2023
69d050e
remove apischema sphinx plugin
gilesknap Jul 14, 2023
1a727d5
fix sphinx
gilesknap Jul 17, 2023
fd14fb4
add comment re schema error messages
gilesknap Jul 17, 2023
90ee451
add simple references example
gilesknap Jul 17, 2023
003f6b2
add example of object ref with createmodel
gilesknap Jul 17, 2023
479c330
another example of refs - more like ioc.py
gilesknap Jul 17, 2023
b44b119
fix pydantic test broken yaml file
gilesknap Jul 17, 2023
9b9891f
update examples with more info
gilesknap Jul 18, 2023
99b5ab8
add test example that is identical to ibek?
gilesknap Jul 18, 2023
dd3c950
updated examples to demo issue
gilesknap Jul 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "Debug Unit Test",
"name": "Debug example test",
"type": "python",
"request": "launch",
"justMyCode": false,
Expand Down
9 changes: 4 additions & 5 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@
"sphinx.ext.viewcode",
# Adds the inheritance-diagram generation directive
"sphinx.ext.inheritance_diagram",
# Makes autodoc understand apischema annotated classes/functions
"sphinx_apischema",
# Add a copy button to each code block
"sphinx_copybutton",
# For the card element
Expand All @@ -67,16 +65,17 @@
("py:class", "typing_extensions.Literal"),
]

# Both the class’ and the __init__ method’s docstring are concatenated and
# inserted into the main body of the autoclass directive
autoclass_content = "both"
# Dont use the __init__ docstring because pydantic base classes cause sphinx
# to generate a lot of warnings
autoclass_content = "class"

# Order the members by the order they appear in the source code
autodoc_member_order = "bysource"

# Don't inherit docstrings from baseclasses
autodoc_inherit_docstrings = False


# Output graphviz directive produced images in a scalable format
graphviz_output_format = "svg"

Expand Down
38 changes: 38 additions & 0 deletions examples/README.md
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
60 changes: 60 additions & 0 deletions examples/test_refs1.py
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"},
]
}
)
75 changes: 75 additions & 0 deletions examples/test_refs2.py
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"},
]
}
)
104 changes: 104 additions & 0 deletions examples/test_refs3.py
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",
},
]
}
)
Loading