diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 7b990b86..e12fab3a 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -27,7 +27,7 @@ class ConnectionOrListField(Field): if not field_object_type: raise SkipField() if is_node(field_object_type): - field = DjangoConnectionField(field_object_type) + field = ConnectionField(field_object_type) else: field = Field(List(field_object_type)) field.contribute_to_class(self.object_type, self.attname) diff --git a/graphene/contrib/django/utils.py b/graphene/contrib/django/utils.py index 54c6420c..3c72a0e1 100644 --- a/graphene/contrib/django/utils.py +++ b/graphene/contrib/django/utils.py @@ -1,5 +1,8 @@ from django.db import models from django.db.models.manager import Manager +from django.db.models.query import QuerySet + +from graphene.utils import LazyList def get_type_for_model(schema, model): @@ -19,7 +22,18 @@ def get_reverse_fields(model): yield related +class WrappedQueryset(LazyList): + + def __len__(self): + # Dont calculate the length using len(queryset), as this will + # evaluate the whole queryset and return it's length. + # Use .count() instead + return self._origin.count() + + def maybe_queryset(value): if isinstance(value, Manager): value = value.get_queryset() + if isinstance(value, QuerySet): + return WrappedQueryset(value) return value diff --git a/graphene/core/classtypes/base.py b/graphene/core/classtypes/base.py index 31a2c8d2..9dafff0e 100644 --- a/graphene/core/classtypes/base.py +++ b/graphene/core/classtypes/base.py @@ -1,10 +1,10 @@ import copy import inspect +from functools import partial from collections import OrderedDict import six -from ..exceptions import SkipField from .options import Options @@ -48,8 +48,8 @@ class ClassTypeMeta(type): if not cls._meta.abstract: from ..types import List, NonNull - setattr(cls, 'NonNull', NonNull(cls)) - setattr(cls, 'List', List(cls)) + setattr(cls, 'NonNull', partial(NonNull, cls)) + setattr(cls, 'List', partial(List, cls)) return cls @@ -81,13 +81,18 @@ class FieldsOptions(Options): def fields_map(self): return OrderedDict([(f.attname, f) for f in self.fields]) + @property + def fields_group_type(self): + from ..types.field import FieldsGroupType + return FieldsGroupType(*self.local_fields) + class FieldsClassTypeMeta(ClassTypeMeta): options_class = FieldsOptions def extend_fields(cls, bases): new_fields = cls._meta.local_fields - field_names = {f.name: f for f in new_fields} + field_names = {f.attname: f for f in new_fields} for base in bases: if not isinstance(base, FieldsClassTypeMeta): @@ -95,17 +100,17 @@ class FieldsClassTypeMeta(ClassTypeMeta): parent_fields = base._meta.local_fields for field in parent_fields: - if field.name in field_names and field.type.__class__ != field_names[ - field.name].type.__class__: + if field.attname in field_names and field.type.__class__ != field_names[ + field.attname].type.__class__: raise Exception( 'Local field %r in class %r (%r) clashes ' 'with field with similar name from ' 'Interface %s (%r)' % ( - field.name, + field.attname, cls.__name__, field.__class__, base.__name__, - field_names[field.name].__class__) + field_names[field.attname].__class__) ) new_field = copy.copy(field) cls.add_to_class(field.attname, new_field) @@ -123,11 +128,4 @@ class FieldsClassType(six.with_metaclass(FieldsClassTypeMeta, ClassType)): @classmethod def fields_internal_types(cls, schema): - fields = [] - for field in cls._meta.fields: - try: - fields.append((field.name, schema.T(field))) - except SkipField: - continue - - return OrderedDict(fields) + return schema.T(cls._meta.fields_group_type) diff --git a/graphene/core/classtypes/tests/test_base.py b/graphene/core/classtypes/tests/test_base.py index 5017f94d..4666fdc2 100644 --- a/graphene/core/classtypes/tests/test_base.py +++ b/graphene/core/classtypes/tests/test_base.py @@ -23,15 +23,26 @@ def test_classtype_advanced(): def test_classtype_definition_list(): class Character(ClassType): '''Character description''' - assert isinstance(Character.List, List) - assert Character.List.of_type == Character + assert isinstance(Character.List(), List) + assert Character.List().of_type == Character def test_classtype_definition_nonnull(): class Character(ClassType): '''Character description''' - assert isinstance(Character.NonNull, NonNull) - assert Character.NonNull.of_type == Character + assert isinstance(Character.NonNull(), NonNull) + assert Character.NonNull().of_type == Character + + +def test_fieldsclasstype_definition_order(): + class Character(ClassType): + '''Character description''' + + class Query(FieldsClassType): + name = String() + char = Character.NonNull() + + assert list(Query._meta.fields_map.keys()) == ['name', 'char'] def test_fieldsclasstype(): diff --git a/graphene/core/classtypes/tests/test_mutation.py b/graphene/core/classtypes/tests/test_mutation.py index ac32585e..85dd2368 100644 --- a/graphene/core/classtypes/tests/test_mutation.py +++ b/graphene/core/classtypes/tests/test_mutation.py @@ -24,4 +24,4 @@ def test_mutation(): assert list(object_type.get_fields().keys()) == ['name'] assert MyMutation._meta.fields_map['name'].object_type == MyMutation assert isinstance(MyMutation.arguments, ArgumentsGroup) - assert 'argName' in MyMutation.arguments + assert 'argName' in schema.T(MyMutation.arguments) diff --git a/graphene/core/schema.py b/graphene/core/schema.py index 1b0ce8f9..4da067e4 100644 --- a/graphene/core/schema.py +++ b/graphene/core/schema.py @@ -12,6 +12,7 @@ from graphene import signals from .classtypes.base import ClassType from .types.base import BaseType +from ..plugins import Plugin, CamelCase class GraphQLSchema(_GraphQLSchema): @@ -25,7 +26,7 @@ class Schema(object): _executor = None def __init__(self, query=None, mutation=None, subscription=None, - name='Schema', executor=None): + name='Schema', executor=None, plugins=None, auto_camelcase=True): self._types_names = {} self._types = {} self.mutation = mutation @@ -33,11 +34,27 @@ class Schema(object): self.subscription = subscription self.name = name self.executor = executor + self.plugins = [] + plugins = plugins or [] + if auto_camelcase: + plugins.append(CamelCase()) + for plugin in plugins: + self.add_plugin(plugin) signals.init_schema.send(self) def __repr__(self): return '' % (str(self.name), hash(self)) + def add_plugin(self, plugin): + assert isinstance(plugin, Plugin), 'A plugin need to subclass graphene.Plugin and be instantiated' + plugin.contribute_to_schema(self) + self.plugins.append(plugin) + + def get_internal_type(self, objecttype): + for plugin in self.plugins: + objecttype = plugin.transform_type(objecttype) + return objecttype.internal_type(self) + def T(self, object_type): if not object_type: return @@ -45,7 +62,7 @@ class Schema(object): object_type, (BaseType, ClassType)) or isinstance( object_type, BaseType): if object_type not in self._types: - internal_type = object_type.internal_type(self) + internal_type = self.get_internal_type(object_type) self._types[object_type] = internal_type is_objecttype = inspect.isclass( object_type) and issubclass(object_type, ClassType) diff --git a/graphene/core/tests/test_old_fields.py b/graphene/core/tests/test_old_fields.py index 95bf9aab..3f24aedf 100644 --- a/graphene/core/tests/test_old_fields.py +++ b/graphene/core/tests/test_old_fields.py @@ -34,10 +34,11 @@ def test_field_type(): assert schema.T(f).type == GraphQLString -def test_field_name_automatic_camelcase(): +def test_field_name(): f = Field(GraphQLString) f.contribute_to_class(MyOt, 'field_name') - assert f.name == 'fieldName' + assert f.name is None + assert f.attname == 'field_name' def test_field_name_use_name_if_exists(): diff --git a/graphene/core/types/argument.py b/graphene/core/types/argument.py index 0892c446..a8fe343e 100644 --- a/graphene/core/types/argument.py +++ b/graphene/core/types/argument.py @@ -1,19 +1,19 @@ -from collections import OrderedDict from functools import wraps from itertools import chain from graphql.core.type import GraphQLArgument -from ...utils import ProxySnakeDict, to_camel_case -from .base import ArgumentType, BaseType, OrderedType +from ...utils import ProxySnakeDict +from .base import ArgumentType, GroupNamedType, NamedType, OrderedType -class Argument(OrderedType): +class Argument(NamedType, OrderedType): def __init__(self, type, description=None, default=None, name=None, _creation_counter=None): super(Argument, self).__init__(_creation_counter=_creation_counter) self.name = name + self.attname = None self.type = type self.description = description self.default = default @@ -27,47 +27,32 @@ class Argument(OrderedType): return self.name -class ArgumentsGroup(BaseType): +class ArgumentsGroup(GroupNamedType): def __init__(self, *args, **kwargs): arguments = to_arguments(*args, **kwargs) - self.arguments = OrderedDict([(arg.name, arg) for arg in arguments]) - - def internal_type(self, schema): - return OrderedDict([(arg.name, schema.T(arg)) - for arg in self.arguments.values()]) - - def __len__(self): - return len(self.arguments) - - def __iter__(self): - return iter(self.arguments) - - def __contains__(self, *args): - return self.arguments.__contains__(*args) - - def __getitem__(self, *args): - return self.arguments.__getitem__(*args) + super(ArgumentsGroup, self).__init__(*arguments) def to_arguments(*args, **kwargs): arguments = {} iter_arguments = chain(kwargs.items(), [(None, a) for a in args]) - for name, arg in iter_arguments: + for attname, arg in iter_arguments: if isinstance(arg, Argument): argument = arg elif isinstance(arg, ArgumentType): argument = arg.as_argument() else: - raise ValueError('Unknown argument %s=%r' % (name, arg)) + raise ValueError('Unknown argument %s=%r' % (attname, arg)) - if name: - argument.name = to_camel_case(name) - assert argument.name, 'Argument in field must have a name' - assert argument.name not in arguments, 'Found more than one Argument with same name {}'.format( - argument.name) - arguments[argument.name] = argument + if attname: + argument.attname = attname + + name = argument.name or argument.attname + assert name, 'Argument in field must have a name' + assert name not in arguments, 'Found more than one Argument with same name {}'.format(name) + arguments[name] = argument return sorted(arguments.values()) diff --git a/graphene/core/types/base.py b/graphene/core/types/base.py index 2b4078e4..94e8a75c 100644 --- a/graphene/core/types/base.py +++ b/graphene/core/types/base.py @@ -1,7 +1,10 @@ -from functools import total_ordering +from collections import OrderedDict +from functools import total_ordering, partial import six +from ...utils import to_camel_case + class BaseType(object): @@ -126,3 +129,31 @@ class FieldType(MirroredType): class MountedType(FieldType, ArgumentType): pass + + +class NamedType(BaseType): + pass + + +class GroupNamedType(BaseType): + def __init__(self, *types): + self.types = types + + def get_named_type(self, schema, type): + name = type.name or type.attname + return name, schema.T(type) + + def internal_type(self, schema): + return OrderedDict(map(partial(self.get_named_type, schema), self.types)) + + def __len__(self): + return len(self.types) + + def __iter__(self): + return iter(self.types) + + def __contains__(self, *args): + return self.types.__contains__(*args) + + def __getitem__(self, *args): + return self.types.__getitem__(*args) diff --git a/graphene/core/types/field.py b/graphene/core/types/field.py index d7643b5e..cfe168f7 100644 --- a/graphene/core/types/field.py +++ b/graphene/core/types/field.py @@ -4,16 +4,16 @@ from functools import wraps import six from graphql.core.type import GraphQLField, GraphQLInputObjectField -from ...utils import to_camel_case from ..classtypes.base import FieldsClassType from ..classtypes.inputobjecttype import InputObjectType from ..classtypes.mutation import Mutation +from ..exceptions import SkipField from .argument import ArgumentsGroup, snake_case_args -from .base import LazyType, MountType, OrderedType +from .base import LazyType, NamedType, MountType, OrderedType, GroupNamedType from .definitions import NonNull -class Field(OrderedType): +class Field(NamedType, OrderedType): def __init__( self, type, description=None, args=None, name=None, resolver=None, @@ -36,8 +36,6 @@ class Field(OrderedType): assert issubclass( cls, (FieldsClassType)), 'Field {} cannot be mounted in {}'.format( self, cls) - if not self.name: - self.name = to_camel_case(attname) self.attname = attname self.object_type = cls self.mount(cls) @@ -63,6 +61,9 @@ class Field(OrderedType): return NonNull(self.type) return self.type + def decorate_resolver(self, resolver): + return snake_case_args(resolver) + def internal_type(self, schema): resolver = self.resolver description = self.description @@ -85,9 +86,9 @@ class Field(OrderedType): return my_resolver(instance, args, info) resolver = wrapped_func - resolver = snake_case_args(resolver) assert type, 'Internal type for field %s is None' % str(self) - return GraphQLField(type, args=schema.T(arguments), resolver=resolver, + return GraphQLField(type, args=schema.T(arguments), + resolver=self.decorate_resolver(resolver), description=description,) def __repr__(self): @@ -114,7 +115,7 @@ class Field(OrderedType): return hash((self.creation_counter, self.object_type)) -class InputField(OrderedType): +class InputField(NamedType, OrderedType): def __init__(self, type, description=None, default=None, name=None, _creation_counter=None, required=False): @@ -130,8 +131,6 @@ class InputField(OrderedType): assert issubclass( cls, (InputObjectType)), 'InputField {} cannot be mounted in {}'.format( self, cls) - if not self.name: - self.name = to_camel_case(attname) self.attname = attname self.object_type = cls self.mount(cls) @@ -143,3 +142,14 @@ class InputField(OrderedType): return GraphQLInputObjectField( schema.T(self.type), default_value=self.default, description=self.description) + + +class FieldsGroupType(GroupNamedType): + def internal_type(self, schema): + fields = [] + for field in sorted(self.types): + try: + fields.append(self.get_named_type(schema, field)) + except SkipField: + continue + return OrderedDict(fields) diff --git a/graphene/core/types/tests/test_argument.py b/graphene/core/types/tests/test_argument.py index 26bbb310..cf07a7d3 100644 --- a/graphene/core/types/tests/test_argument.py +++ b/graphene/core/types/tests/test_argument.py @@ -27,8 +27,8 @@ def test_to_arguments(): other_kwarg=String(), ) - assert [a.name for a in arguments] == [ - 'myArg', 'otherArg', 'myKwarg', 'otherKwarg'] + assert [a.name or a.attname for a in arguments] == [ + 'myArg', 'otherArg', 'my_kwarg', 'other_kwarg'] def test_to_arguments_no_name(): diff --git a/graphene/core/types/tests/test_field.py b/graphene/core/types/tests/test_field.py index 8253ed20..7bb23ca9 100644 --- a/graphene/core/types/tests/test_field.py +++ b/graphene/core/types/tests/test_field.py @@ -20,7 +20,7 @@ def test_field_internal_type(): schema = Schema(query=Query) type = schema.T(field) - assert field.name == 'myField' + assert field.name is None assert field.attname == 'my_field' assert isinstance(type, GraphQLField) assert type.description == 'My argument' @@ -98,9 +98,10 @@ def test_field_string_reference(): def test_field_custom_arguments(): field = Field(None, name='my_customName', p=String()) + schema = Schema() args = field.arguments - assert 'p' in args + assert 'p' in schema.T(args) def test_inputfield_internal_type(): @@ -115,7 +116,7 @@ def test_inputfield_internal_type(): schema = Schema(query=MyObjectType) type = schema.T(field) - assert field.name == 'myField' + assert field.name is None assert field.attname == 'my_field' assert isinstance(type, GraphQLInputObjectField) assert type.description == 'My input field' diff --git a/graphene/plugins/__init__.py b/graphene/plugins/__init__.py new file mode 100644 index 00000000..5a9bf26b --- /dev/null +++ b/graphene/plugins/__init__.py @@ -0,0 +1,6 @@ +from .base import Plugin +from .camel_case import CamelCase + +__all__ = [ + 'Plugin', 'CamelCase' +] diff --git a/graphene/plugins/base.py b/graphene/plugins/base.py new file mode 100644 index 00000000..557099c7 --- /dev/null +++ b/graphene/plugins/base.py @@ -0,0 +1,6 @@ +class Plugin(object): + def contribute_to_schema(self, schema): + self.schema = schema + + def transform_type(self, objecttype): + return objecttype diff --git a/graphene/plugins/camel_case.py b/graphene/plugins/camel_case.py new file mode 100644 index 00000000..cba39ad5 --- /dev/null +++ b/graphene/plugins/camel_case.py @@ -0,0 +1,22 @@ +from .base import Plugin + +from ..core.types.base import GroupNamedType +from ..utils import memoize, to_camel_case + + +def camelcase_named_type(schema, type): + name = type.name or to_camel_case(type.attname) + return name, schema.T(type) + + +class CamelCase(Plugin): + @memoize + def transform_group(self, _type): + new_type = _type.__class__(*_type.types) + setattr(new_type, 'get_named_type', camelcase_named_type) + return new_type + + def transform_type(self, _type): + if isinstance(_type, GroupNamedType): + return self.transform_group(_type) + return _type diff --git a/graphene/relay/tests/test_mutations.py b/graphene/relay/tests/test_mutations.py index 4356a1ec..02287725 100644 --- a/graphene/relay/tests/test_mutations.py +++ b/graphene/relay/tests/test_mutations.py @@ -34,8 +34,7 @@ schema = Schema(query=Query, mutation=MyResultMutation) def test_mutation_arguments(): assert ChangeNumber.arguments - assert list(ChangeNumber.arguments) == ['input'] - assert 'input' in ChangeNumber.arguments + assert 'input' in schema.T(ChangeNumber.arguments) inner_type = ChangeNumber.input_type client_mutation_id_field = inner_type._meta.fields_map[ 'client_mutation_id'] diff --git a/graphene/utils/__init__.py b/graphene/utils/__init__.py index 52fb6417..6adb1f73 100644 --- a/graphene/utils/__init__.py +++ b/graphene/utils/__init__.py @@ -3,8 +3,9 @@ from .proxy_snake_dict import ProxySnakeDict from .caching import cached_property, memoize from .misc import enum_to_graphql_enum from .resolve_only_args import resolve_only_args +from .lazylist import LazyList __all__ = ['to_camel_case', 'to_snake_case', 'ProxySnakeDict', 'cached_property', 'memoize', 'enum_to_graphql_enum', - 'resolve_only_args'] + 'resolve_only_args', 'LazyList'] diff --git a/graphene/utils/lazylist.py b/graphene/utils/lazylist.py new file mode 100644 index 00000000..434dcfd4 --- /dev/null +++ b/graphene/utils/lazylist.py @@ -0,0 +1,43 @@ +class LazyList(object): + + def __init__(self, origin, state=None): + self._origin = origin + self._state = state or [] + self._origin_iter = None + self._finished = False + + def __iter__(self): + return self if not self._finished else iter(self._state) + + def iter(self): + return self.__iter__() + + def __len__(self): + return self._origin.__len__() + + def __next__(self): + try: + if not self._origin_iter: + self._origin_iter = self._origin.__iter__() + n = next(self._origin_iter) + except StopIteration as e: + self._finished = True + raise e + else: + self._state.append(n) + return n + + def next(self): + return self.__next__() + + def __getitem__(self, key): + item = self._origin[key] + if isinstance(key, slice): + return self.__class__(item) + return item + + def __getattr__(self, name): + return getattr(self._origin, name) + + def __repr__(self): + return "<{} {}>".format(self.__class__.__name__, repr(self._origin)) diff --git a/graphene/utils/tests/test_lazylist.py b/graphene/utils/tests/test_lazylist.py new file mode 100644 index 00000000..972e2942 --- /dev/null +++ b/graphene/utils/tests/test_lazylist.py @@ -0,0 +1,23 @@ +from py.test import raises + +from ..lazylist import LazyList + + +def test_lazymap(): + data = list(range(10)) + lm = LazyList(data) + assert len(lm) == 10 + assert lm[1] == 1 + assert isinstance(lm[1:4], LazyList) + assert lm.append == data.append + assert repr(lm) == '' + + +def test_lazymap_iter(): + data = list(range(2)) + lm = LazyList(data) + iter_lm = iter(lm) + assert iter_lm.next() == 0 + assert iter_lm.next() == 1 + with raises(StopIteration): + iter_lm.next() diff --git a/setup.py b/setup.py index e8c61433..2c36fcec 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ class PyTest(TestCommand): setup( name='graphene', - version='0.4.2', + version='0.4.3', description='Graphene: Python DSL for GraphQL', long_description=open('README.rst').read(),