-
-
Notifications
You must be signed in to change notification settings - Fork 593
Define PartialType metaclass #3994
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
Release type: minor | ||
|
||
Add `PartialType` metaclass to make fields optional dynamically. | ||
|
||
```py | ||
from strawberry.tools import PartialType | ||
|
||
|
||
@strawberry.type | ||
class UserCreate: | ||
firstname: str | ||
lastname: str | ||
|
||
|
||
@strawberry.type | ||
class UserUpdate(UserCreate, metaclass=PartialType): | ||
pass | ||
``` |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -94,4 +94,33 @@ type ComboQuery { | |||||||||||||
} | ||||||||||||||
``` | ||||||||||||||
|
||||||||||||||
### `PartialType` | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Missing section separator. Should have |
||||||||||||||
|
||||||||||||||
`PartialType` metaclass is used to extend your type but makes its all field | ||||||||||||||
optional. Consider you have different types for each operation on the same model | ||||||||||||||
such as `UserCreate`, `UserUpdate` and `UserQuery`. `UserQuery` should have id | ||||||||||||||
field but the other does not. All fields of `UserQuery` and `UserUpdate` might | ||||||||||||||
be optional. In this case instead of defining the same field for each type one | ||||||||||||||
can define in a single type and extend it. | ||||||||||||||
|
||||||||||||||
```py | ||||||||||||||
Comment on lines
+105
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. syntax: Missing required imports. The example uses
Suggested change
|
||||||||||||||
from strawberry.tools import PartialType | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
@strawberry.type | ||||||||||||||
class UserCreate: | ||||||||||||||
firstname: str | ||||||||||||||
lastname: str | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
@strawberry.type | ||||||||||||||
class UserUpdate(UserCreate, metaclass=PartialType): | ||||||||||||||
pass | ||||||||||||||
|
||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. syntax: Missing default value assignment. Based on the implementation, fields should have
Suggested change
|
||||||||||||||
|
||||||||||||||
@strawberry.type | ||||||||||||||
class UserQuery(UserCreate, metaclass=PartialType): | ||||||||||||||
id: Optional[strawberry.ID] | ||||||||||||||
``` | ||||||||||||||
Comment on lines
+123
to
+124
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Incomplete code block structure. The |
||||||||||||||
|
||||||||||||||
</CodeGrid> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,9 @@ | ||
from .create_type import create_type | ||
from .merge_types import merge_types | ||
from .partialtype import PartialType | ||
|
||
__all__ = [ | ||
"PartialType", | ||
"create_type", | ||
"merge_types", | ||
] |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,30 @@ | ||||||||||||||||||||||||||||||||||||
from abc import ABCMeta | ||||||||||||||||||||||||||||||||||||
from typing import Optional | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
import strawberry | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
class PartialType(ABCMeta): | ||||||||||||||||||||||||||||||||||||
def __new__(cls, name: str, bases: tuple, namespaces: dict, **kwargs): | ||||||||||||||||||||||||||||||||||||
mro = super().__new__(cls, name, bases, namespaces, **kwargs).mro() | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Creates class instance to call |
||||||||||||||||||||||||||||||||||||
annotations = namespaces.get("__annotations__", {}) | ||||||||||||||||||||||||||||||||||||
fields: list[str] = [] | ||||||||||||||||||||||||||||||||||||
for base in mro[:-1]: # the object class has no __annotations__ attr | ||||||||||||||||||||||||||||||||||||
for k, v in base.__annotations__.items(): | ||||||||||||||||||||||||||||||||||||
# To prevent overriding the higher attr annotation | ||||||||||||||||||||||||||||||||||||
if k not in annotations: | ||||||||||||||||||||||||||||||||||||
annotations[k] = v | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
fields.extend(field.name for field in base._type_definition.fields) | ||||||||||||||||||||||||||||||||||||
Comment on lines
+12
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (bug_risk): Accessing annotations directly may raise AttributeError for some base classes. Use getattr(base, "annotations", {}) to safely access annotations and prevent exceptions.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Assumes all bases have _type_definition.fields, which may not always be true. Use hasattr or a try/except block to avoid AttributeError when a base lacks _type_definition.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Accessing |
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
for field in annotations: | ||||||||||||||||||||||||||||||||||||
if not field.startswith("_"): | ||||||||||||||||||||||||||||||||||||
annotations[field] = Optional[annotations[field]] | ||||||||||||||||||||||||||||||||||||
Comment on lines
+20
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Wrapping all non-private fields in Optional may not preserve existing Optional types. Check if a field's annotation is already Optional before applying another Optional wrapper to avoid redundancy.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Already Optional fields will be double-wrapped as |
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
namespaces["__annotations__"] = annotations | ||||||||||||||||||||||||||||||||||||
klass = super().__new__(cls, name, bases, namespaces, **kwargs) | ||||||||||||||||||||||||||||||||||||
for field in fields: | ||||||||||||||||||||||||||||||||||||
if not hasattr(klass, field): | ||||||||||||||||||||||||||||||||||||
setattr(klass, field, strawberry.UNSET) | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
return klass |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import dataclasses | ||
|
||
import strawberry | ||
from strawberry.tools import PartialType | ||
from strawberry.type import StrawberryOptional | ||
|
||
|
||
def test_partialtype(): | ||
@strawberry.type | ||
class RoleRead: | ||
name: str | ||
description: str | ||
|
||
@strawberry.type | ||
class UserRead: | ||
firstname: str | ||
lastname: str | ||
role: RoleRead | ||
|
||
@strawberry.input | ||
class RoleInput(RoleRead): | ||
pass | ||
|
||
@strawberry.input | ||
class UserQuery(UserRead, metaclass=PartialType): | ||
role: RoleInput | ||
|
||
read_firstname, read_lastname, read_role = UserRead._type_definition.fields | ||
|
||
# user read type firstname field tests | ||
assert read_firstname.python_name == "firstname" | ||
assert read_firstname.graphql_name is None | ||
assert read_firstname.default is dataclasses.MISSING | ||
assert read_firstname.type is str | ||
|
||
# user read type lastname field tests | ||
assert read_lastname.python_name == "lastname" | ||
assert read_lastname.graphql_name is None | ||
assert read_lastname.default is dataclasses.MISSING | ||
assert read_lastname.type is str | ||
|
||
assert read_role.python_name == "role" | ||
assert read_role.graphql_name is None | ||
assert read_role.default is dataclasses.MISSING | ||
assert read_role.type is RoleRead | ||
|
||
query_firstname, query_lastname, query_role = UserQuery._type_definition.fields | ||
|
||
# user query type firstname field tests | ||
assert query_firstname.python_name == "firstname" | ||
assert query_firstname.graphql_name is None | ||
assert query_firstname.default is strawberry.UNSET | ||
assert isinstance(query_firstname.type, StrawberryOptional) | ||
assert query_firstname.type.of_type is str | ||
|
||
# user query type lastname field tests | ||
assert query_lastname.python_name == "lastname" | ||
assert query_lastname.graphql_name is None | ||
assert query_lastname.default is strawberry.UNSET | ||
assert isinstance(query_lastname.type, StrawberryOptional) | ||
assert query_lastname.type.of_type is str | ||
|
||
# user query type role field tests | ||
assert query_role.python_name == "role" | ||
assert query_role.graphql_name is None | ||
assert query_role.default is strawberry.UNSET | ||
assert isinstance(query_role.type, StrawberryOptional) | ||
assert query_role.type.of_type is RoleInput | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Missing newline at end of file |
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.
style: The description could be more specific about what 'dynamically' means here - that it automatically wraps inherited fields with Optional[] and sets defaults to strawberry.UNSET
Context Used: Context - When documenting features, ensure that explanations are clear and provide context on why certain functionalities are useful, especially when dealing with integrations like Pydantic. (link)