-
-
Notifications
You must be signed in to change notification settings - Fork 533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Full support for nested pydantic, sqlmodel and ormar models #1637
Conversation
Thanks for adding the Here's a preview of the changelog:
PydanticGraphQL container types ( class User(pydantic.BaseModel):
name: str
hobby: Optional[List["Hobby"]]
class Hobby(pydantic.BaseModel):
name: str
@strawberry.experimental.pydantic.type(User, all_fields=True)
class UserType:
pass
@strawberry.experimental.pydantic.type(Hobby, all_fields=True)
class HobbyType:
pass Ormar
class Hobby(ormar.Model):
name: str
class User(ormar.Model):
name: str = ormar.String(max_length=255)
hobby: Hobby = ormar.ForeignKey(Hobby, nullable=False)
@strawberry.experimental.pydantic.type(Hobby, all_fields=True)
class HobbyType:
pass
@strawberry.experimental.pydantic.type(User, all_fields=True)
class UserType:
pass type HobbyType {
name: String!
users: [UserType]
}
type UserType {
name: String!
hobby: HobbyType!
} SLQModelSQLModel is another pydantic-based orm, that uses SQLAlchemy to define models. All relations are defined using the class Hobby(SQLModel, table=True):
name: str
users: List["User"] = Relationship(back_populates="hobby")
class User(SQLModel, table=True):
name: str = Field()
hobby: Hobby = Relationship(back_populates="users")
@strawberry.experimental.pydantic.type(Hobby, all_fields=True)
class HobbyType:
pass
@strawberry.experimental.pydantic.type(User, all_fields=True)
class UserType:
pass type HobbyType {
name: String!
users: [UserType!]!
}
type UserType {
name: String!
hobby: HobbyType!
} Here's the preview release card for twitter: Here's the tweet text:
|
Hi @gazorby! Thanks for this PR, I'll try to take a look in the weekend <3 |
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## main #1637 +/- ##
==========================================
- Coverage 98.14% 98.12% -0.02%
==========================================
Files 129 131 +2
Lines 4528 4649 +121
Branches 779 799 +20
==========================================
+ Hits 4444 4562 +118
- Misses 43 45 +2
- Partials 41 42 +1 |
Hi @patrick91! |
pyproject.toml
Outdated
sqlmodel = {version = "^0.0.6", optional = true} | ||
ormar = {version = "^0.10.24", optional = true} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The inclusion of sqlmodel and ormar had an impact on sqlalchemy which I had locked to 1.4.31 in my test project, ie it forced a downgrade like so:
Because strawberry-graphql (0.96.0) depends on ormar (>=0.10.24,<0.11.0)
and no versions of ormar match >0.10.24,<0.11.0, strawberry-graphql (0.96.0) requires ormar (0.10.24).
And because ormar (0.10.24) depends on SQLAlchemy (>=1.3.18,<=1.4.29), strawberry-graphql (0.96.0) requires SQLAlchemy (>=1.3.18,<=1.4.29).
And because sqlalchemy (1.4.31) depends on sqlalchemy (1.4.31)
and no versions of sqlalchemy match >1.4.31,<2.0.0, strawberry-graphql (0.96.0) is incompatible with SQLAlchemy (>=1.4.31,<2.0.0).
So, because main-api-server depends on both SQLAlchemy (^1.4.31) and strawberry-graphql (0.96.0), version solving failed.
after changing the pin on 1.4.31 the changes were as follows
Updating sqlalchemy (1.4.31 -> 1.4.29)
• Installing databases (0.5.4)
• Installing ormar (0.10.24)
• Installing sqlmodel (0.0.6)
• Updating strawberry-graphql (0.97.0 -> 0.96.0 /test/strawberry/dist/strawberry_graphql-0.96.0-py3-none-any.whl)
They're currently marked as optional; adding them to [tool.poetry.extras]
prevents them to be installed by default
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, b95dd95 has fixed that
if hasattr(type_, "__args__"): | ||
replaced_type = type_.copy_with( | ||
tuple(replace_pydantic_types(t, is_input) for t in type_.__args__) | ||
tuple( | ||
replace_pydantic_types(t, is_input, model, name) for t in type_.__args__ | ||
) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @gazorby
Many thanks for your work on this :-)
I'm testing the PR against a set of nested Pydantic Models. In this section, I'm running into an issue with pydantic fields using a list like orders: Optional[list[Order]]
and using a dict like name_dir: dict[str, Any]
where it then throws AttributeError: type object 'dict' has no attribute 'copy_with'
or AttributeError: type object 'list' has no attribute 'copy_with'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @alexhafner
Thanks for your reports, I think we don't support builtin types (like list
) yet, only those from the typing module. I tested with the following nested structure and it works:
class HobbyModel(pydantic.BaseModel):
name: str
class UserModel(pydantic.BaseModel):
age: int
hobbies: Optional[List[HobbyModel]]
@strawberry.experimental.pydantic.type(HobbyModel, fields=["name"])
class Hobby:
pass
@strawberry.experimental.pydantic.type(UserModel, fields=["age", "hobbies"])
class User:
pass
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for that @gazorby - I have changed the underlying pydantic models using list[] to List[] and it works fine. dict[...] or Dict[...] doesn't seem to be supported at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some of the checks in if issubclass(type_, BaseModel):
fail with issubclass() arg 1 must be a class
. For example if you try to use Dict[str, str]
you get TypeError: Foo fields cannot be resolved. Unexpected type 'typing.Dict[str, str]'
. With Dict[str, Any]
you end up with TypeError: issubclass() arg 1 must be a class
. Once you add an additional class check like if inspect.isclass(type_) and issubclass(type_, BaseModel):
then the expected error is returned TypeError: Foo fields cannot be resolved. Unexpected type 'typing.Dict[str, typing.Any]'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does strawberry currently support typing
containers, e.g. typing.Set
? I see errors like
TypeError: Unexpected type 'typing.Set[pydantic.networks.EmailStr]'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GraphQL doesn't have a Set container type, only List. You'll have to convert it to list before you instantiate the strawberry ObjectType
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, gotcha, thanks for the pointer and apologies for hijacking the PR here 🙇
For anyone landing on this page, here are the docs and my code to solve this:
@staticmethod
def from_pydantic(
instance: MyPydanticModel,
extra: Optional[Dict[str, Any]] = None,
) -> 'MyModel':
data = instance.dict()
data['emails'] = list(data.get('emails', []))
return MyModel(**data)
def to_pydantic(self) -> MyPydanticModel:
data = dataclasses.asdict(self)
data['emails'] = {EmailStr(email) for email in data.get('emails', [])}
return MyPydanticModel(**data)
def replace_pydantic_types( | ||
type_: Any, is_input: bool, model: Type[BaseModel], name: str | ||
): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did also run in the Enum issue throwing `TypeError: StrawberryModel fields cannot be resolved. _enum_definition` raised in #1598. The solution suggested there, with the addition of a type check before the issubclass check, solved the issue for me, along the lines of
import inspect
from enum import Enum
# ...
if inspect.isclass(type_) and issubclass(type_, Enum):
strawberry.enum(type_)
@gazorby thank you so much for doing this! is there any chance we can split this PR into multiple ones? it would make it easier to get things merged 😊 |
@gazorby @patrick91 I wonder how we could move this forward / split the PR if required. I've used @gazorby 's fork in a POC for a while (the Pydantic features, not ormar or SQLModel) and I feel features like the nested models delivered here are essential for most non-trivial Strawberry implementations using Pydantic. With the (thankfully!) frequent releases of Strawberry though this is not sustainable for long as we want to uptake the latest features and releases, and some pydantic related code has also been moving again. Would be fantastic to be able to merge the functionality soon. |
Hi @alexhafner, thanks for the message, that's great to know! I might need to do some pydantic+strawberry stuff for work so I should be able to take a look at this PR :) Are you on discord by the way? I might want to ask you some questions regarding this PR 😊 |
@patrick91 sure thing, I'll message you there |
Sorry for being silent here, was busy on other projects. I realized that doing the same in strawberry if this PR was merged would be quite cumbersome as it lacks some way of customizing what's happening during nested generation (eg: define a resolver that would accept a page input and return a connection). Of course we don't want to bloat more this PR by adding relay support, but maybe some bindings (decorator param?) to let strawberry users customize how nested types would be generated. |
no worries at all!
Yup, I agree 😌
Did you make a PR? would be interesting to see how you implemented it
Would you be able to write a summary for this? Also what do you think of splitting this PR in multiple pieces? |
Hi! Is this feature development still in active development? |
@gazorby a lot of great work here. Worth breaking down what can be merged straight away into a separate pr? |
@gazorby These are fantastic changes! Any chance we can get them merged soon? |
hi, I need exclude pls this PR looks great |
Unfortunately I have no more interest in deriving strawberry types from sqlmodel/ormar models and pydantic first class support is tracked in #2181. |
This PR add the following features:
List
,Optional
,Union
andForwardRef
)ForeignKey
,ManyToMany
, and reverse relations)Relationship
fieldsexclude
param to thestrawberry.experimental.pydantic.type
decorator, allowing to include all fields while excluding someDescription
Pydantic
Nested pydantic models should works in most situations, as long as it's GraphQL typed.
Example :
The order in which strawberry types are defined doesn't matter (doesn't have to follow pydantic models declaration order):
Ormar
Omar is an orm that uses pydantic as its base: all ormar models are pydantic models.
ForeignKey
,ManyToMany
and reverse relations (related fields in Django) are supported.When using the pydantic decorator to generate a strawberry type, ormar fields will be mapped as you would expect:
Will gives the following GraphQL schema:
When using
all_fields=True
it also includes the reverse relationusers
that ormar automatically created in theHobby
model:SQLModel
SQLModel is another pydantic-based orm, that uses SQLAlchemy to define models. All relations are defined using the
Relationship
field:Will translate in the following schema:
Types of Changes
Issues Fixed or Closed by This PR
Checklist