diff --git a/.travis.yml b/.travis.yml index b313bff4a..541c0c0ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,19 +6,19 @@ python: - 3.5 - pypy before_install: - - | - if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then - export PYENV_ROOT="$HOME/.pyenv" - if [ -f "$PYENV_ROOT/bin/pyenv" ]; then - cd "$PYENV_ROOT" && git pull - else - rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT" - fi - export PYPY_VERSION="4.0.1" - "$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION" - virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION" - source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate" - fi +- | + if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then + export PYENV_ROOT="$HOME/.pyenv" + if [ -f "$PYENV_ROOT/bin/pyenv" ]; then + cd "$PYENV_ROOT" && git pull + else + rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT" + fi + export PYPY_VERSION="4.0.1" + "$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION" + virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION" + source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate" + fi install: - | if [ "$TEST_TYPE" = build ]; then @@ -52,3 +52,10 @@ matrix: include: - python: '2.7' env: TEST_TYPE=lint +deploy: + provider: pypi + user: syrusakbary + on: + tags: true + password: + secure: LHOp9DvYR+70vj4YVY8+JRNCKUOfYZREEUY3+4lMUpY7Zy5QwDfgEMXG64ybREH9dFldpUqVXRj53eeU3spfudSfh8NHkgqW7qihez2AhSnRc4dK6ooNfB+kLcSoJ4nUFGxdYImABc4V1hJvflGaUkTwDNYVxJF938bPaO797IvSbuI86llwqkvuK2Vegv9q/fy9sVGaF9VZIs4JgXwR5AyDR7FBArl+S84vWww4vTFD33hoE88VR4QvFY3/71BwRtQrnCMm7AOm31P9u29yi3bpzQpiOR2rHsgrsYdm597QzFKVxYwsmf9uAx2bpbSPy2WibunLePIvOFwm8xcfwnz4/J4ONBc5PSFmUytTWpzEnxb0bfUNLuYloIS24V6OZ8BfAhiYZ1AwySeJCQDM4Vk1V8IF6trTtyx5EW/uV9jsHCZ3LFsAD7UnFRTosIgN3SAK3ZWCEk5oF2IvjecsolEfkRXB3q9EjMkkuXRUeFDH2lWJLgNE27BzY6myvZVzPmfwZUsPBlPD/6w+WLSp97Rjgr9zS3T1d4ddqFM4ZYu04f2i7a/UUQqG+itzzuX5DWLPvzuNt37JB45mB9IsvxPyXZ6SkAcLl48NGyKok1f3vQnvphkfkl4lni29woKhaau8xlsuEDrcwOoeAsVcZXiItg+l+z2SlIwM0A06EvQ= diff --git a/README.md b/README.md index c6f924e3a..d9a483803 100644 --- a/README.md +++ b/README.md @@ -83,3 +83,21 @@ After developing, the full test suite can be evaluated by running: ```sh python setup.py test # Use --pytest-args="-v -s" for verbose mode ``` + + +### Documentation + +The documentation is generated using the excellent [Sphinx](http://www.sphinx-doc.org/) and a custom theme. + +The documentation dependencies are installed by running: + +```sh +cd docs +pip install -r requirements.txt +``` + +Then to produce a HTML version of the documentation: + +```sh +make html +``` diff --git a/README.rst b/README.rst index 72a6a0209..39da6b9d9 100644 --- a/README.rst +++ b/README.rst @@ -1,37 +1,38 @@ -Please read `UPGRADE-v1.0.md`_ to learn how to upgrade to Graphene ``1.0``. +Please read `UPGRADE-v1.0.md `__ to learn how to +upgrade to Graphene ``1.0``. -------------- -|Graphene Logo| `Graphene`_ |Build Status| |PyPI version| |Coverage Status| -=========================================================================== +|Graphene Logo| `Graphene `__ |Build Status| |PyPI version| |Coverage Status| +========================================================================================================= -`Graphene`_ is a Python library for building GraphQL schemas/types fast -and easily. +`Graphene `__ is a Python library for +building GraphQL schemas/types fast and easily. - **Easy to use:** Graphene helps you use GraphQL in Python without effort. - **Relay:** Graphene has builtin support for Relay - **Data agnostic:** Graphene supports any kind of data source: SQL - (Django, SQLAlchemy), NoSQL, custom Python objects, etc. We believe that - by providing a complete API you could plug Graphene anywhere your - data lives and make your data available through GraphQL. + (Django, SQLAlchemy), NoSQL, custom Python objects, etc. We believe + that by providing a complete API you could plug Graphene anywhere + your data lives and make your data available through GraphQL. Integrations ------------ Graphene has multiple integrations with different frameworks: -+---------------------+-------------------------------------+ -| integration | Package | -+=====================+=====================================+ -| Django | `graphene-django`_ | -+---------------------+-------------------------------------+ -| SQLAlchemy | `graphene-sqlalchemy`_ | -+---------------------+-------------------------------------+ -| Google App Engine | `graphene-gae`_ | -+---------------------+-------------------------------------+ -| Peewee | *In progress* (`Tracking Issue`_) | -+---------------------+-------------------------------------+ ++---------------------+----------------------------------------------------------------------------------------------+ +| integration | Package | ++=====================+==============================================================================================+ +| Django | `graphene-django `__ | ++---------------------+----------------------------------------------------------------------------------------------+ +| SQLAlchemy | `graphene-sqlalchemy `__ | ++---------------------+----------------------------------------------------------------------------------------------+ +| Google App Engine | `graphene-gae `__ | ++---------------------+----------------------------------------------------------------------------------------------+ +| Peewee | *In progress* (`Tracking Issue `__) | ++---------------------+----------------------------------------------------------------------------------------------+ Installation ------------ @@ -45,7 +46,8 @@ For instaling graphene, just run this command in your shell 1.0 Upgrade Guide ----------------- -Please read `UPGRADE-v1.0.md`_ to learn how to upgrade. +Please read `UPGRADE-v1.0.md `__ to learn how to +upgrade. Examples -------- @@ -74,10 +76,11 @@ Then Querying ``graphene.Schema`` is as simple as: result = schema.execute(query) If you want to learn even more, you can also check the following -`examples`_: +`examples `__: -- **Basic Schema**: `Starwars example`_ -- **Relay Schema**: `Starwars Relay example`_ +- **Basic Schema**: `Starwars example `__ +- **Relay Schema**: `Starwars Relay + example `__ Contributing ------------ @@ -94,15 +97,24 @@ After developing, the full test suite can be evaluated by running: python setup.py test # Use --pytest-args="-v -s" for verbose mode -.. _UPGRADE-v1.0.md: /UPGRADE-v1.0.md -.. _Graphene: http://graphene-python.org -.. _graphene-django: https://github.com/graphql-python/graphene-django/ -.. _graphene-sqlalchemy: https://github.com/graphql-python/graphene-sqlalchemy/ -.. _graphene-gae: https://github.com/graphql-python/graphene-gae/ -.. _Tracking Issue: https://github.com/graphql-python/graphene/issues/289 -.. _examples: examples/ -.. _Starwars example: examples/starwars -.. _Starwars Relay example: examples/starwars_relay +Documentation +~~~~~~~~~~~~~ + +The documentation is generated using the excellent +`Sphinx `__ and a custom theme. + +The documentation dependencies are installed by running: + +.. code:: sh + + cd docs + pip install -r requirements.txt + +Then to produce a HTML version of the documentation: + +.. code:: sh + + make html .. |Graphene Logo| image:: http://graphene-python.org/favicon.png .. |Build Status| image:: https://travis-ci.org/graphql-python/graphene.svg?branch=master diff --git a/docs/Makefile b/docs/Makefile index 7da67c312..2973acec9 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -223,3 +223,7 @@ dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." + +.PHONY: livehtml +livehtml: + sphinx-autobuild -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html diff --git a/docs/execution/index.rst b/docs/execution/index.rst new file mode 100644 index 000000000..849832d43 --- /dev/null +++ b/docs/execution/index.rst @@ -0,0 +1,40 @@ +========= +Execution +========= + +For executing a query a schema, you can directly call the ``execute`` method on it. + + +.. code:: python + + schema = graphene.Schema(...) + result = schema.execute('{ name }') + +``result`` represents he result of execution. ``result.data`` is the result of executing the query, ``result.errors`` is ``None`` if no errors occurred, and is a non-empty list if an error occurred. + + +Context +_______ + +You can pass context to a query via ``context_value``. + + +.. code:: python + + class Query(graphene.ObjectType): + name = graphene.String() + + def resolve_name(self, args, context, info): + return context.get('name') + + schema = graphene.Schema(Query) + result = schema.execute('{ name }', context_value={'name': 'Syrus'}) + + +Middleware +__________ + +.. toctree:: + :maxdepth: 1 + + middleware diff --git a/docs/execution/middleware.rst b/docs/execution/middleware.rst new file mode 100644 index 000000000..54eb55dba --- /dev/null +++ b/docs/execution/middleware.rst @@ -0,0 +1,44 @@ +Middleware +========== + +You can use ``middleware`` to affect the evaluation of fields in your schema. + +A middleware is any object that responds to ``resolve(*args, next_middleware)``. + +Inside that method, it should either: + +- Send ``resolve`` to the next middleware to continue the evaluation; or +- Return a value to end the evaluation early. + + +Resolve arguments +----------------- + +Middlewares ``resolve`` is invoked with several arguments: + +- ``next`` represents the execution chain. Call ``next`` to continue evalution. +- ``root`` is the root value object passed throughout the query. +- ``args`` is the hash of arguments passed to the field. +- ``context`` is the context object passed throughout the query. +- ``info`` is the resolver info. + + +Example +------- + +This middleware only continues evaluation if the ``field_name`` is not ``'user'`` + +.. code:: python + + class AuthorizationMiddleware(object): + def resolve(self, next, root, args, context, info): + if info.field_name == 'user': + return None + return next(root, args, context, info) + + +And then execute it with: + +.. code:: python + + result = schema.execute('THE QUERY', middleware=[AuthorizationMiddleware()]) diff --git a/docs/index.rst b/docs/index.rst index d7aa9dcae..675051b38 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,6 +8,7 @@ Contents: quickstart types/index + execution/index relay/index Integrations diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 90382fcbe..c72259c15 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,6 +1,12 @@ Getting started =============== +What is GraphQL? +---------------- + +For an introduction to GraphQL and an overview of its concepts, please refer +to `the official introduction `. + Let’s build a basic GraphQL schema from scratch. Requirements diff --git a/docs/types/interfaces.rst b/docs/types/interfaces.rst index 25eefefec..8c049a246 100644 --- a/docs/types/interfaces.rst +++ b/docs/types/interfaces.rst @@ -23,14 +23,14 @@ and ``Droid`` are two implementations of that interface. name = graphene.String() # Human is a Character implementation - class Human(ObjectType): + class Human(graphene.ObjectType): class Meta: interfaces = (Character, ) born_in = graphene.String() # Droid is a Character implementation - class Droid(Character): + class Droid(graphene.ObjectType): class Meta: interfaces = (Character, ) diff --git a/docs/types/mutations.rst b/docs/types/mutations.rst index 3f39658a0..e447dc348 100644 --- a/docs/types/mutations.rst +++ b/docs/types/mutations.rst @@ -76,3 +76,69 @@ We should receive: "ok": true } } + +InputFields and InputObjectTypes +---------------------- +InputFields are used in mutations to allow nested input data for mutations + +To use an InputField you define an InputObjectType that specifies the structure of your input data + + + + +.. code:: python + + import graphene + + class PersonInput(graphene.InputObjectType): + name = graphene.String() + age = graphene.Int() + + class CreatePerson(graphene.Mutation): + class Input: + person_data = graphene.InputField(PersonInput) + + person = graphene.Field(lambda: Person) + + def mutate(self, args, context, info): + p_data = args.get('person_data') + + name = p_data.get('name') + age = p_data.get('age') + + person = Person(name=name, age=age) + return CreatePerson(person=person) + + +Note that **name** and **age** are part of **person_data** now + +Using the above mutation your new query would look like this: + +.. code:: graphql + + mutation myFirstMutation { + createPerson(personData: {name:"Peter", age: 24}) { + person { + name, + age + } + } + } + +InputObjectTypes can also be fields of InputObjectTypes allowing you to have +as complex of input data as you need + +.. code:: python + + import graphene + + class LatLngInput(graphene.InputObjectType): + lat = graphene.Float() + lng = graphene.Float() + + #A location has a latlng associated to it + class LocationInput(graphene.InputObjectType): + name = graphene.String() + latlng = graphene.InputField(LatLngInputType) + + diff --git a/docs/types/scalars.rst b/docs/types/scalars.rst index 382d9f200..ae1958847 100644 --- a/docs/types/scalars.rst +++ b/docs/types/scalars.rst @@ -9,9 +9,10 @@ Graphene defines the following base Scalar Types: - ``graphene.Boolean`` - ``graphene.ID`` -Graphene also provides custom scalars for Dates and JSON: +Graphene also provides custom scalars for Dates, Times, and JSON: - ``graphene.types.datetime.DateTime`` +- ``graphene.types.datetime.Time`` - ``graphene.types.json.JSONString`` @@ -24,8 +25,8 @@ The following is an example for creating a DateTime scalar: .. code:: python import datetime - from graphene.core.classtypes import Scalar - from graphql.core.language import ast + from graphene.types import Scalar + from graphql.language import ast class DateTime(Scalar): '''DateTime Scalar Description''' @@ -57,14 +58,18 @@ Scalars mounted in a ``ObjectType``, ``Interface`` or ``Mutation`` act as # Is equivalent to: class Person(graphene.ObjectType): - name = graphene.Field(graphene.String()) + name = graphene.Field(graphene.String) +**Note:** when using the ``Field`` constructor directly, pass the type and +not an instance. + Types mounted in a ``Field`` act as ``Argument``\ s. + .. code:: python - graphene.Field(graphene.String(), to=graphene.String()) + graphene.Field(graphene.String, to=graphene.String()) # Is equivalent to: - graphene.Field(graphene.String(), to=graphene.Argument(graphene.String())) + graphene.Field(graphene.String, to=graphene.Argument(graphene.String())) diff --git a/examples/context_example.py b/examples/context_example.py new file mode 100644 index 000000000..058e578b1 --- /dev/null +++ b/examples/context_example.py @@ -0,0 +1,39 @@ +import graphene + + +class User(graphene.ObjectType): + id = graphene.ID() + name = graphene.String() + + +class Query(graphene.ObjectType): + me = graphene.Field(User) + + def resolve_me(self, args, context, info): + return context['user'] + +schema = graphene.Schema(query=Query) +query = ''' + query something{ + me { + id + name + } + } +''' + + +def test_query(): + result = schema.execute(query, context_value={'user': User(id='1', name='Syrus')}) + assert not result.errors + assert result.data == { + 'me': { + 'id': '1', + 'name': 'Syrus', + } + } + + +if __name__ == '__main__': + result = schema.execute(query, context_value={'user': User(id='X', name='Console')}) + print(result.data['me']) diff --git a/examples/starwars_relay/tests/test_objectidentification.py b/examples/starwars_relay/tests/test_objectidentification.py index 873944215..327d5b0c1 100644 --- a/examples/starwars_relay/tests/test_objectidentification.py +++ b/examples/starwars_relay/tests/test_objectidentification.py @@ -56,7 +56,7 @@ def test_str_schema(): type ShipConnection { pageInfo: PageInfo! - edges: [ShipEdge] + edges: [ShipEdge]! } type ShipEdge { diff --git a/graphene/__init__.py b/graphene/__init__.py index 7a01ed162..7a680edc7 100644 --- a/graphene/__init__.py +++ b/graphene/__init__.py @@ -10,7 +10,7 @@ __SETUP__ = False -VERSION = (1, 0, 2, 'final', 0) +VERSION = (1, 1, 3, 'final', 0) __version__ = get_version(VERSION) diff --git a/graphene/pyutils/enum.py b/graphene/pyutils/enum.py index 8c09a6ad7..928d0be25 100644 --- a/graphene/pyutils/enum.py +++ b/graphene/pyutils/enum.py @@ -71,18 +71,18 @@ def _is_descriptor(obj): def _is_dunder(name): """Returns True if a __dunder__ name, False otherwise.""" - return (name[:2] == name[-2:] == '__' and + return (len(name) > 4 and + name[:2] == name[-2:] == '__' and name[2:3] != '_' and - name[-3:-2] != '_' and - len(name) > 4) + name[-3:-2] != '_') def _is_sunder(name): """Returns True if a _sunder_ name, False otherwise.""" - return (name[0] == name[-1] == '_' and + return (len(name) > 2 and + name[0] == name[-1] == '_' and name[1:2] != '_' and - name[-2:-1] != '_' and - len(name) > 2) + name[-2:-1] != '_') def _make_class_unpicklable(cls): diff --git a/graphene/pyutils/tests/__init__.py b/graphene/pyutils/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/graphene/pyutils/tests/test_enum.py b/graphene/pyutils/tests/test_enum.py new file mode 100644 index 000000000..bf15a6202 --- /dev/null +++ b/graphene/pyutils/tests/test_enum.py @@ -0,0 +1,41 @@ +from ..enum import _is_dunder, _is_sunder + + +def test__is_dunder(): + dunder_names = [ + '__i__', + '__test__', + ] + non_dunder_names = [ + 'test', + '__test', + '_test', + '_test_', + 'test__', + '', + ] + + for name in dunder_names: + assert _is_dunder(name) is True + + for name in non_dunder_names: + assert _is_dunder(name) is False + +def test__is_sunder(): + sunder_names = [ + '_i_', + '_test_', + ] + + non_sunder_names = [ + '__i__', + '_i__', + '__i_', + '', + ] + + for name in sunder_names: + assert _is_sunder(name) is True + + for name in non_sunder_names: + assert _is_sunder(name) is False diff --git a/graphene/relay/connection.py b/graphene/relay/connection.py index e63478e51..f85b675fa 100644 --- a/graphene/relay/connection.py +++ b/graphene/relay/connection.py @@ -5,6 +5,7 @@ import six from graphql_relay import connection_from_list +from promise import is_thenable, promisify from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union) @@ -81,7 +82,7 @@ class EdgeBase(AbstractType): class ConnectionBase(AbstractType): page_info = Field(PageInfo, name='pageInfo', required=True) - edges = List(edge) + edges = NonNull(List(edge)) bases = (ConnectionBase, ) + bases attrs = dict(attrs, _meta=options, Edge=edge) @@ -114,32 +115,41 @@ def type(self): connection_type = type assert issubclass(connection_type, Connection), ( '{} type have to be a subclass of Connection. Received "{}".' - ).format(str(self), connection_type) + ).format(self.__class__.__name__, connection_type) return connection_type @classmethod - def connection_resolver(cls, resolver, connection, root, args, context, info): - resolved = resolver(root, args, context, info) - - if isinstance(resolved, connection): + def resolve_connection(cls, connection_type, args, resolved): + if isinstance(resolved, connection_type): return resolved assert isinstance(resolved, Iterable), ( 'Resolved value from the connection field have to be iterable or instance of {}. ' 'Received "{}"' - ).format(connection, resolved) + ).format(connection_type, resolved) connection = connection_from_list( resolved, args, - connection_type=connection, - edge_type=connection.Edge, + connection_type=connection_type, + edge_type=connection_type.Edge, pageinfo_type=PageInfo ) connection.iterable = resolved return connection + @classmethod + def connection_resolver(cls, resolver, connection_type, root, args, context, info): + resolved = resolver(root, args, context, info) + + on_resolve = partial(cls.resolve_connection, connection_type, args) + if is_thenable(resolved): + return promisify(resolved).then(on_resolve) + + return on_resolve(resolved) + def get_resolver(self, parent_resolver): resolver = super(IterableConnectionField, self).get_resolver(parent_resolver) return partial(self.connection_resolver, resolver, self.type) + ConnectionField = IterableConnectionField diff --git a/graphene/relay/node.py b/graphene/relay/node.py index 85d692767..3db30e93e 100644 --- a/graphene/relay/node.py +++ b/graphene/relay/node.py @@ -12,9 +12,9 @@ def is_node(objecttype): ''' Check if the given objecttype has Node as an interface ''' - assert issubclass(objecttype, ObjectType), ( - 'Only ObjectTypes can have a Node interface.' - ) + if not issubclass(objecttype, ObjectType): + return False + for i in objecttype._meta.interfaces: if issubclass(i, Node): return True @@ -35,24 +35,26 @@ class Meta: class GlobalID(Field): - def __init__(self, node, *args, **kwargs): - super(GlobalID, self).__init__(ID, *args, **kwargs) - self.node = node + def __init__(self, node=None, parent_type=None, required=True, *args, **kwargs): + super(GlobalID, self).__init__(ID, required=required, *args, **kwargs) + self.node = node or Node + self.parent_type_name = parent_type._meta.name if parent_type else None @staticmethod - def id_resolver(parent_resolver, node, root, args, context, info): - id = parent_resolver(root, args, context, info) - return node.to_global_id(info.parent_type.name, id) # root._meta.name + def id_resolver(parent_resolver, node, root, args, context, info, parent_type_name=None): + type_id = parent_resolver(root, args, context, info) + parent_type_name = parent_type_name or info.parent_type.name + return node.to_global_id(parent_type_name, type_id) # root._meta.name def get_resolver(self, parent_resolver): - return partial(self.id_resolver, parent_resolver, self.node) + return partial(self.id_resolver, parent_resolver, self.node, parent_type_name=self.parent_type_name) class NodeMeta(InterfaceMeta): def __new__(cls, name, bases, attrs): cls = InterfaceMeta.__new__(cls, name, bases, attrs) - cls._meta.fields['id'] = GlobalID(cls, required=True, description='The ID of the object.') + cls._meta.fields['id'] = GlobalID(cls, description='The ID of the object.') return cls diff --git a/graphene/relay/tests/test_connection.py b/graphene/relay/tests/test_connection.py index 18d890c14..87f937aed 100644 --- a/graphene/relay/tests/test_connection.py +++ b/graphene/relay/tests/test_connection.py @@ -28,8 +28,9 @@ class Edge: pageinfo_field = fields['page_info'] assert isinstance(edge_field, Field) - assert isinstance(edge_field.type, List) - assert edge_field.type.of_type == MyObjectConnection.Edge + assert isinstance(edge_field.type, NonNull) + assert isinstance(edge_field.type.of_type, List) + assert edge_field.type.of_type.of_type == MyObjectConnection.Edge assert isinstance(pageinfo_field, Field) assert isinstance(pageinfo_field.type, NonNull) diff --git a/graphene/relay/tests/test_connection_query.py b/graphene/relay/tests/test_connection_query.py index cc1f12ce7..068081d6a 100644 --- a/graphene/relay/tests/test_connection_query.py +++ b/graphene/relay/tests/test_connection_query.py @@ -1,6 +1,7 @@ from collections import OrderedDict from graphql_relay.utils import base64 +from promise import Promise from ...types import ObjectType, Schema, String from ..connection import ConnectionField, PageInfo @@ -20,12 +21,16 @@ class Meta: class Query(ObjectType): letters = ConnectionField(Letter) connection_letters = ConnectionField(Letter) + promise_letters = ConnectionField(Letter) node = Node.Field() def resolve_letters(self, args, context, info): return list(letters.values()) + def resolve_promise_letters(self, args, context, info): + return Promise.resolve(list(letters.values())) + def resolve_connection_letters(self, args, context, info): return Letter.Connection( page_info=PageInfo( @@ -228,3 +233,38 @@ def test_connection_type_nodes(): } } } + + +def test_connection_promise(): + result = schema.execute(''' + { + promiseLetters(first:1) { + edges { + node { + id + letter + } + } + pageInfo { + hasPreviousPage + hasNextPage + } + } + } + ''') + + assert not result.errors + assert result.data == { + 'promiseLetters': { + 'edges': [{ + 'node': { + 'id': 'TGV0dGVyOjA=', + 'letter': 'A', + }, + }], + 'pageInfo': { + 'hasPreviousPage': False, + 'hasNextPage': True, + } + } + } diff --git a/graphene/relay/tests/test_global_id.py b/graphene/relay/tests/test_global_id.py new file mode 100644 index 000000000..b0a6c5cba --- /dev/null +++ b/graphene/relay/tests/test_global_id.py @@ -0,0 +1,60 @@ +from graphql_relay import to_global_id + +from ..node import Node, GlobalID +from ...types import NonNull, ID, ObjectType, String +from ...types.definitions import GrapheneObjectType + + +class CustomNode(Node): + + class Meta: + name = 'Node' + + +class User(ObjectType): + + class Meta: + interfaces = [CustomNode] + name = String() + + +class Info(object): + + def __init__(self, parent_type): + self.parent_type = GrapheneObjectType( + graphene_type=parent_type, + name=parent_type._meta.name, + description=parent_type._meta.description, + fields=None, + is_type_of=parent_type.is_type_of, + interfaces=None + ) + + +def test_global_id_defaults_to_required_and_node(): + gid = GlobalID() + assert isinstance(gid.type, NonNull) + assert gid.type.of_type == ID + assert gid.node == Node + + +def test_global_id_allows_overriding_of_node_and_required(): + gid = GlobalID(node=CustomNode, required=False) + assert gid.type == ID + assert gid.node == CustomNode + + +def test_global_id_defaults_to_info_parent_type(): + my_id = '1' + gid = GlobalID() + id_resolver = gid.get_resolver(lambda *_: my_id) + my_global_id = id_resolver(None, None, None, Info(User)) + assert my_global_id == to_global_id(User._meta.name, my_id) + + +def test_global_id_allows_setting_customer_parent_type(): + my_id = '1' + gid = GlobalID(parent_type=User) + id_resolver = gid.get_resolver(lambda *_: my_id) + my_global_id = id_resolver(None, None, None, None) + assert my_global_id == to_global_id(User._meta.name, my_id) diff --git a/graphene/tests/__init__.py b/graphene/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/graphene/tests/issues/__init__.py b/graphene/tests/issues/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/graphene/tests/issues/test_313.py b/graphene/tests/issues/test_313.py new file mode 100644 index 000000000..bed872900 --- /dev/null +++ b/graphene/tests/issues/test_313.py @@ -0,0 +1,52 @@ +# https://github.com/graphql-python/graphene/issues/313 + +import graphene +from graphene import resolve_only_args + +class Success(graphene.ObjectType): + yeah = graphene.String() + + +class Error(graphene.ObjectType): + message = graphene.String() + + +class CreatePostResult(graphene.Union): + class Meta: + types = [Success, Error] + + +class CreatePost(graphene.Mutation): + class Input: + text = graphene.String(required=True) + + result = graphene.Field(CreatePostResult) + + @resolve_only_args + def mutate(self, text): + result = Success(yeah='yeah') + + return CreatePost(result=result) + + +class Mutations(graphene.ObjectType): + create_post = CreatePost.Field() + +# tests.py + +def test_create_post(): + query_string = ''' + mutation { + createPost(text: "Try this out") { + result { + __typename + } + } + } + ''' + + schema = graphene.Schema(mutation=Mutations) + result = schema.execute(query_string) + + assert not result.errors + assert result.data['createPost']['result']['__typename'] == 'Success' \ No newline at end of file diff --git a/graphene/tests/issues/test_356.py b/graphene/tests/issues/test_356.py new file mode 100644 index 000000000..605594e1a --- /dev/null +++ b/graphene/tests/issues/test_356.py @@ -0,0 +1,24 @@ +# https://github.com/graphql-python/graphene/issues/356 + +import pytest +import graphene +from graphene import relay + +class SomeTypeOne(graphene.ObjectType): + pass + +class SomeTypeTwo(graphene.ObjectType): + pass + +class MyUnion(graphene.Union): + class Meta: + types = (SomeTypeOne, SomeTypeTwo) + +def test_issue(): + with pytest.raises(Exception) as exc_info: + class Query(graphene.ObjectType): + things = relay.ConnectionField(MyUnion) + + schema = graphene.Schema(query=Query) + + assert str(exc_info.value) == 'IterableConnectionField type have to be a subclass of Connection. Received "MyUnion".' diff --git a/graphene/types/argument.py b/graphene/types/argument.py index 49784b105..8d486f1b4 100644 --- a/graphene/types/argument.py +++ b/graphene/types/argument.py @@ -1,11 +1,12 @@ from collections import OrderedDict from itertools import chain -from ..utils.orderedtype import OrderedType +from .mountedtype import MountedType from .structures import NonNull +from .dynamic import Dynamic -class Argument(OrderedType): +class Argument(MountedType): def __init__(self, type, default_value=None, description=None, name=None, required=False, _creation_counter=None): super(Argument, self).__init__(_creation_counter=_creation_counter) @@ -27,14 +28,33 @@ def __eq__(self, other): ) -def to_arguments(args, extra_args): +def to_arguments(args, extra_args=None): from .unmountedtype import UnmountedType - extra_args = sorted(extra_args.items(), key=lambda f: f[1]) + from .field import Field + from .inputfield import InputField + if extra_args: + extra_args = sorted(extra_args.items(), key=lambda f: f[1]) + else: + extra_args = [] iter_arguments = chain(args.items(), extra_args) arguments = OrderedDict() for default_name, arg in iter_arguments: + if isinstance(arg, Dynamic): + arg = arg.get_type() + if arg is None: + # If the Dynamic type returned None + # then we skip the Argument + continue + if isinstance(arg, UnmountedType): - arg = arg.Argument() + arg = Argument.mount(arg) + + if isinstance(arg, (InputField, Field)): + raise ValueError('Expected {} to be Argument, but received {}. Try using Argument({}).'.format( + default_name, + type(arg).__name__, + arg.type + )) if not isinstance(arg, Argument): raise ValueError('Unknown argument "{}".'.format(default_name)) diff --git a/graphene/types/datetime.py b/graphene/types/datetime.py index 3dfbbb97d..0a0e33435 100644 --- a/graphene/types/datetime.py +++ b/graphene/types/datetime.py @@ -29,11 +29,37 @@ def serialize(dt): ) return dt.isoformat() - @staticmethod - def parse_literal(node): + @classmethod + def parse_literal(cls, node): if isinstance(node, ast.StringValue): - return iso8601.parse_date(node.value) + return cls.parse_value(node.value) @staticmethod def parse_value(value): return iso8601.parse_date(value) + + +class Time(Scalar): + ''' + The `Time` scalar type represents a Time value as + specified by + [iso8601](https://en.wikipedia.org/wiki/ISO_8601). + ''' + epoch_date = '1970-01-01' + + @staticmethod + def serialize(time): + assert isinstance(time, datetime.time), ( + 'Received not compatible time "{}"'.format(repr(time)) + ) + return time.isoformat() + + @classmethod + def parse_literal(cls, node): + if isinstance(node, ast.StringValue): + return cls.parse_value(node.value) + + @classmethod + def parse_value(cls, value): + dt = iso8601.parse_date('{}T{}'.format(cls.epoch_date, value)) + return datetime.time(dt.hour, dt.minute, dt.second, dt.microsecond, dt.tzinfo) diff --git a/graphene/types/dynamic.py b/graphene/types/dynamic.py index b7e2aaa10..c5aada20b 100644 --- a/graphene/types/dynamic.py +++ b/graphene/types/dynamic.py @@ -1,9 +1,9 @@ import inspect -from ..utils.orderedtype import OrderedType +from .mountedtype import MountedType -class Dynamic(OrderedType): +class Dynamic(MountedType): ''' A Dynamic Type let us get the type in runtime when we generate the schema. So we can have lazy fields. diff --git a/graphene/types/enum.py b/graphene/types/enum.py index 7895f258a..3bff137c6 100644 --- a/graphene/types/enum.py +++ b/graphene/types/enum.py @@ -58,5 +58,10 @@ class Enum(six.with_metaclass(EnumTypeMeta, UnmountedType)): kind of type, often integers. ''' - def get_type(self): - return type(self) + @classmethod + def get_type(cls): + ''' + This function is called when the unmounted type (Enum instance) + is mounted (as a Field, InputField or Argument) + ''' + return cls diff --git a/graphene/types/field.py b/graphene/types/field.py index 3b9347c0a..ab1d503da 100644 --- a/graphene/types/field.py +++ b/graphene/types/field.py @@ -2,8 +2,8 @@ from collections import Mapping, OrderedDict from functools import partial -from ..utils.orderedtype import OrderedType from .argument import Argument, to_arguments +from .mountedtype import MountedType from .structures import NonNull from .unmountedtype import UnmountedType @@ -18,7 +18,7 @@ def source_resolver(source, root, args, context, info): return resolved -class Field(OrderedType): +class Field(MountedType): def __init__(self, type, args=None, resolver=None, source=None, deprecation_reason=None, name=None, description=None, @@ -60,7 +60,7 @@ def __init__(self, type, args=None, resolver=None, source=None, @property def type(self): - if inspect.isfunction(self._type): + if inspect.isfunction(self._type) or type(self._type) is partial: return self._type() return self._type diff --git a/graphene/types/inputfield.py b/graphene/types/inputfield.py index 09ccf5a88..8bf5973e5 100644 --- a/graphene/types/inputfield.py +++ b/graphene/types/inputfield.py @@ -1,8 +1,8 @@ -from ..utils.orderedtype import OrderedType +from .mountedtype import MountedType from .structures import NonNull -class InputField(OrderedType): +class InputField(MountedType): def __init__(self, type, name=None, default_value=None, deprecation_reason=None, description=None, diff --git a/graphene/types/inputobjecttype.py b/graphene/types/inputobjecttype.py index 2dc91cd5b..cbc13f956 100644 --- a/graphene/types/inputobjecttype.py +++ b/graphene/types/inputobjecttype.py @@ -50,4 +50,8 @@ class InputObjectType(six.with_metaclass(InputObjectTypeMeta, UnmountedType)): @classmethod def get_type(cls): + ''' + This function is called when the unmounted type (InputObjectType instance) + is mounted (as a Field, InputField or Argument) + ''' return cls diff --git a/graphene/types/mountedtype.py b/graphene/types/mountedtype.py new file mode 100644 index 000000000..e2d0b7a3e --- /dev/null +++ b/graphene/types/mountedtype.py @@ -0,0 +1,21 @@ +from ..utils.orderedtype import OrderedType +from .unmountedtype import UnmountedType + + +class MountedType(OrderedType): + + @classmethod + def mount(cls, unmounted): # noqa: N802 + ''' + Mount the UnmountedType instance + ''' + assert isinstance(unmounted, UnmountedType), ( + '{} can\'t mount {}' + ).format(cls.__name__, repr(unmounted)) + + return cls( + unmounted.get_type(), + *unmounted.args, + _creation_counter=unmounted.creation_counter, + **unmounted.kwargs + ) diff --git a/graphene/types/options.py b/graphene/types/options.py index 50e982c76..0002db68e 100644 --- a/graphene/types/options.py +++ b/graphene/types/options.py @@ -19,18 +19,18 @@ def __init__(self, meta=None, **defaults): for attr_name, value in defaults.items(): if attr_name in meta_attrs: value = meta_attrs.pop(attr_name) - elif hasattr(meta, attr_name): - value = getattr(meta, attr_name) setattr(self, attr_name, value) - # If meta_attrs is not empty, it implicit means + # If meta_attrs is not empty, it implicitly means # it received invalid attributes if meta_attrs: raise TypeError( "Invalid attributes: {}".format( - ','.join(meta_attrs.keys()) + ', '.join(sorted(meta_attrs.keys())) ) ) def __repr__(self): - return ''.format(props(self)) + options_props = props(self) + props_as_attrs = ' '.join(['{}={}'.format(key, value) for key, value in options_props.items()]) + return ''.format(props_as_attrs) diff --git a/graphene/types/scalars.py b/graphene/types/scalars.py index d6060d33d..6f07c91c8 100644 --- a/graphene/types/scalars.py +++ b/graphene/types/scalars.py @@ -43,8 +43,13 @@ class Scalar(six.with_metaclass(ScalarTypeMeta, UnmountedType)): @classmethod def get_type(cls): + ''' + This function is called when the unmounted type (Scalar instance) + is mounted (as a Field, InputField or Argument) + ''' return cls + # As per the GraphQL Spec, Integers are only treated as valid when a valid # 32-bit signed integer, providing the broadest support across platforms. # diff --git a/graphene/types/schema.py b/graphene/types/schema.py index a57e6c374..b95490ca2 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -6,6 +6,7 @@ from graphql.utils.introspection_query import introspection_query from graphql.utils.schema_printer import print_schema +from .definitions import GrapheneGraphQLType from .typemap import TypeMap, is_graphene_type @@ -46,6 +47,20 @@ def get_mutation_type(self): def get_subscription_type(self): return self.get_graphql_type(self._subscription) + def __getattr__(self, type_name): + ''' + This function let the developer select a type in a given schema + by accessing its attrs. + + Example: using schema.Query for accessing the "Query" type in the Schema + ''' + _type = super(Schema, self).get_type(type_name) + if _type is None: + raise AttributeError('Type "{}" not found in the Schema'.format(type_name)) + if isinstance(_type, GrapheneGraphQLType): + return _type.graphene_type + return _type + def get_graphql_type(self, _type): if not _type: return _type @@ -61,9 +76,6 @@ def get_graphql_type(self, _type): def execute(self, *args, **kwargs): return graphql(self, *args, **kwargs) - def register(self, object_type): - self.types.append(object_type) - def introspect(self): return self.execute(introspection_query).data diff --git a/graphene/types/structures.py b/graphene/types/structures.py index 6c9c0e7ed..1ecfa83d2 100644 --- a/graphene/types/structures.py +++ b/graphene/types/structures.py @@ -9,9 +9,22 @@ class Structure(UnmountedType): def __init__(self, of_type, *args, **kwargs): super(Structure, self).__init__(*args, **kwargs) + if not isinstance(of_type, Structure) and isinstance(of_type, UnmountedType): + cls_name = type(self).__name__ + of_type_name = type(of_type).__name__ + raise Exception("{} could not have a mounted {}() as inner type. Try with {}({}).".format( + cls_name, + of_type_name, + cls_name, + of_type_name, + )) self.of_type = of_type def get_type(self): + ''' + This function is called when the unmounted type (List or NonNull instance) + is mounted (as a Field, InputField or Argument) + ''' return self @@ -52,7 +65,7 @@ def __init__(self, *args, **kwargs): super(NonNull, self).__init__(*args, **kwargs) assert not isinstance(self.of_type, NonNull), ( 'Can only create NonNull of a Nullable GraphQLType but got: {}.' - ).format(type) + ).format(self.of_type) def __str__(self): return '{}!'.format(self.of_type) diff --git a/graphene/types/tests/test_argument.py b/graphene/types/tests/test_argument.py index 34ed31447..b4cc3d58e 100644 --- a/graphene/types/tests/test_argument.py +++ b/graphene/types/tests/test_argument.py @@ -1,6 +1,8 @@ import pytest -from ..argument import Argument +from ..argument import Argument, to_arguments +from ..field import Field +from ..inputfield import InputField from ..structures import NonNull from ..scalars import String @@ -24,3 +26,38 @@ def test_argument_comparasion(): def test_argument_required(): arg = Argument(String, required=True) assert arg.type == NonNull(String) + + +def test_to_arguments(): + args = { + 'arg_string': Argument(String), + 'unmounted_arg': String(required=True) + } + + my_args = to_arguments(args) + assert my_args == { + 'arg_string': Argument(String), + 'unmounted_arg': Argument(String, required=True) + } + + +def test_to_arguments_raises_if_field(): + args = { + 'arg_string': Field(String), + } + + with pytest.raises(ValueError) as exc_info: + to_arguments(args) + + assert str(exc_info.value) == 'Expected arg_string to be Argument, but received Field. Try using Argument(String).' + + +def test_to_arguments_raises_if_inputfield(): + args = { + 'arg_string': InputField(String), + } + + with pytest.raises(ValueError) as exc_info: + to_arguments(args) + + assert str(exc_info.value) == 'Expected arg_string to be Argument, but received InputField. Try using Argument(String).' diff --git a/graphene/types/tests/test_datetime.py b/graphene/types/tests/test_datetime.py index f55cd8c6e..cef7384fb 100644 --- a/graphene/types/tests/test_datetime.py +++ b/graphene/types/tests/test_datetime.py @@ -1,18 +1,22 @@ import datetime import pytz -from ..datetime import DateTime +from ..datetime import DateTime, Time from ..objecttype import ObjectType from ..schema import Schema class Query(ObjectType): datetime = DateTime(_in=DateTime(name='in')) + time = Time(_at=Time(name='at')) def resolve_datetime(self, args, context, info): _in = args.get('in') return _in + def resolve_time(self, args, context, info): + return args.get('at') + schema = Schema(query=Query) @@ -27,6 +31,17 @@ def test_datetime_query(): } +def test_time_query(): + now = datetime.datetime.now().replace(tzinfo=pytz.utc) + time = datetime.time(now.hour, now.minute, now.second, now.microsecond, now.tzinfo) + isoformat = time.isoformat() + + result = schema.execute('''{ time(at: "%s") }'''%isoformat) + assert not result.errors + assert result.data == { + 'time': isoformat + } + def test_datetime_query_variable(): now = datetime.datetime.now().replace(tzinfo=pytz.utc) isoformat = now.isoformat() @@ -39,3 +54,18 @@ def test_datetime_query_variable(): assert result.data == { 'datetime': isoformat } + + +def test_time_query_variable(): + now = datetime.datetime.now().replace(tzinfo=pytz.utc) + time = datetime.time(now.hour, now.minute, now.second, now.microsecond, now.tzinfo) + isoformat = time.isoformat() + + result = schema.execute( + '''query Test($time: Time){ time(at: $time) }''', + variable_values={'time': isoformat} + ) + assert not result.errors + assert result.data == { + 'time': isoformat + } diff --git a/graphene/types/tests/test_dynamic.py b/graphene/types/tests/test_dynamic.py new file mode 100644 index 000000000..61dcbd812 --- /dev/null +++ b/graphene/types/tests/test_dynamic.py @@ -0,0 +1,27 @@ +from ..structures import List, NonNull +from ..scalars import String +from ..dynamic import Dynamic + + +def test_dynamic(): + dynamic = Dynamic(lambda: String) + assert dynamic.get_type() == String + assert str(dynamic.get_type()) == 'String' + + +def test_nonnull(): + dynamic = Dynamic(lambda: NonNull(String)) + assert dynamic.get_type().of_type == String + assert str(dynamic.get_type()) == 'String!' + + +def test_list(): + dynamic = Dynamic(lambda: List(String)) + assert dynamic.get_type().of_type == String + assert str(dynamic.get_type()) == '[String]' + + +def test_list_non_null(): + dynamic = Dynamic(lambda: List(NonNull(String))) + assert dynamic.get_type().of_type.of_type == String + assert str(dynamic.get_type()) == '[String!]' diff --git a/graphene/types/tests/test_enum.py b/graphene/types/tests/test_enum.py index 9160df8cb..a5a2d4c1f 100644 --- a/graphene/types/tests/test_enum.py +++ b/graphene/types/tests/test_enum.py @@ -1,4 +1,7 @@ from ..enum import Enum, PyEnum +from ..field import Field +from ..inputfield import InputField +from ..argument import Argument def test_enum_construction(): @@ -72,3 +75,39 @@ class RGB(Enum): assert RGB.RED.value == 1 assert RGB.GREEN.value == 2 assert RGB.BLUE.value == 3 + + +def test_enum_value_as_unmounted_field(): + class RGB(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + unmounted = RGB() + unmounted_field = unmounted.Field() + assert isinstance(unmounted_field, Field) + assert unmounted_field.type == RGB + + +def test_enum_value_as_unmounted_inputfield(): + class RGB(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + unmounted = RGB() + unmounted_field = unmounted.InputField() + assert isinstance(unmounted_field, InputField) + assert unmounted_field.type == RGB + + +def test_enum_value_as_unmounted_argument(): + class RGB(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + unmounted = RGB() + unmounted_field = unmounted.Argument() + assert isinstance(unmounted_field, Argument) + assert unmounted_field.type == RGB diff --git a/graphene/types/tests/test_field.py b/graphene/types/tests/test_field.py index 7ca557ba7..7633e2514 100644 --- a/graphene/types/tests/test_field.py +++ b/graphene/types/tests/test_field.py @@ -3,6 +3,7 @@ from ..argument import Argument from ..field import Field from ..structures import NonNull +from ..scalars import String class MyInstance(object): @@ -75,6 +76,20 @@ def test_field_source_func(): assert field.resolver(MyInstance(), {}, None, None) == MyInstance.value_func() +def test_field_source_as_argument(): + MyType = object() + field = Field(MyType, source=String()) + assert 'source' in field.args + assert field.args['source'].type == String + + +def test_field_name_as_argument(): + MyType = object() + field = Field(MyType, name=String()) + assert 'name' in field.args + assert field.args['name'].type == String + + def test_field_source_argument_as_kw(): MyType = object() field = Field(MyType, b=NonNull(True), c=Argument(None), a=NonNull(False)) diff --git a/graphene/types/tests/test_inputobjecttype.py b/graphene/types/tests/test_inputobjecttype.py index ca73156c3..7f8eaa7a7 100644 --- a/graphene/types/tests/test_inputobjecttype.py +++ b/graphene/types/tests/test_inputobjecttype.py @@ -1,7 +1,9 @@ from ..abstracttype import AbstractType from ..field import Field +from ..argument import Argument from ..inputfield import InputField +from ..objecttype import ObjectType from ..inputobjecttype import InputObjectType from ..unmountedtype import UnmountedType @@ -61,6 +63,22 @@ class MyInputObjectType(InputObjectType): assert isinstance(MyInputObjectType._meta.fields['field'], InputField) +def test_generate_inputobjecttype_as_argument(): + class MyInputObjectType(InputObjectType): + field = MyScalar() + + class MyObjectType(ObjectType): + field = Field(MyType, input=MyInputObjectType()) + + assert 'field' in MyObjectType._meta.fields + field = MyObjectType._meta.fields['field'] + assert isinstance(field, Field) + assert field.type == MyType + assert 'input' in field.args + assert isinstance(field.args['input'], Argument) + assert field.args['input'].type == MyInputObjectType + + def test_generate_inputobjecttype_inherit_abstracttype(): class MyAbstractType(AbstractType): field1 = MyScalar(MyType) diff --git a/graphene/types/tests/test_mountedtype.py b/graphene/types/tests/test_mountedtype.py new file mode 100644 index 000000000..3df7e7fbb --- /dev/null +++ b/graphene/types/tests/test_mountedtype.py @@ -0,0 +1,26 @@ +import pytest + +from ..mountedtype import MountedType +from ..field import Field +from ..scalars import String + + +class CustomField(Field): + def __init__(self, *args, **kwargs): + self.metadata = kwargs.pop('metadata', None) + super(CustomField, self).__init__(*args, **kwargs) + + +def test_mounted_type(): + unmounted = String() + mounted = Field.mount(unmounted) + assert isinstance(mounted, Field) + assert mounted.type == String + + +def test_mounted_type_custom(): + unmounted = String(metadata={'hey': 'yo!'}) + mounted = CustomField.mount(unmounted) + assert isinstance(mounted, CustomField) + assert mounted.type == String + assert mounted.metadata == {'hey': 'yo!'} diff --git a/graphene/types/tests/test_mutation.py b/graphene/types/tests/test_mutation.py index 2af6f4fd4..8ff8773f5 100644 --- a/graphene/types/tests/test_mutation.py +++ b/graphene/types/tests/test_mutation.py @@ -4,6 +4,7 @@ from ..objecttype import ObjectType from ..schema import Schema from ..scalars import String +from ..dynamic import Dynamic def test_generate_mutation_no_args(): @@ -47,12 +48,16 @@ def test_mutation_execution(): class CreateUser(Mutation): class Input: name = String() + dynamic = Dynamic(lambda: String()) + dynamic_none = Dynamic(lambda: None) name = String() + dynamic = Dynamic(lambda: String()) def mutate(self, args, context, info): name = args.get('name') - return CreateUser(name=name) + dynamic = args.get('dynamic') + return CreateUser(name=name, dynamic=dynamic) class Query(ObjectType): a = String() @@ -62,14 +67,16 @@ class MyMutation(ObjectType): schema = Schema(query=Query, mutation=MyMutation) result = schema.execute(''' mutation mymutation { - createUser(name:"Peter") { + createUser(name:"Peter", dynamic: "dynamic") { name + dynamic } } ''') assert not result.errors assert result.data == { 'createUser': { - 'name': "Peter" + 'name': 'Peter', + 'dynamic': 'dynamic', } } diff --git a/graphene/types/tests/test_options.py b/graphene/types/tests/test_options.py new file mode 100644 index 000000000..fbcba2db0 --- /dev/null +++ b/graphene/types/tests/test_options.py @@ -0,0 +1,30 @@ +import pytest + +from ..options import Options + + +def test_options(): + class BaseOptions: + option_1 = False + name = True + meta = Options(BaseOptions, name=False, option_1=False) + assert meta.name == True + assert meta.option_1 == False + + +def test_options_extra_attrs(): + class BaseOptions: + name = True + type = True + + with pytest.raises(Exception) as exc_info: + meta = Options(BaseOptions) + + assert str(exc_info.value) == 'Invalid attributes: name, type' + + +def test_options_repr(): + class BaseOptions: + name = True + meta = Options(BaseOptions, name=False) + assert repr(meta) == '' diff --git a/graphene/types/tests/test_query.py b/graphene/types/tests/test_query.py index 4f9d8810a..daeb63e83 100644 --- a/graphene/types/tests/test_query.py +++ b/graphene/types/tests/test_query.py @@ -4,12 +4,15 @@ from graphql import Source, execute, parse, GraphQLError from ..field import Field +from ..interface import Interface from ..inputfield import InputField from ..inputobjecttype import InputObjectType from ..objecttype import ObjectType from ..scalars import Int, String from ..schema import Schema from ..structures import List +from ..union import Union +from ..dynamic import Dynamic def test_query(): @@ -23,6 +26,112 @@ class Query(ObjectType): assert executed.data == {'hello': 'World'} +def test_query_union(): + class one_object(object): + pass + + class two_object(object): + pass + + class One(ObjectType): + one = String() + + @classmethod + def is_type_of(cls, root, context, info): + return isinstance(root, one_object) + + class Two(ObjectType): + two = String() + + @classmethod + def is_type_of(cls, root, context, info): + return isinstance(root, two_object) + + class MyUnion(Union): + class Meta: + types = (One, Two) + + class Query(ObjectType): + unions = List(MyUnion) + + def resolve_unions(self, args, context, info): + return [one_object(), two_object()] + + hello_schema = Schema(Query) + + executed = hello_schema.execute('{ unions { __typename } }') + assert not executed.errors + assert executed.data == { + 'unions': [{ + '__typename': 'One' + }, { + '__typename': 'Two' + }] + } + + +def test_query_interface(): + class one_object(object): + pass + + class two_object(object): + pass + + class MyInterface(Interface): + base = String() + + class One(ObjectType): + class Meta: + interfaces = (MyInterface, ) + + one = String() + + @classmethod + def is_type_of(cls, root, context, info): + return isinstance(root, one_object) + + class Two(ObjectType): + class Meta: + interfaces = (MyInterface, ) + + two = String() + + @classmethod + def is_type_of(cls, root, context, info): + return isinstance(root, two_object) + + class Query(ObjectType): + interfaces = List(MyInterface) + + def resolve_interfaces(self, args, context, info): + return [one_object(), two_object()] + + hello_schema = Schema(Query, types=[One, Two]) + + executed = hello_schema.execute('{ interfaces { __typename } }') + assert not executed.errors + assert executed.data == { + 'interfaces': [{ + '__typename': 'One' + }, { + '__typename': 'Two' + }] + } + + +def test_query_dynamic(): + class Query(ObjectType): + hello = Dynamic(lambda: String(resolver=lambda *_: 'World')) + hellos = Dynamic(lambda: List(String, resolver=lambda *_: ['Worlds'])) + hello_field = Dynamic(lambda: Field(String, resolver=lambda *_: 'Field World')) + + hello_schema = Schema(Query) + + executed = hello_schema.execute('{ hello hellos helloField }') + assert not executed.errors + assert executed.data == {'hello': 'World', 'hellos': ['Worlds'], 'helloField': 'Field World'} + + def test_query_default_value(): class MyType(ObjectType): field = String() diff --git a/graphene/types/tests/test_schema.py b/graphene/types/tests/test_schema.py new file mode 100644 index 000000000..af9bc14c0 --- /dev/null +++ b/graphene/types/tests/test_schema.py @@ -0,0 +1,54 @@ +import pytest + +from ..schema import Schema +from ..objecttype import ObjectType +from ..scalars import String +from ..field import Field + + +class MyOtherType(ObjectType): + field = String() + + +class Query(ObjectType): + inner = Field(MyOtherType) + + +def test_schema(): + schema = Schema(Query) + assert schema.get_query_type() == schema.get_graphql_type(Query) + + +def test_schema_get_type(): + schema = Schema(Query) + assert schema.Query == Query + assert schema.MyOtherType == MyOtherType + + +def test_schema_get_type_error(): + schema = Schema(Query) + with pytest.raises(AttributeError) as exc_info: + schema.X + + assert str(exc_info.value) == 'Type "X" not found in the Schema' + + +def test_schema_str(): + schema = Schema(Query) + assert str(schema) == """schema { + query: Query +} + +type MyOtherType { + field: String +} + +type Query { + inner: MyOtherType +} +""" + + +def test_schema_introspect(): + schema = Schema(Query) + assert '__schema' in schema.introspect() diff --git a/graphene/types/tests/test_structures.py b/graphene/types/tests/test_structures.py index 9027895ea..e45f09e24 100644 --- a/graphene/types/tests/test_structures.py +++ b/graphene/types/tests/test_structures.py @@ -10,12 +10,51 @@ def test_list(): assert str(_list) == '[String]' +def test_list_with_unmounted_type(): + with pytest.raises(Exception) as exc_info: + List(String()) + + assert str(exc_info.value) == 'List could not have a mounted String() as inner type. Try with List(String).' + + +def test_list_inherited_works_list(): + _list = List(List(String)) + assert isinstance(_list.of_type, List) + assert _list.of_type.of_type == String + + +def test_list_inherited_works_nonnull(): + _list = List(NonNull(String)) + assert isinstance(_list.of_type, NonNull) + assert _list.of_type.of_type == String + + def test_nonnull(): nonnull = NonNull(String) assert nonnull.of_type == String assert str(nonnull) == 'String!' +def test_nonnull_inherited_works_list(): + _list = NonNull(List(String)) + assert isinstance(_list.of_type, List) + assert _list.of_type.of_type == String + + +def test_nonnull_inherited_dont_work_nonnull(): + with pytest.raises(Exception) as exc_info: + NonNull(NonNull(String)) + + assert str(exc_info.value) == 'Can only create NonNull of a Nullable GraphQLType but got: String!.' + + +def test_nonnull_with_unmounted_type(): + with pytest.raises(Exception) as exc_info: + NonNull(String()) + + assert str(exc_info.value) == 'NonNull could not have a mounted String() as inner type. Try with NonNull(String).' + + def test_list_comparasion(): list1 = List(String) list2 = List(String) diff --git a/graphene/types/typemap.py b/graphene/types/typemap.py index c2b472791..70aa84cc4 100644 --- a/graphene/types/typemap.py +++ b/graphene/types/typemap.py @@ -5,20 +5,26 @@ from graphql import (GraphQLArgument, GraphQLBoolean, GraphQLField, GraphQLFloat, GraphQLID, GraphQLInputObjectField, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLString) -from graphql.type import GraphQLEnumValue from graphql.execution.executor import get_default_resolve_type_fn +from graphql.type import GraphQLEnumValue from graphql.type.typemap import GraphQLTypeMap -from ..utils.str_converters import to_camel_case from ..utils.get_unbound_function import get_unbound_function +from ..utils.str_converters import to_camel_case +from .definitions import (GrapheneEnumType, GrapheneInputObjectType, + GrapheneInterfaceType, GrapheneObjectType, + GrapheneScalarType, GrapheneUnionType, + GrapheneGraphQLType) from .dynamic import Dynamic from .enum import Enum +from .field import Field from .inputobjecttype import InputObjectType from .interface import Interface from .objecttype import ObjectType from .scalars import ID, Boolean, Float, Int, Scalar, String from .structures import List, NonNull from .union import Union +from .utils import get_field_as def is_graphene_type(_type): @@ -28,18 +34,18 @@ def is_graphene_type(_type): return True -def resolve_type(resolve_type_func, map, root, context, info): +def resolve_type(resolve_type_func, map, type_name, root, context, info): _type = resolve_type_func(root, context, info) - # assert inspect.isclass(_type) and issubclass(_type, ObjectType), ( - # 'Received incompatible type "{}".'.format(_type) - # ) + if not _type: - return get_default_resolve_type_fn(root, context, info, info.return_type) + return_type = map[type_name] + return get_default_resolve_type_fn(root, context, info, return_type) if inspect.isclass(_type) and issubclass(_type, ObjectType): graphql_type = map.get(_type._meta.name) assert graphql_type and graphql_type.graphene_type == _type return graphql_type + return _type @@ -63,7 +69,7 @@ def graphene_reducer(self, map, type): return self.reducer(map, type.of_type) if type._meta.name in map: _type = map[type._meta.name] - if is_graphene_type(_type): + if isinstance(_type, GrapheneGraphQLType): assert _type.graphene_type == type return map if issubclass(type, ObjectType): @@ -81,7 +87,6 @@ def graphene_reducer(self, map, type): return map def construct_scalar(self, map, type): - from .definitions import GrapheneScalarType _scalars = { String: GraphQLString, Int: GraphQLInt, @@ -104,7 +109,6 @@ def construct_scalar(self, map, type): return map def construct_enum(self, map, type): - from .definitions import GrapheneEnumType values = OrderedDict() for name, value in type._meta.enum.__members__.items(): values[name] = GraphQLEnumValue( @@ -122,10 +126,9 @@ def construct_enum(self, map, type): return map def construct_objecttype(self, map, type): - from .definitions import GrapheneObjectType if type._meta.name in map: _type = map[type._meta.name] - if is_graphene_type(_type): + if isinstance(_type, GrapheneGraphQLType): assert _type.graphene_type == type return map map[type._meta.name] = GrapheneObjectType( @@ -146,10 +149,9 @@ def construct_objecttype(self, map, type): return map def construct_interface(self, map, type): - from .definitions import GrapheneInterfaceType _resolve_type = None if type.resolve_type: - _resolve_type = partial(resolve_type, type.resolve_type, map) + _resolve_type = partial(resolve_type, type.resolve_type, map, type._meta.name) map[type._meta.name] = GrapheneInterfaceType( graphene_type=type, name=type._meta.name, @@ -162,7 +164,6 @@ def construct_interface(self, map, type): return map def construct_inputobjecttype(self, map, type): - from .definitions import GrapheneInputObjectType map[type._meta.name] = GrapheneInputObjectType( graphene_type=type, name=type._meta.name, @@ -173,10 +174,9 @@ def construct_inputobjecttype(self, map, type): return map def construct_union(self, map, type): - from .definitions import GrapheneUnionType _resolve_type = None if type.resolve_type: - _resolve_type = partial(resolve_type, type.resolve_type, map) + _resolve_type = partial(resolve_type, type.resolve_type, map, type._meta.name) types = [] for i in type._meta.types: map = self.construct_objecttype(map, i) @@ -202,7 +202,7 @@ def construct_fields_for_type(self, map, type, is_input_type=False): fields = OrderedDict() for name, field in type._meta.fields.items(): if isinstance(field, Dynamic): - field = field.get_type() + field = get_field_as(field.get_type(), _as=Field) if not field: continue map = self.reducer(map, field.type) diff --git a/graphene/types/union.py b/graphene/types/union.py index fa178594d..3d236000d 100644 --- a/graphene/types/union.py +++ b/graphene/types/union.py @@ -39,7 +39,11 @@ class Union(six.with_metaclass(UnionMeta)): to determine which type is actually used when the field is resolved. ''' - resolve_type = None + @classmethod + def resolve_type(cls, instance, context, info): + from .objecttype import ObjectType + if isinstance(instance, ObjectType): + return type(instance) def __init__(self, *args, **kwargs): - raise Exception("An Union cannot be intitialized") + raise Exception("A Union cannot be intitialized") diff --git a/graphene/types/unmountedtype.py b/graphene/types/unmountedtype.py index c9b366310..4dfe762ce 100644 --- a/graphene/types/unmountedtype.py +++ b/graphene/types/unmountedtype.py @@ -8,7 +8,7 @@ class UnmountedType(OrderedType): Instead of writing >>> class MyObjectType(ObjectType): - >>> my_field = Field(String(), description='Description here') + >>> my_field = Field(String, description='Description here') It let you write >>> class MyObjectType(ObjectType): @@ -21,43 +21,35 @@ def __init__(self, *args, **kwargs): self.kwargs = kwargs def get_type(self): + ''' + This function is called when the UnmountedType instance + is mounted (as a Field, InputField or Argument) + ''' raise NotImplementedError("get_type not implemented in {}".format(self)) + def mount_as(self, _as): + return _as.mount(self) + def Field(self): # noqa: N802 ''' Mount the UnmountedType as Field ''' from .field import Field - return Field( - self.get_type(), - *self.args, - _creation_counter=self.creation_counter, - **self.kwargs - ) + return self.mount_as(Field) def InputField(self): # noqa: N802 ''' Mount the UnmountedType as InputField ''' from .inputfield import InputField - return InputField( - self.get_type(), - *self.args, - _creation_counter=self.creation_counter, - **self.kwargs - ) + return self.mount_as(InputField) def Argument(self): # noqa: N802 ''' Mount the UnmountedType as Argument ''' from .argument import Argument - return Argument( - self.get_type(), - *self.args, - _creation_counter=self.creation_counter, - **self.kwargs - ) + return self.mount_as(Argument) def __eq__(self, other): return ( diff --git a/graphene/types/utils.py b/graphene/types/utils.py index c171e9e00..3c70711c3 100644 --- a/graphene/types/utils.py +++ b/graphene/types/utils.py @@ -1,8 +1,6 @@ from collections import OrderedDict -from .dynamic import Dynamic -from .field import Field -from .inputfield import InputField +from .mountedtype import MountedType from .unmountedtype import UnmountedType @@ -35,34 +33,16 @@ def get_base_fields(bases, _as=None): return fields -def mount_as(unmounted_field, _as): - ''' - Mount the UnmountedType dinamically as Field or InputField - ''' - if _as is None: - return unmounted_field - - elif _as is Field: - return unmounted_field.Field() - - elif _as is InputField: - return unmounted_field.InputField() - - raise Exception( - 'Unmounted field "{}" cannot be mounted in {}.'.format( - unmounted_field, _as - ) - ) - - def get_field_as(value, _as=None): ''' Get type mounted ''' - if isinstance(value, (Field, InputField, Dynamic)): + if isinstance(value, MountedType): return value elif isinstance(value, UnmountedType): - return mount_as(value, _as) + if _as is None: + return value + return _as.mount(value) def yank_fields_from_attrs(attrs, _as=None, delete=True, sort=True): diff --git a/graphene/utils/tests/test_orderedtype.py b/graphene/utils/tests/test_orderedtype.py index 2845b8766..ea6c7cc09 100644 --- a/graphene/utils/tests/test_orderedtype.py +++ b/graphene/utils/tests/test_orderedtype.py @@ -23,3 +23,19 @@ def test_orderedtype_hash(): assert hash(one) == hash(one) assert hash(one) != hash(two) + + +def test_orderedtype_resetcounter(): + one = OrderedType() + two = OrderedType() + one.reset_counter() + + assert one > two + + +def test_orderedtype_non_orderabletypes(): + one = OrderedType() + + assert one.__lt__(1) == NotImplemented + assert one.__gt__(1) == NotImplemented + assert not one == 1 diff --git a/setup.py b/setup.py index b9f8d1f84..4c3364510 100644 --- a/setup.py +++ b/setup.py @@ -70,9 +70,9 @@ def run_tests(self): install_requires=[ 'six>=1.10.0', - 'graphql-core>=1.0', - 'graphql-relay>=0.4.4', - 'promise', + 'graphql-core>=1.0.1', + 'graphql-relay>=0.4.5', + 'promise>=1.0.1', ], tests_require=[ 'pytest>=2.7.2', diff --git a/tox.ini b/tox.ini index 7dbcffa5b..13d40c960 100644 --- a/tox.ini +++ b/tox.ini @@ -5,12 +5,15 @@ skipsdist = true [testenv] deps= pytest>=2.7.2 - graphql-core>=0.5.1 - graphql-relay>=0.4.3 + graphql-core>=1.0.1 + graphql-relay>=0.4.5 six blinker singledispatch mock + pytz + iso8601 + pytest-benchmark setenv = PYTHONPATH = .:{envdir} commands=