From bf0651d0d5bc4b7d16bc53f85fc95804910ab959 Mon Sep 17 00:00:00 2001 From: Ethan Henderson Date: Wed, 7 Aug 2024 19:54:14 +0100 Subject: [PATCH 01/13] Add ability to use custom info objects schema-wide --- strawberry/schema/config.py | 6 ++++++ strawberry/schema/schema_converter.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/strawberry/schema/config.py b/strawberry/schema/config.py index 2c5a37eb53..75729e1735 100644 --- a/strawberry/schema/config.py +++ b/strawberry/schema/config.py @@ -3,6 +3,8 @@ from dataclasses import InitVar, dataclass, field from typing import Any, Callable +from strawberry.types.info import Info + from .name_converter import NameConverter @@ -13,6 +15,7 @@ class StrawberryConfig: default_resolver: Callable[[Any, str], object] = getattr relay_max_results: int = 100 disable_field_suggestions: bool = False + info_class: type[Info] = field(default_factory=lambda: Info) def __post_init__( self, @@ -21,5 +24,8 @@ def __post_init__( if auto_camel_case is not None: self.name_converter.auto_camel_case = auto_camel_case + if not issubclass(self.info_class, Info): + raise ValueError("`info_class` must be a subclass of strawberry.types.Info") + __all__ = ["StrawberryConfig"] diff --git a/strawberry/schema/schema_converter.py b/strawberry/schema/schema_converter.py index 9d19ad0345..1083b46f9b 100644 --- a/strawberry/schema/schema_converter.py +++ b/strawberry/schema/schema_converter.py @@ -64,7 +64,6 @@ ) from strawberry.types.enum import EnumDefinition from strawberry.types.field import UNRESOLVED -from strawberry.types.info import Info from strawberry.types.lazy_type import LazyType from strawberry.types.private import is_private from strawberry.types.scalar import ScalarWrapper @@ -90,6 +89,7 @@ from strawberry.schema_directive import StrawberrySchemaDirective from strawberry.types.enum import EnumValue from strawberry.types.field import StrawberryField + from strawberry.types.info import Info from strawberry.types.scalar import ScalarDefinition @@ -664,7 +664,7 @@ def _get_basic_result(_source: Any, *args: str, **kwargs: Any) -> Any: return _get_basic_result def _strawberry_info_from_graphql(info: GraphQLResolveInfo) -> Info: - return Info( + return self.config.info_class( _raw_info=info, _field=field, ) From 5c8c7270d5f1274aba5abc07276a2afec4be0f68 Mon Sep 17 00:00:00 2001 From: Ethan Henderson Date: Wed, 7 Aug 2024 21:11:26 +0100 Subject: [PATCH 02/13] Add RELEASE.md --- RELEASE.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000..e2d84438d5 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,43 @@ +Release type: minor + +You can now configure your schemas to provide a custom subclass of +`strawberry.types.Info` to your types and queries. + +```py +import strawberry +from strawberry.schema.config import StrawberryConfig + +from .models import ProductModel + + +class CustomInfo(strawberry.Info): + def is_selected(field: str) -> bool: + """Check if the field is selected on the top-level of the query.""" + return field in [sel.name for sel in info.selected_fields] + + +@strawberry.type +class Product: + id: strawberry.ID + orders: list[Order] + + +@strawberry.type +class Query: + @strawberry.field + def product(self, id: strawberry.ID, info: CustomInfo) -> Product: + kwargs = {"id": id} + + if info.is_selected("orders"): + # Do some expensive operation here that we wouldn't want to + # do if the field wasn't selected. + kwargs["orders"] = ... + + return Product(**kwargs) + + +schema = strawberry.Schema( + Query, + config=StrawberryConfig(info_class=CustomInfo), +) +``` From 4db4f1324c43cd13652b00b8d4d54d5be2a23027 Mon Sep 17 00:00:00 2001 From: Ethan Henderson Date: Wed, 7 Aug 2024 21:19:40 +0100 Subject: [PATCH 03/13] Improve RELEASE.md example --- RELEASE.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index e2d84438d5..1302f01391 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,11 +11,15 @@ from .models import ProductModel class CustomInfo(strawberry.Info): - def is_selected(field: str) -> bool: + def is_selected(self, field: str) -> bool: """Check if the field is selected on the top-level of the query.""" return field in [sel.name for sel in info.selected_fields] +@strawberry.type +class Order: ... + + @strawberry.type class Product: id: strawberry.ID From 3957ca9c1f37930bead99c0438e36d24d61834cc Mon Sep 17 00:00:00 2001 From: Ethan Henderson Date: Thu, 8 Aug 2024 12:15:06 +0100 Subject: [PATCH 04/13] Use 'Info' class directly instead of default factory for dataclass field --- strawberry/schema/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strawberry/schema/config.py b/strawberry/schema/config.py index 75729e1735..cf8d48ae9d 100644 --- a/strawberry/schema/config.py +++ b/strawberry/schema/config.py @@ -15,7 +15,7 @@ class StrawberryConfig: default_resolver: Callable[[Any, str], object] = getattr relay_max_results: int = 100 disable_field_suggestions: bool = False - info_class: type[Info] = field(default_factory=lambda: Info) + info_class: type[Info] = Info def __post_init__( self, From 06100d2d0ed02370d94c7e3e76ec55425245ed4c Mon Sep 17 00:00:00 2001 From: Ethan Henderson Date: Tue, 13 Aug 2024 10:08:18 +0100 Subject: [PATCH 05/13] Add no cover pragma + change to TypeError over ValueError --- strawberry/schema/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/strawberry/schema/config.py b/strawberry/schema/config.py index cf8d48ae9d..cf3c83e0a0 100644 --- a/strawberry/schema/config.py +++ b/strawberry/schema/config.py @@ -24,8 +24,8 @@ def __post_init__( if auto_camel_case is not None: self.name_converter.auto_camel_case = auto_camel_case - if not issubclass(self.info_class, Info): - raise ValueError("`info_class` must be a subclass of strawberry.types.Info") + if not issubclass(self.info_class, Info): # pragma: no cover + raise TypeError("`info_class` must be a subclass of strawberry.types.Info") __all__ = ["StrawberryConfig"] From 67d913fc96e7396a8ba08c76c2b2b3e5e8cb7157 Mon Sep 17 00:00:00 2001 From: Ethan Henderson Date: Tue, 13 Aug 2024 10:36:20 +0100 Subject: [PATCH 06/13] Update the RELEASE example --- RELEASE.md | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 1302f01391..2a3ac6f238 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,33 +11,37 @@ from .models import ProductModel class CustomInfo(strawberry.Info): - def is_selected(self, field: str) -> bool: - """Check if the field is selected on the top-level of the query.""" - return field in [sel.name for sel in info.selected_fields] + @property + def selected_group_id(self) -> int | None: + """Get the ID of the group you're logged in as.""" + return self.context["request"].headers.get("Group-ID") @strawberry.type -class Order: ... +class Group: + id: strawberry.ID + name: str @strawberry.type -class Product: +class User: id: strawberry.ID - orders: list[Order] + name: str + group: Group @strawberry.type class Query: @strawberry.field - def product(self, id: strawberry.ID, info: CustomInfo) -> Product: - kwargs = {"id": id} + def user(self, id: strawberry.ID, info: CustomInfo) -> Product: + kwargs = {"id": id, "name": ...} - if info.is_selected("orders"): - # Do some expensive operation here that we wouldn't want to - # do if the field wasn't selected. - kwargs["orders"] = ... + if info.selected_group_id is not None: + # Get information about the group you're a part of, if + # available. + kwargs["group"] = ... - return Product(**kwargs) + return User(**kwargs) schema = strawberry.Schema( From f16fdb0fdc0b7738a744e9614499dc5c147286fa Mon Sep 17 00:00:00 2001 From: Ethan Henderson Date: Tue, 13 Aug 2024 10:54:31 +0100 Subject: [PATCH 07/13] Update documentation to make note of the new 'info_class' config option --- docs/types/schema-configurations.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/types/schema-configurations.md b/docs/types/schema-configurations.md index 099153238e..6b4e5bf7ae 100644 --- a/docs/types/schema-configurations.md +++ b/docs/types/schema-configurations.md @@ -105,3 +105,23 @@ schema = strawberry.Schema( query=Query, config=StrawberryConfig(disable_field_suggestions=True) ) ``` + +### info_class + +By default Strawberry will create an object of type `strawberry.types.Info` when +the user defines `info: Info` as a parameter to a type or query. You can change +this behaviour by setting `info_class` to a subclass of `strawberry.types.Info`. + +This can be useful when you want to create a simpler interface for info- or +context-based properties, or if you wanted to attach additional properties to +the `Info` class. + +```python +class CustomInfo(Info): + @property + def response_headers(self) -> Headers: + return self.context["response"].headers + + +schema = strawberry.Schema(query=Query, info_class=CustomInfo) +``` From 941e3faca13af74483f0096276c839f72315fd06 Mon Sep 17 00:00:00 2001 From: Ethan Henderson <43846948+parafoxia@users.noreply.github.com> Date: Tue, 13 Aug 2024 23:41:10 +0100 Subject: [PATCH 08/13] Use 'strawberry.Info' instead of 'strawberry.types.Info' in info_class docs Co-authored-by: Jonathan Ehwald --- docs/types/schema-configurations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/types/schema-configurations.md b/docs/types/schema-configurations.md index 6b4e5bf7ae..2fc2217b75 100644 --- a/docs/types/schema-configurations.md +++ b/docs/types/schema-configurations.md @@ -108,9 +108,9 @@ schema = strawberry.Schema( ### info_class -By default Strawberry will create an object of type `strawberry.types.Info` when +By default Strawberry will create an object of type `strawberry.Info` when the user defines `info: Info` as a parameter to a type or query. You can change -this behaviour by setting `info_class` to a subclass of `strawberry.types.Info`. +this behaviour by setting `info_class` to a subclass of `strawberry.Info`. This can be useful when you want to create a simpler interface for info- or context-based properties, or if you wanted to attach additional properties to From 613f869f2f3ee186855a0dc0b2f791082e3cd20e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 22:41:25 +0000 Subject: [PATCH 09/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/types/schema-configurations.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/types/schema-configurations.md b/docs/types/schema-configurations.md index 2fc2217b75..5ad12c9c6c 100644 --- a/docs/types/schema-configurations.md +++ b/docs/types/schema-configurations.md @@ -108,9 +108,9 @@ schema = strawberry.Schema( ### info_class -By default Strawberry will create an object of type `strawberry.Info` when -the user defines `info: Info` as a parameter to a type or query. You can change -this behaviour by setting `info_class` to a subclass of `strawberry.Info`. +By default Strawberry will create an object of type `strawberry.Info` when the +user defines `info: Info` as a parameter to a type or query. You can change this +behaviour by setting `info_class` to a subclass of `strawberry.Info`. This can be useful when you want to create a simpler interface for info- or context-based properties, or if you wanted to attach additional properties to From 74cdd58fd5b78e62026d97f6a9a43cc834330e27 Mon Sep 17 00:00:00 2001 From: Ethan Henderson Date: Wed, 14 Aug 2024 09:45:56 +0100 Subject: [PATCH 10/13] Add __post_init__ tests for StrawberryConfig --- strawberry/schema/config.py | 4 ++-- tests/schema/test_config.py | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 tests/schema/test_config.py diff --git a/strawberry/schema/config.py b/strawberry/schema/config.py index cf3c83e0a0..230ae7dc10 100644 --- a/strawberry/schema/config.py +++ b/strawberry/schema/config.py @@ -24,8 +24,8 @@ def __post_init__( if auto_camel_case is not None: self.name_converter.auto_camel_case = auto_camel_case - if not issubclass(self.info_class, Info): # pragma: no cover - raise TypeError("`info_class` must be a subclass of strawberry.types.Info") + if not issubclass(self.info_class, Info): + raise TypeError("`info_class` must be a subclass of strawberry.Info") __all__ = ["StrawberryConfig"] diff --git a/tests/schema/test_config.py b/tests/schema/test_config.py new file mode 100644 index 0000000000..665b772391 --- /dev/null +++ b/tests/schema/test_config.py @@ -0,0 +1,39 @@ +import pytest + +from strawberry.schema.config import StrawberryConfig +from strawberry.types.info import Info + + +def test_config_post_init_auto_camel_case(): + config = StrawberryConfig(auto_camel_case=True) + + assert config.name_converter.auto_camel_case is True + + +def test_config_post_init_no_auto_camel_case(): + config = StrawberryConfig(auto_camel_case=False) + + assert config.name_converter.auto_camel_case is False + + +def test_config_post_init_info_class(): + class CustomInfo(Info): + test: str = "foo" + + config = StrawberryConfig(info_class=CustomInfo) + + assert config.info_class is CustomInfo + assert config.info_class.test == "foo" + + +def test_config_post_init_info_class_is_default(): + config = StrawberryConfig() + + assert config.info_class is Info + + +def test_config_post_init_info_class_is_not_subclass(): + with pytest.raises(TypeError) as exc_info: + StrawberryConfig(info_class=object) + + assert str(exc_info.value) == "`info_class` must be a subclass of strawberry.Info" From 0c68ae32ffb0decaaa297af41b64780cbce5cd7b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 Aug 2024 10:35:45 +0000 Subject: [PATCH 11/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/types/generics.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/types/generics.md b/docs/types/generics.md index 36fc44e5ee..9e4e424793 100644 --- a/docs/types/generics.md +++ b/docs/types/generics.md @@ -181,6 +181,7 @@ type Mutation { > `PostInput` are Input types. Providing `posts: CollectionInput[Post]` to > `add_posts` (i.e. using the non-input `Post` type) would have resulted in an > error: +> > ``` > PostCollectionInput fields cannot be resolved. Input field type must be a > GraphQL input type From abf5d0be590f5db26a8663d19f62aba8cc5bd2ac Mon Sep 17 00:00:00 2001 From: Ethan Henderson Date: Thu, 29 Aug 2024 15:10:18 +0100 Subject: [PATCH 12/13] Allow for the use of custom info classes within directives --- strawberry/extensions/directives.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/strawberry/extensions/directives.py b/strawberry/extensions/directives.py index b72923adda..82f9efe146 100644 --- a/strawberry/extensions/directives.py +++ b/strawberry/extensions/directives.py @@ -3,7 +3,6 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple from strawberry.extensions import SchemaExtension -from strawberry.types import Info from strawberry.types.nodes import convert_arguments from strawberry.utils.await_maybe import await_maybe @@ -81,7 +80,9 @@ def process_directive( field_name=info.field_name, type_name=info.parent_type.name, ) - arguments[info_parameter.name] = Info(_raw_info=info, _field=field) + arguments[info_parameter.name] = schema.config.info_class( + _raw_info=info, _field=field + ) if value_parameter: arguments[value_parameter.name] = value return strawberry_directive, arguments From e3e1ded51b94b10425ec4117f014cdd6dcad37a7 Mon Sep 17 00:00:00 2001 From: Ethan Henderson Date: Fri, 30 Aug 2024 10:43:56 +0100 Subject: [PATCH 13/13] Add testing to ensure custom info classes are included in directives and extensions --- .../extensions/test_field_extensions.py | 34 +++++++++++++++++++ tests/schema/test_directives.py | 34 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/tests/schema/extensions/test_field_extensions.py b/tests/schema/extensions/test_field_extensions.py index eea9e0e1ab..a59eaa9938 100644 --- a/tests/schema/extensions/test_field_extensions.py +++ b/tests/schema/extensions/test_field_extensions.py @@ -10,6 +10,7 @@ FieldExtension, SyncExtensionResolver, ) +from strawberry.schema.config import StrawberryConfig class UpperCaseExtension(FieldExtension): @@ -380,3 +381,36 @@ def string( }, "another_input": {}, } + + +def test_extension_has_custom_info_class(): + class CustomInfo(strawberry.Info): + test: str = "foo" + + class CustomExtension(FieldExtension): + def resolve( + self, + next_: Callable[..., Any], + source: Any, + info: CustomInfo, + **kwargs: Any, + ): + assert isinstance(info, CustomInfo) + # Explicitly check it's not Info. + assert strawberry.Info in type(info).__bases__ + assert info.test == "foo" + return next_(source, info, **kwargs) + + @strawberry.type + class Query: + @strawberry.field(extensions=[CustomExtension()]) + def string(self) -> str: + return "This is a test!!" + + schema = strawberry.Schema( + query=Query, config=StrawberryConfig(info_class=CustomInfo) + ) + query = "query { string }" + result = schema.execute_sync(query) + assert result.data, result.errors + assert result.data["string"] == "This is a test!!" diff --git a/tests/schema/test_directives.py b/tests/schema/test_directives.py index 9c82d483e0..3b31e97258 100644 --- a/tests/schema/test_directives.py +++ b/tests/schema/test_directives.py @@ -5,6 +5,7 @@ import pytest import strawberry +from strawberry import Info from strawberry.directive import DirectiveLocation, DirectiveValue from strawberry.extensions import SchemaExtension from strawberry.schema.config import StrawberryConfig @@ -654,3 +655,36 @@ def uppercase(value: str, input: DirectiveInput): ''' assert schema.as_str() == textwrap.dedent(expected_schema).strip() + + +@pytest.mark.asyncio +async def test_directive_with_custom_info_class() -> NoReturn: + @strawberry.type + class Query: + @strawberry.field + def greeting(self) -> str: + return "Hi" + + class CustomInfo(Info): + test: str = "foo" + + @strawberry.directive(locations=[DirectiveLocation.FIELD]) + def append_names(value: DirectiveValue[str], names: List[str], info: CustomInfo): + assert isinstance(names, list) + assert isinstance(info, CustomInfo) + assert Info in type(info).__bases__ # Explicitly check it's not Info. + assert info.test == "foo" + return f"{value} {', '.join(names)}" + + schema = strawberry.Schema( + query=Query, + directives=[append_names], + config=StrawberryConfig(info_class=CustomInfo), + ) + + result = await schema.execute( + 'query { greeting @appendNames(names: ["foo", "bar"])}' + ) + + assert result.errors is None + assert result.data["greeting"] == "Hi foo, bar"