Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions RELEASED.md
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.
Copy link
Contributor

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)


```py
from strawberry.tools import PartialType


@strawberry.type
class UserCreate:
firstname: str
lastname: str


@strawberry.type
class UserUpdate(UserCreate, metaclass=PartialType):
pass
```
29 changes: 29 additions & 0 deletions docs/guides/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,33 @@ type ComboQuery {
}
```

### `PartialType`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Missing section separator. Should have --- before the PartialType section to match the formatting of other sections.


`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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: Missing required imports. The example uses Optional and strawberry.ID but doesn't import them.

Suggested change
```py
from typing import Optional
from strawberry.tools import PartialType
@strawberry.type

from strawberry.tools import PartialType


@strawberry.type
class UserCreate:
firstname: str
lastname: str


@strawberry.type
class UserUpdate(UserCreate, metaclass=PartialType):
pass

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: Missing default value assignment. Based on the implementation, fields should have = strawberry.UNSET when not explicitly provided.

Suggested change
id: Optional[strawberry.ID] = strawberry.UNSET


@strawberry.type
class UserQuery(UserCreate, metaclass=PartialType):
id: Optional[strawberry.ID]
```
Comment on lines +123 to +124
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Incomplete code block structure. The </CodeGrid> tag appears without proper context or corresponding GraphQL output example like the other sections.


</CodeGrid>
2 changes: 2 additions & 0 deletions strawberry/tools/__init__.py
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",
]
30 changes: 30 additions & 0 deletions strawberry/tools/partialtype.py
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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Creates class instance to call mro() but doesn't use the proper instance for subsequent operations

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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
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)
for base in mro[:-1]: # the object class has no __annotations__ attr
for k, v in getattr(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)

Copy link
Contributor

Choose a reason for hiding this comment

The 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
fields.extend(field.name for field in base._type_definition.fields)
if hasattr(base, "_type_definition") and hasattr(base._type_definition, "fields"):
fields.extend(field.name for field in base._type_definition.fields)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Accessing _type_definition may fail if base classes haven't been processed by strawberry decorators yet during MRO traversal


for field in annotations:
if not field.startswith("_"):
annotations[field] = Optional[annotations[field]]
Comment on lines +20 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The 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
for field in annotations:
if not field.startswith("_"):
annotations[field] = Optional[annotations[field]]
from typing import get_origin
for field, annotation in annotations.items():
if not field.startswith("_"):
# Check if annotation is already Optional
origin = get_origin(annotation)
if origin is not Optional:
annotations[field] = Optional[annotation]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Already Optional fields will be double-wrapped as Optional[Optional[T]], potentially causing type resolution issues


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
68 changes: 68 additions & 0 deletions tests/tools/test_partialtype.py
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Missing newline at end of file

Loading