Skip to content
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

from __future__ import annotations incompatible with strawberry.Private #1586

Closed
rjgildea opened this issue Jan 22, 2022 · 9 comments · Fixed by #1684
Closed

from __future__ import annotations incompatible with strawberry.Private #1586

rjgildea opened this issue Jan 22, 2022 · 9 comments · Fixed by #1684
Assignees
Labels
bug Something isn't working

Comments

@rjgildea
Copy link

rjgildea commented Jan 22, 2022

Using a minor variation from the getting started tutorial (addition of from __future__ import annotations and the use of strawberry.Private) results in a traceback. The code works as expected when commenting out the from __future__ import.

from __future__ import annotations

import typing
import strawberry

@strawberry.type
class Book:
    title: str
    author: str
    
    foo: strawberry.Private[int]


def get_books():
    return [
        Book(
            title='The Great Gatsby',
            author='F. Scott Fitzgerald',
            foo=1,
        ),
    ]


@strawberry.type
class Query:
    books: typing.List[Book] = strawberry.field(resolver=get_books)


schema = strawberry.Schema(query=Query)

Starting the built-in strawberry server results in the following traceback:

$ strawberry server schema
Traceback (most recent call last):
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/graphql/type/definition.py", line 735, in fields
    fields = resolve_thunk(self._fields)
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/graphql/type/definition.py", line 264, in resolve_thunk
    return thunk() if callable(thunk) else thunk
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/strawberry/schema/schema_converter.py", line 280, in get_graphql_fields
    graphql_fields[field_name] = self.from_field(field)
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/strawberry/schema/schema_converter.py", line 136, in from_field
    field_type = self.from_non_optional(field.type)
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/strawberry/schema/schema_converter.py", line 261, in from_non_optional
    of_type = self.from_type(type_)
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/strawberry/schema/schema_converter.py", line 469, in from_type
    raise TypeError(f"Unexpected type '{type_}'")
TypeError: Unexpected type 'typing.Annotated[int, <strawberry.private.StrawberryPrivate object at 0x7f72dcd2b820>]'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/path/to/project/virtualenv/bin/strawberry", line 8, in <module>
    sys.exit(run())
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/click/core.py", line 1128, in __call__
    return self.main(*args, **kwargs)
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/click/core.py", line 1053, in main
    rv = self.invoke(ctx)
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/click/core.py", line 1659, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/click/core.py", line 1395, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/click/core.py", line 754, in invoke
    return __callback(*args, **kwargs)
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/strawberry/cli/commands/server.py", line 46, in server
    schema_symbol = import_module_symbol(schema, default_symbol_name="schema")
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/strawberry/utils/importer.py", line 15, in import_module_symbol
    module = importlib.import_module(module_name)
  File "/dls_sw/apps/mamba/0.19.1/lib/python3.10/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 883, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/path/to/project/./schema.py", line 29, in <module>
    schema = strawberry.Schema(query=Query)
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/strawberry/schema/schema.py", line 84, in __init__
    self._schema = GraphQLSchema(
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/graphql/type/schema.py", line 208, in __init__
    collect_referenced_types(query)
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/graphql/type/schema.py", line 423, in collect_referenced_types
    collect_referenced_types(field.type)
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/graphql/type/schema.py", line 422, in collect_referenced_types
    for field in named_type.fields.values():
  File "/dls_sw/apps/mamba/0.19.1/lib/python3.10/functools.py", line 970, in __get__
    val = self.func(instance)
  File "/path/to/project/virtualenv/lib/python3.10/site-packages/graphql/type/definition.py", line 737, in fields
    raise TypeError(f"{self.name} fields cannot be resolved. {error}")
TypeError: Book fields cannot be resolved. Unexpected type 'typing.Annotated[int, <strawberry.private.StrawberryPrivate object at 0x7f72dcd2b820>]'

Python version:

$ python
Python 3.10.1 | packaged by conda-forge | (main, Dec 22 2021, 01:39:05) [GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.

Strawberry version:

$ pip show strawberry-graphql
Name: strawberry-graphql
Version: 0.94.0
Summary: A library for creating GraphQL APIs
Home-page: https://strawberry.rocks/
Author: Patrick Arminio
Author-email: patrick.arminio@gmail.com
License: MIT
Location: /path/to/project/virtualenv/lib/python3.10/site-packages
Requires: python-multipart, backports.cached-property, click, graphql-core, python-dateutil, pygments, typing_extensions, sentinel
Required-by: 

OS: RHEL7.9

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar
@thejaminator
Copy link
Contributor

thejaminator commented Jan 23, 2022

we call dataclasses.fields to get the field types. if we import annotations, the actual types aren't available to us. its just a string.

Field(name='foo',type='strawberry.Private[int]',  ...)

vs without the import

Field(name='foo',type=typing.Annotated[int, <strawberry.private.StrawberryPrivate object at 0x7fd249796370>] ,...)

still checking it out, i think we can call .resolve() to fix it?

# _get_fields
            type_resolved = StrawberryAnnotation(
                annotation=field_type,
                namespace=module.__dict__,
            ).resolve()
            if is_private(type_resolved):
                continue

@BryceBeagle BryceBeagle self-assigned this Jan 25, 2022
@BryceBeagle BryceBeagle added the bug Something isn't working label Jan 25, 2022
@BryceBeagle
Copy link
Member

Very easy to reproduce. This should be considered a bug.

import strawberry

@strawberry.type
class PrivateForward:
    not_seen: "strawberry.Private[int]"

strawberry.Schema(query=PrivateForward)

I'm investigating this now

@cpontvieux-systra
Copy link

annotations from future implies to resolve types later, so it’s perfectly normal that the return type is a string.

This behavior was scheduled to be included by default with Python 3.10 but was postponed for this reason: multiple libraries are not compatible with it.

To make this work, one should add code to resolve the type from string: https://www.python.org/dev/peps/pep-0563/#resolving-type-hints-at-runtime

@cpontvieux-systra
Copy link

from typing import get_type_hints
class A:
    ...
class B:
    def toto() -> A:
        ...
print(B.toto.__annotations__)
print(get_type_hints(B.toto))

Returns:

{'return': <class '__main__.A'>}
{'return': <class '__main__.A'>}
from __future__ import annotations
from typing import get_type_hints
class A:
    ...
class B:
    def toto() -> A:
        ...
print(B.toto.__annotations__)
print(get_type_hints(B.toto))

Returns:

{'return': 'A'}
{'return': <class '__main__.A'>}

@patrick91
Copy link
Member

To make this work, one should add code to resolve the type from string: https://www.python.org/dev/peps/pep-0563/#resolving-type-hints-at-runtime

We do this already :) unfortunately we do check for Private before that happens, so we need to refactor the check to make it work properly :)

@skilkis
Copy link
Contributor

skilkis commented Feb 11, 2022

Examining this problem I came to the following conclusion: If one creates a schema where the Private field type refers to a class defined after the decorated class, then runtime type resolution during construction of the StrawberryField would fail. This is because the referred class does not yet exist in the module namespace. For example:

from __future__ import annotations

import strawberry

@strawberry.type
class Foo:
    bar: strawberry.Private[Bar]

class Bar:
   pass

In the above, when _get_fields is called by strawberry.type the class Bar does not yet exist in the module namespace.
To counteract this problem, I think the check for Private fields should to be delayed to when the actual schema is built. A potential solution implementing this approach would be to modify GraphQLCoreConverter.from_object() with a call to is_private like so:

    def from_object(self, object_type: TypeDefinition) -> GraphQLObjectType:
        ...
        def get_graphql_fields() -> Dict[str, GraphQLField]:
            graphql_fields = {}

            for field in object_type.fields:
                if not is_private(field.type):
                    field_name = self.config.name_converter.from_field(field)

                    graphql_fields[field_name] = self.from_field(field)

            return graphql_fields

To avoid duplication, the checks for private fields in _get_fields could be removed I think. Please, let me know if I missed something @patrick91 @BryceBeagle. If you like this solution, I would be happy to write up a PR 😄

@thejaminator
Copy link
Contributor

thanks @skilkis ! @BryceBeagle is now working on the solution, he'll update this soon

skilkis added a commit to skilkis/strawberry that referenced this issue Feb 25, 2022
This commit Closes strawberry-graphql#1586 by checking for private fields at schema
conversion time. As per [PEP-563], in the future Python annotations will
no longer be evaluated at definition time. As reported by Issue strawberry-graphql#1586,
this means that `strawberry.Private` is incompatible with postponed
evaluation as the check for a private field requires an evaluated
type-hint annotation to work.

By checking for private fields at schema conversion time, it is
guaranteed that all fields that should be included in the schema are
resolvable using an eval. This ensures that the current approach for
defining private fields can be left intact.

The current filtering for fields annotated with `strawberry.Private` in
`types.type_resolver.py` are left intact to not needlessly instantiate
`StrawberryField` objects when `strawberry.Private` is resolvable.

Summary of Changes:
- Added check for private fields at schema evaluation time
- Added test to check that postponed evaluation with
  `strawberry.Private` functions correctly
- Reduced code duplication in `schema_converter.py`

[PEP-563]: https://www.python.org/dev/peps/pep-0563/
@skilkis
Copy link
Contributor

skilkis commented Feb 25, 2022

@BryceBeagle I don't want to step on your toes here. I really needed this fix for my current work and wanted to contribute to resolving this issue. Feel free to reject the PR if you want to implement a different solution! Cheers 🍺

@BryceBeagle
Copy link
Member

BryceBeagle commented Feb 27, 2022

Hey @skilkis, thanks for working on this, and sorry for the delay. Don't fear about stepping on my toes; I hadn't found a working solution to the problem yet. I'll take a look at your PR tonight 👍

Edit: tomorrow...

skilkis added a commit to skilkis/strawberry that referenced this issue Mar 7, 2022
This commit Closes strawberry-graphql#1586 by checking for private fields at schema
conversion time. As per [PEP-563], in the future Python annotations will
no longer be evaluated at definition time. As reported by Issue strawberry-graphql#1586,
this means that `strawberry.Private` is incompatible with postponed
evaluation as the check for a private field requires an evaluated
type-hint annotation to work.

By checking for private fields at schema conversion time, it is
guaranteed that all fields that should be included in the schema are
resolvable using an eval. This ensures that the current approach for
defining private fields can be left intact.

The current filtering for fields annotated with `strawberry.Private` in
`types.type_resolver.py` are left intact to not needlessly instantiate
`StrawberryField` objects when `strawberry.Private` is resolvable.

Summary of Changes:
- Added check for private fields at schema evaluation time
- Added test to check that postponed evaluation with
  `strawberry.Private` functions correctly
- Reduced code duplication in `schema_converter.py`

[PEP-563]: https://www.python.org/dev/peps/pep-0563/
patrick91 pushed a commit to skilkis/strawberry that referenced this issue Mar 8, 2022
This commit Closes strawberry-graphql#1586 by checking for private fields at schema
conversion time. As per [PEP-563], in the future Python annotations will
no longer be evaluated at definition time. As reported by Issue strawberry-graphql#1586,
this means that `strawberry.Private` is incompatible with postponed
evaluation as the check for a private field requires an evaluated
type-hint annotation to work.

By checking for private fields at schema conversion time, it is
guaranteed that all fields that should be included in the schema are
resolvable using an eval. This ensures that the current approach for
defining private fields can be left intact.

The current filtering for fields annotated with `strawberry.Private` in
`types.type_resolver.py` are left intact to not needlessly instantiate
`StrawberryField` objects when `strawberry.Private` is resolvable.

Summary of Changes:
- Added check for private fields at schema evaluation time
- Added test to check that postponed evaluation with
  `strawberry.Private` functions correctly
- Reduced code duplication in `schema_converter.py`

[PEP-563]: https://www.python.org/dev/peps/pep-0563/
patrick91 added a commit that referenced this issue Mar 8, 2022
* Make `strawberry.Private` compatible with PEP-563

This commit Closes #1586 by checking for private fields at schema
conversion time. As per [PEP-563], in the future Python annotations will
no longer be evaluated at definition time. As reported by Issue #1586,
this means that `strawberry.Private` is incompatible with postponed
evaluation as the check for a private field requires an evaluated
type-hint annotation to work.

By checking for private fields at schema conversion time, it is
guaranteed that all fields that should be included in the schema are
resolvable using an eval. This ensures that the current approach for
defining private fields can be left intact.

The current filtering for fields annotated with `strawberry.Private` in
`types.type_resolver.py` are left intact to not needlessly instantiate
`StrawberryField` objects when `strawberry.Private` is resolvable.

Summary of Changes:
- Added check for private fields at schema evaluation time
- Added test to check that postponed evaluation with
  `strawberry.Private` functions correctly
- Reduced code duplication in `schema_converter.py`

[PEP-563]: https://www.python.org/dev/peps/pep-0563/

* Add RELEASE.md

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add docstring to `_get_thunk_mapping`

* Improve testing of private fields

This commit mainly addresses a limitation of PEP-563 wherby annotations
tagetting classes defined outside of module-level scope (i.e. a
function) cannot be resolved. As a result, even at schema-conversion
time, checking `is_private` is not guaranteed to evaluate to a valid
type. As a result, a test is added to ensure that a `strawberry.Private`
annotated field that forward-references a locally-defined class does not
appear in the generated Schema. Although this is a failing test for now
it demonstrates the issue to prevent a potential security vulnerability.

Summary of Changes:
- Check that the `notSeen` private field does not exist in the schema
  and that querying it returns no data.
- Add failing test to show issue of classes defined outside module-scope
- Move import of dataclass to the top of the file
- Correct typo in the word `accessible`

* Prevent unresolved types from entering the schema

This commit changes `StrawberryField.type` to return a new sentinel
`UNRESOLVED` instead of `None` to make sure that unresolvable types such
as those defined within a local scope of a function do not appear in the
schema as a `Void` field. With the new behavior a `TypeError` will be
raised during schema conversion if any types are still unresolved. Most
importantly, this new behavior prevents a security vulnerability where
`Strawberry.Private` annotated fields would previously appear in the
schema as a `Void` field; thereby, becomming executible and potentially
leaking sensitive data in the resultant error message.

Summary of Changes:
- Return a new sentinel `UNRESOLVED` from `StrawberryField.type` instead
  of `None`
- Update `GraphQLCoreConverter._get_thunk_mapping` to raise a
  `TypeError` if any unresolved types exist during schema conversion
- Update failing `test_private_field_defined_outside_module_scope` to
  check if the correct `TypeError` is raised and to further ensure
  that if no error is raised, that the `Strawberry.Private` annotated
  field does not enter the schema.

* Make `strawberry.Private` compatible with PEP-563

This commit Closes #1586 by checking for private fields at schema
conversion time. As per [PEP-563], in the future Python annotations will
no longer be evaluated at definition time. As reported by Issue #1586,
this means that `strawberry.Private` is incompatible with postponed
evaluation as the check for a private field requires an evaluated
type-hint annotation to work.

By checking for private fields at schema conversion time, it is
guaranteed that all fields that should be included in the schema are
resolvable using an eval. This ensures that the current approach for
defining private fields can be left intact.

The current filtering for fields annotated with `strawberry.Private` in
`types.type_resolver.py` are left intact to not needlessly instantiate
`StrawberryField` objects when `strawberry.Private` is resolvable.

Summary of Changes:
- Added check for private fields at schema evaluation time
- Added test to check that postponed evaluation with
  `strawberry.Private` functions correctly
- Reduced code duplication in `schema_converter.py`

[PEP-563]: https://www.python.org/dev/peps/pep-0563/

* Create custom exception for unresolved field types

* Remove dependency on sentinel package

* Fix sentinel

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Patrick Arminio <patrick.arminio@gmail.com>
msobas pushed a commit to msobas/strawberry that referenced this issue Apr 10, 2022
…#1684)

* Make `strawberry.Private` compatible with PEP-563

This commit Closes strawberry-graphql#1586 by checking for private fields at schema
conversion time. As per [PEP-563], in the future Python annotations will
no longer be evaluated at definition time. As reported by Issue strawberry-graphql#1586,
this means that `strawberry.Private` is incompatible with postponed
evaluation as the check for a private field requires an evaluated
type-hint annotation to work.

By checking for private fields at schema conversion time, it is
guaranteed that all fields that should be included in the schema are
resolvable using an eval. This ensures that the current approach for
defining private fields can be left intact.

The current filtering for fields annotated with `strawberry.Private` in
`types.type_resolver.py` are left intact to not needlessly instantiate
`StrawberryField` objects when `strawberry.Private` is resolvable.

Summary of Changes:
- Added check for private fields at schema evaluation time
- Added test to check that postponed evaluation with
  `strawberry.Private` functions correctly
- Reduced code duplication in `schema_converter.py`

[PEP-563]: https://www.python.org/dev/peps/pep-0563/

* Add RELEASE.md

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add docstring to `_get_thunk_mapping`

* Improve testing of private fields

This commit mainly addresses a limitation of PEP-563 wherby annotations
tagetting classes defined outside of module-level scope (i.e. a
function) cannot be resolved. As a result, even at schema-conversion
time, checking `is_private` is not guaranteed to evaluate to a valid
type. As a result, a test is added to ensure that a `strawberry.Private`
annotated field that forward-references a locally-defined class does not
appear in the generated Schema. Although this is a failing test for now
it demonstrates the issue to prevent a potential security vulnerability.

Summary of Changes:
- Check that the `notSeen` private field does not exist in the schema
  and that querying it returns no data.
- Add failing test to show issue of classes defined outside module-scope
- Move import of dataclass to the top of the file
- Correct typo in the word `accessible`

* Prevent unresolved types from entering the schema

This commit changes `StrawberryField.type` to return a new sentinel
`UNRESOLVED` instead of `None` to make sure that unresolvable types such
as those defined within a local scope of a function do not appear in the
schema as a `Void` field. With the new behavior a `TypeError` will be
raised during schema conversion if any types are still unresolved. Most
importantly, this new behavior prevents a security vulnerability where
`Strawberry.Private` annotated fields would previously appear in the
schema as a `Void` field; thereby, becomming executible and potentially
leaking sensitive data in the resultant error message.

Summary of Changes:
- Return a new sentinel `UNRESOLVED` from `StrawberryField.type` instead
  of `None`
- Update `GraphQLCoreConverter._get_thunk_mapping` to raise a
  `TypeError` if any unresolved types exist during schema conversion
- Update failing `test_private_field_defined_outside_module_scope` to
  check if the correct `TypeError` is raised and to further ensure
  that if no error is raised, that the `Strawberry.Private` annotated
  field does not enter the schema.

* Make `strawberry.Private` compatible with PEP-563

This commit Closes strawberry-graphql#1586 by checking for private fields at schema
conversion time. As per [PEP-563], in the future Python annotations will
no longer be evaluated at definition time. As reported by Issue strawberry-graphql#1586,
this means that `strawberry.Private` is incompatible with postponed
evaluation as the check for a private field requires an evaluated
type-hint annotation to work.

By checking for private fields at schema conversion time, it is
guaranteed that all fields that should be included in the schema are
resolvable using an eval. This ensures that the current approach for
defining private fields can be left intact.

The current filtering for fields annotated with `strawberry.Private` in
`types.type_resolver.py` are left intact to not needlessly instantiate
`StrawberryField` objects when `strawberry.Private` is resolvable.

Summary of Changes:
- Added check for private fields at schema evaluation time
- Added test to check that postponed evaluation with
  `strawberry.Private` functions correctly
- Reduced code duplication in `schema_converter.py`

[PEP-563]: https://www.python.org/dev/peps/pep-0563/

* Create custom exception for unresolved field types

* Remove dependency on sentinel package

* Fix sentinel

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Patrick Arminio <patrick.arminio@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants