diff --git a/docs/conf.py b/docs/conf.py index 873531ae6..c5d3bb3b8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,6 +42,7 @@ "sphinx.ext.coverage", "sphinx.ext.viewcode", "sphinx.ext.napoleon", + "sphinx_autodoc_typehints", ] if not on_rtd: extensions += ["sphinx.ext.githubpages"] diff --git a/docs/execution/queryvalidation.rst b/docs/execution/queryvalidation.rst index 02e29a350..dad71cc19 100644 --- a/docs/execution/queryvalidation.rst +++ b/docs/execution/queryvalidation.rst @@ -82,6 +82,49 @@ Here is how you would disable introspection for your schema. ) +Enforcing validation on every query +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The example above validates a single already-parsed query. To apply a rule to +*every* request, run :func:`graphql.validate` before executing and return the +errors instead of the result when validation fails: + +.. code:: python + + from graphql import graphql_sync, parse, validate + from graphene import ObjectType, Schema, String + from graphene.validation import DisableIntrospection + + + class MyQuery(ObjectType): + name = String(required=True) + + + schema = Schema(query=MyQuery) + + validation_rules = (DisableIntrospection,) + + + def execute(query_string): + document = parse(query_string) + + # Validation runs against the same schema that will execute the query. + errors = validate(schema.graphql_schema, document, rules=validation_rules) + if errors: + return {"errors": [error.formatted for error in errors]} + + return graphql_sync(schema.graphql_schema, query_string).formatted + +An introspection query is now rejected:: + + >>> execute("{ __schema { types { name } } }") + {'errors': [{'message': "Cannot query '__schema': introspection is disabled.", + 'locations': [{'line': 1, 'column': 3}]}]} + +The same ``validation_rules`` tuple is where any other +:doc:`custom validator ` is registered. + + Implementing custom validators ------------------------------ All custom query validators should extend the `ValidationRule `_ diff --git a/docs/requirements.txt b/docs/requirements.txt index dee009c70..5a310a69c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,6 @@ # Required library Sphinx==6.1.3 sphinx-autobuild==2021.3.14 +sphinx-autodoc-typehints==2.2.0 # Docs template http://graphene-python.org/sphinx_graphene_theme.zip diff --git a/docs/types/list-and-nonnull.rst b/docs/types/list-and-nonnull.rst index a127a9d24..e50a5724a 100644 --- a/docs/types/list-and-nonnull.rst +++ b/docs/types/list-and-nonnull.rst @@ -49,6 +49,50 @@ Lists work in a similar way: We can use a type modifier to mark a type as a It works the same for arguments, where the validation step will expect a list for that value. +A list can hold any type, including other ``ObjectType`` classes. The resolver +returns a plain Python iterable, and Graphene maps each item through the +declared type: + +.. code:: python + + import graphene + + class Person(graphene.ObjectType): + name = graphene.String() + + class Query(graphene.ObjectType): + people = graphene.List(Person) + + def resolve_people(root, info): + return [Person(name="Ada"), Person(name="Grace")] + +This produces the type ``[Person]``, and the resolver's return value is +serialized item by item:: + + {"data": {"people": [{"name": "Ada"}, {"name": "Grace"}]}} + +The parent object needs to know nothing about the child type beyond the class +itself; ``Person`` only has to be defined before it is referenced. + +Self-referencing lists +---------------------- + +A type that returns a list of itself would otherwise need to be defined before +it exists. Wrap the class in ``lambda`` so the reference is resolved lazily: + +.. code:: python + + import graphene + + class User(graphene.ObjectType): + name = graphene.String() + friends = graphene.List(lambda: User) + + def resolve_friends(root, info): + return [User(name=f"{root.name}'s friend")] + +The same lazy form is what makes circular references between two types work. + NonNull Lists ------------- @@ -69,3 +113,14 @@ The above results in the type definition: type Character { appearsIn: [String!] } + +Combining ``NonNull`` with ``required=True`` makes the list itself non-null as +well as its items: + +.. code:: python + + tags = graphene.List(graphene.NonNull(graphene.String), required=True) + +which gives ``[String!]!`` — the list is always present and never contains +``null``. + diff --git a/graphene/types/field.py b/graphene/types/field.py index 06ba84872..11e457b79 100644 --- a/graphene/types/field.py +++ b/graphene/types/field.py @@ -1,6 +1,7 @@ import inspect from collections.abc import Mapping from functools import partial +from typing import Any, Callable, Optional, Union from .argument import Argument, to_arguments from .mountedtype import MountedType @@ -13,7 +14,7 @@ base_type = type -def source_resolver(source, root, info, **args): +def source_resolver(source: str, root: Any, info: Any, **args: Any) -> Any: resolved = default_resolver(source, None, root, info, **args) if inspect.isfunction(resolved) or inspect.ismethod(resolved): return resolved() @@ -41,43 +42,43 @@ class Person(ObjectType): last_name = graphene.Field(String, description='Surname') # explicitly mounted as Field args: - type (class for a graphene.UnmountedType): Must be a class (not an instance) of an + type_: Must be a class (not an instance) of an unmounted graphene type (ex. scalar or object) which is used for the type of this field in the GraphQL schema. You can provide a dotted module import path (string) to the class instead of the class itself (e.g. to avoid circular import issues). - args (optional, Dict[str, graphene.Argument]): Arguments that can be input to the field. + args: Arguments that can be input to the field. Prefer to use ``**extra_args``, unless you use an argument name that clashes with one of the Field arguments presented here (see :ref:`example`). - resolver (optional, Callable): A function to get the value for a Field from the parent + resolver: A function to get the value for a Field from the parent value object. If not set, the default resolver method for the schema is used. - source (optional, str): attribute name to resolve for this field from the parent value + source: attribute name to resolve for this field from the parent value object. Alternative to resolver (cannot set both source and resolver). - deprecation_reason (optional, str): Setting this value indicates that the field is + deprecation_reason: Setting this value indicates that the field is depreciated and may provide instruction or reason on how for clients to proceed. - required (optional, bool): indicates this field as not null in the graphql schema. Same behavior as + required: indicates this field as not null in the graphql schema. Same behavior as graphene.NonNull. Default False. - name (optional, str): the name of the GraphQL field (must be unique in a type). Defaults to attribute + name: the name of the GraphQL field (must be unique in a type). Defaults to attribute name. - description (optional, str): the description of the GraphQL field in the schema. - default_value (optional, Any): Default value to resolve if none set from schema. - **extra_args (optional, Dict[str, Union[graphene.Argument, graphene.UnmountedType]): any + description: the description of the GraphQL field in the schema. + default_value: Default value to resolve if none set from schema. + **extra_args: any additional arguments to mount on the field. """ def __init__( self, - type_, - args=None, - resolver=None, - source=None, - deprecation_reason=None, - name=None, - description=None, - required=False, - _creation_counter=None, - default_value=None, - **extra_args, - ): + type_: Any, + args: Optional[Mapping[str, Any]] = None, + resolver: Optional[Callable[..., Any]] = None, + source: Optional[Union[str, Argument, UnmountedType]] = None, + deprecation_reason: Optional[str] = None, + name: Optional[Union[str, Argument, UnmountedType]] = None, + description: Optional[str] = None, + required: bool = False, + _creation_counter: Optional[int] = None, + default_value: Any = None, + **extra_args: Any, + ) -> None: super(Field, self).__init__(_creation_counter=_creation_counter) assert not args or isinstance( args, Mapping @@ -113,12 +114,12 @@ def __init__( self.default_value = default_value @property - def type(self): + def type(self) -> Any: return get_type(self._type) - get_resolver = None + get_resolver: Optional[Callable[..., Any]] = None - def wrap_resolve(self, parent_resolver): + def wrap_resolve(self, parent_resolver: Callable[..., Any]) -> Callable[..., Any]: """ Wraps a function resolver, using the ObjectType resolve_{FIELD_NAME} (parent_resolver) if the Field definition has no resolver. @@ -131,7 +132,9 @@ def wrap_resolve(self, parent_resolver): return self.resolver or parent_resolver - def wrap_subscribe(self, parent_subscribe): + def wrap_subscribe( + self, parent_subscribe: Callable[..., Any] + ) -> Callable[..., Any]: """ Wraps a function subscribe, using the ObjectType subscribe_{FIELD_NAME} (parent_subscribe) if the Field definition has no subscribe. diff --git a/graphene/types/structures.py b/graphene/types/structures.py index a6763978e..c7c1d3055 100644 --- a/graphene/types/structures.py +++ b/graphene/types/structures.py @@ -1,3 +1,5 @@ +from typing import Any + from .unmountedtype import UnmountedType from .utils import get_type @@ -8,7 +10,7 @@ class Structure(UnmountedType): wraps a main type with certain structure. """ - def __init__(self, of_type, *args, **kwargs): + def __init__(self, of_type: Any, *args: Any, **kwargs: Any) -> None: super(Structure, self).__init__(*args, **kwargs) if not isinstance(of_type, Structure) and isinstance(of_type, UnmountedType): cls_name = type(self).__name__ @@ -20,10 +22,10 @@ def __init__(self, of_type, *args, **kwargs): self._of_type = of_type @property - def of_type(self): + def of_type(self) -> Any: return get_type(self._of_type) - def get_type(self): + def get_type(self) -> "Structure": """ This function is called when the unmounted type (List or NonNull instance) is mounted (as a Field, InputField or Argument) @@ -48,10 +50,10 @@ class List(Structure): field_name = List(String, description="There will be many values") """ - def __str__(self): + def __str__(self) -> str: return f"[{self.of_type}]" - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: return isinstance(other, List) and ( self.of_type == other.of_type and self.args == other.args @@ -82,16 +84,16 @@ class NonNull(Structure): """ - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super(NonNull, self).__init__(*args, **kwargs) assert not isinstance( self._of_type, NonNull ), f"Can only create NonNull of a Nullable GraphQLType but got: {self._of_type}." - def __str__(self): + def __str__(self) -> str: return f"{self.of_type}!" - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: return isinstance(other, NonNull) and ( self.of_type == other.of_type and self.args == other.args diff --git a/graphene/types/utils.py b/graphene/types/utils.py index 1976448aa..0070cb9f2 100644 --- a/graphene/types/utils.py +++ b/graphene/types/utils.py @@ -1,12 +1,13 @@ import inspect from functools import partial +from typing import Any, Dict, Optional, Type from ..utils.module_loading import import_string from .mountedtype import MountedType from .unmountedtype import UnmountedType -def get_field_as(value, _as=None): +def get_field_as(value: Any, _as: Optional[Type[MountedType]] = None) -> Any: """ Get type mounted """ @@ -18,7 +19,11 @@ def get_field_as(value, _as=None): return _as.mounted(value) -def yank_fields_from_attrs(attrs, _as=None, sort=True): +def yank_fields_from_attrs( + attrs: Dict[str, Any], + _as: Optional[Type[MountedType]] = None, + sort: bool = True, +) -> Dict[str, Any]: """ Extract all the fields in given attributes (dict) and return them ordered @@ -35,7 +40,7 @@ def yank_fields_from_attrs(attrs, _as=None, sort=True): return dict(fields_with_names) -def get_type(_type): +def get_type(_type: Any) -> Any: if isinstance(_type, str): return import_string(_type) if inspect.isfunction(_type) or isinstance(_type, partial): @@ -43,7 +48,7 @@ def get_type(_type): return _type -def get_underlying_type(_type): +def get_underlying_type(_type: Any) -> Any: """Get the underlying type even if it is wrapped in structures like NonNull""" while hasattr(_type, "of_type"): _type = _type.of_type