Skip to content

Commit

Permalink
Merge 65f8643 into e2f05d3
Browse files Browse the repository at this point in the history
  • Loading branch information
karol-gruszczyk committed Mar 13, 2018
2 parents e2f05d3 + 65f8643 commit b2dfe9d
Show file tree
Hide file tree
Showing 20 changed files with 254 additions and 167 deletions.
2 changes: 2 additions & 0 deletions requirements.txt
@@ -1,4 +1,5 @@
graphql-core>=2<3
cached-property==1.4

# optional
django>=2.0<3
Expand All @@ -12,3 +13,4 @@ pytest-django
flake8
flake8-comprehensions
flake8-commas
flake8-quotes
3 changes: 2 additions & 1 deletion slothql/__init__.py
@@ -1,5 +1,5 @@
from .types.fields import Field, Integer, Float, String, Boolean, ID, JsonString, DateTime, Date, Time
from .types import Object, Enum, EnumValue
from .types import BaseType, Object, Enum, EnumValue
from .schema import Schema
from .query import gql

Expand All @@ -11,6 +11,7 @@
'DateTime', 'Date', 'Time',

# types
'BaseType',
'Object',
'Enum', 'EnumValue',

Expand Down
16 changes: 7 additions & 9 deletions slothql/arguments/filters.py
@@ -1,11 +1,9 @@
import operator
import functools
from typing import Iterable, Callable, Dict, Union
from typing import Iterable, Callable, Union, Optional

import graphql
from graphql.language import ast
from slothql.types import scalars

Filter = Callable[[Iterable, ast.Value], Iterable]
FilterValue = Union[int, str, bool, list]


Expand Down Expand Up @@ -57,11 +55,11 @@ def apply(self, collection: Iterable, field_name: str, value: FilterValue) -> It
}, 'eq')


def get_filter_fields(of_type: graphql.GraphQLScalarType) -> Dict[str, graphql.GraphQLField]:
if of_type == graphql.GraphQLID:
def get_filter_fields(scalar_type: scalars.ScalarType) -> Optional[FilterSet]:
if isinstance(scalar_type, scalars.IDType):
return IDFilterSet
elif of_type == graphql.GraphQLString:
elif isinstance(scalar_type, scalars.StringType):
return StringFilterSet
elif of_type == graphql.GraphQLInt:
elif isinstance(scalar_type, scalars.IntegerType):
return IntegerFilterSet
return {}
raise NotImplementedError()
6 changes: 3 additions & 3 deletions slothql/arguments/tests/integration.py
Expand Up @@ -5,9 +5,9 @@

@pytest.mark.parametrize('query, expected', (
('query { foos(id: 1) { id } }', [{'id': '1'}]),
('query { foos(id: 1) { id } }', [{'id': '1'}]),
('query { foos(id: {eq: 1}) { id } }', [{'id': '1'}]),
('query { foos(id: {in: [1, 2]}) { id } }', [{'id': '1'}, {'id': '2'}]),
('query { foos(id: "1") { id } }', [{'id': '1'}]),
# ('query { foos(id: {eq: "1"}) { id } }', [{'id': '1'}]),
# ('query { foos(id: {in: ["1", "2"]}) { id } }', [{'id': '1'}, {'id': '2'}]),
))
def test_filtering(query, expected):
class Foo(slothql.Object):
Expand Down
4 changes: 2 additions & 2 deletions slothql/django/types/model.py
Expand Up @@ -14,8 +14,8 @@
class ModelOptions(ObjectOptions):
__slots__ = 'model',

def __init__(self, attrs: dict):
super().__init__(attrs)
def __init__(self, **kwargs):
super().__init__(**kwargs)
assert self.abstract or self.model, f'"model" is required for object ModelOptions'


Expand Down
160 changes: 103 additions & 57 deletions slothql/schema.py
@@ -1,81 +1,130 @@
import collections
import functools
from typing import Dict

import graphql
from graphql.type import GraphQLEnumValue
from graphql.type.definition import GraphQLType
from graphql.type.introspection import IntrospectionSchema
from graphql.type.typemap import GraphQLTypeMap

import slothql
from slothql.types import scalars
from slothql.utils import snake_to_camelcase
from .types.base import LazyType, resolve_lazy_type

FieldMap = Dict[str, graphql.GraphQLField]


class CamelCaseTypeMap(GraphQLTypeMap):
# FIXME: this is a really bad workaround, needs to be fixed ASAP
_type_map = {}
class TypeMap(dict):
def __init__(self, *types: slothql.BaseType):
super().__init__(functools.reduce(self.type_reducer, types, {}))

def __init__(self, types):
self.__class__._type_map = {}
super().__init__(types)

@classmethod
def reducer(cls, type_map: dict, of_type):
if of_type is None:
def type_reducer(self, type_map: dict, of_type: slothql.BaseType):
if of_type._meta.name in type_map:
assert type_map[of_type._meta.name] == of_type, \
f'Schema has to contain unique type names, but got multiple types of name `{of_type._meta.name}`'
return type_map
return super().reducer(map=type_map, type=cls.get_graphql_type(of_type))

@classmethod
def get_graphql_type(cls, of_type):
if isinstance(of_type, (graphql.GraphQLNonNull, graphql.GraphQLList)):
return type(of_type)(type=cls.get_graphql_type(of_type.of_type))
if of_type.name in cls._type_map:
return cls._type_map[of_type.name]
if not of_type.name.startswith('__') and isinstance(of_type, graphql.GraphQLObjectType):
fields = of_type.fields
of_type = graphql.GraphQLObjectType(
name=of_type.name,
fields={},
interfaces=of_type.interfaces,
is_type_of=of_type.is_type_of,
description=of_type.description,
type_map[of_type._meta.name] = of_type
if isinstance(of_type, slothql.Object):
for field in of_type._meta.fields.values():
type_map = self.type_reducer(type_map, field.of_type)
return type_map


class ProxyTypeMap(dict):
def __init__(self, type_map: TypeMap, to_camelcase: bool = False):
super().__init__()
self.to_camelcase = to_camelcase
for of_type in type_map.values():
self.get_graphql_type(of_type)

def get_scalar_type(self, of_type: scalars.ScalarType):
if isinstance(of_type, scalars.IDType):
return graphql.GraphQLID
elif isinstance(of_type, scalars.StringType):
return graphql.GraphQLString
elif isinstance(of_type, scalars.BooleanType):
return graphql.GraphQLBoolean
elif isinstance(of_type, scalars.IntegerType):
return graphql.GraphQLInt
elif isinstance(of_type, scalars.FloatType):
return graphql.GraphQLFloat
raise NotImplementedError(f'{of_type} conversion is not implemented')

def get_graphql_type(self, of_type: slothql.BaseType) -> GraphQLType:
if of_type._meta.name in self:
return self[of_type._meta.name]
elif isinstance(of_type, scalars.ScalarType):
graphql_type = self.get_scalar_type(of_type)
elif isinstance(of_type, slothql.Enum):
return graphql.GraphQLEnumType(
name=of_type._meta.name,
values={
(snake_to_camelcase(name) if self.to_camelcase else name): GraphQLEnumValue(
value=value.value, description=value.description)
for name, value in of_type._meta.enum_values.items()
},
description=of_type._meta.description,
)
cls._type_map[of_type.name] = of_type
of_type.fields = cls.construct_fields(fields)
cls._type_map[of_type.name] = of_type
return of_type

@classmethod
def construct_fields(cls, fields: FieldMap) -> FieldMap:
return {
snake_to_camelcase(name): graphql.GraphQLField(
type=cls.get_graphql_type(field.type),
args={snake_to_camelcase(name): arg for name, arg in field.args.items()},
resolver=field.resolver,
deprecation_reason=field.deprecation_reason,
description=field.description,
elif isinstance(of_type, slothql.Object):
graphql_type = graphql.GraphQLObjectType(
name=of_type._meta.name,
fields={},
interfaces=None,
is_type_of=None,
description=None,
)
for name, field in fields.items()
}
self[graphql_type.name] = graphql_type
graphql_type.fields = {
(snake_to_camelcase(name) if self.to_camelcase else name): graphql.GraphQLField(
type=self.get_type(field),
args={
(snake_to_camelcase(arg_name) if self.to_camelcase else arg_name): self.get_argument(arg_field)
for arg_name, arg_field in field.filter_args.items()
},
resolver=field.resolver,
deprecation_reason=None,
description=field.description,
) for name, field in of_type._meta.fields.items()
}
return graphql_type
else:
raise NotImplementedError(f'Unsupported type {of_type}')
self[graphql_type.name] = graphql_type
return graphql_type

def get_type(self, field: slothql.Field):
graphql_type = self.get_graphql_type(field.of_type)
if field.many:
graphql_type = graphql.GraphQLList(type=graphql_type)
return graphql_type

def get_argument(self, field) -> graphql.GraphQLArgument:
return graphql.GraphQLArgument(
type=self.get_input_type(field.of_type),
)

def get_input_type(self, of_type):
return self.get_scalar_type(of_type)


class Schema(graphql.GraphQLSchema):
def __init__(self, query: LazyType, mutation=None, subscription=None, directives=None, types=None,
auto_camelcase: bool = False):
query = resolve_lazy_type(query)._type

type_map = TypeMap(resolve_lazy_type(query))
graphql_type_map = ProxyTypeMap(type_map, to_camelcase=auto_camelcase)
query = query and graphql_type_map[resolve_lazy_type(query)._meta.name]
mutation = None
assert isinstance(query, graphql.GraphQLObjectType), f'Schema query must be Object Type but got: {query}.'
if mutation:
assert isinstance(mutation, graphql.GraphQLObjectType), \
f'Schema mutation must be Object Type but got: {mutation}.'
assert mutation is None or isinstance(mutation, graphql.GraphQLObjectType), \
f'Schema mutation must be Object Type but got: {mutation}.'

if subscription:
assert isinstance(subscription, graphql.GraphQLObjectType), \
f'Schema subscription must be Object Type but got: {subscription}.'
assert subscription is None or isinstance(subscription, graphql.GraphQLObjectType), \
f'Schema subscription must be Object Type but got: {subscription}.'

if types:
assert isinstance(types, collections.Iterable), \
f'Schema types must be iterable if provided but got: {types}.'
assert types is None or isinstance(types, collections.Iterable), \
f'Schema types must be iterable if provided but got: {types}.'

self._query = query
self._mutation = mutation
Expand All @@ -93,7 +142,4 @@ def __init__(self, query: LazyType, mutation=None, subscription=None, directives
subscription,
IntrospectionSchema,
] + (types or [])
self._type_map = CamelCaseTypeMap(initial_types) if auto_camelcase else GraphQLTypeMap(initial_types)

def get_query_type(self):
return self._type_map[self._query.name]
self._type_map = GraphQLTypeMap(initial_types)
4 changes: 2 additions & 2 deletions slothql/tests/schema.py
Expand Up @@ -54,8 +54,8 @@ class Query(slothql.Object):
== slothql.gql(schema, 'query { stringField fooBar { someWeirdField } }')

# shouldn't modify the actual types
assert {'string_field', 'foo_bar'} == Query()._type.fields.keys() == Query()._type._fields.keys()
assert {'some_weird_field', 'foo_bar'} == FooBar()._type.fields.keys() == FooBar()._type._fields.keys()
assert {'string_field', 'foo_bar'} == Query()._meta.fields.keys()
assert {'some_weird_field', 'foo_bar'} == FooBar()._meta.fields.keys()


def test_camelcase_schema_integration__introspection_query():
Expand Down
4 changes: 3 additions & 1 deletion slothql/types/__init__.py
@@ -1,7 +1,9 @@
from .object import Object
from .base import BaseType
from .enum import Enum, EnumValue
from .object import Object

__all__ = (
'BaseType',
'Object',
'Enum', 'EnumValue',
)
13 changes: 5 additions & 8 deletions slothql/types/base.py
@@ -1,8 +1,6 @@
import inspect
from typing import Union, Type, Callable, Tuple, Iterable

from graphql.type.definition import GraphQLType

from slothql.utils import is_magic_name, get_attr_fields
from slothql.utils.singleton import Singleton

Expand All @@ -15,9 +13,9 @@ def set_defaults(self):
if not hasattr(self, name):
setattr(self, name, None)

def __init__(self, attrs: dict):
def __init__(self, **kwargs):
self.set_defaults()
for name, value in attrs.items():
for name, value in kwargs.items():
try:
setattr(self, name, value)
except AttributeError:
Expand All @@ -30,7 +28,9 @@ def __new__(mcs, name: str, bases: Tuple[type], attrs: dict,
assert 'Meta' not in attrs or inspect.isclass(attrs['Meta']), 'attribute Meta has to be a class'
meta_attrs = get_attr_fields(attrs['Meta']) if 'Meta' in attrs else {}
base_option = mcs.merge_options(*mcs.get_options_bases(bases))
meta = options_class(mcs.merge_options(base_option, mcs.get_option_attrs(name, base_option, attrs, meta_attrs)))
meta = options_class(
**mcs.merge_options(base_option, mcs.get_option_attrs(name, base_option, attrs, meta_attrs)),
)
cls = super().__new__(mcs, name, bases, attrs)
cls._meta = meta
return cls
Expand Down Expand Up @@ -71,9 +71,6 @@ def __new__(cls, *more):
assert not cls._meta.abstract, f'Abstract type {cls.__name__} can not be instantiated'
return super().__new__(cls)

def __init__(self, type_: GraphQLType):
self._type = type_


LazyType = Union[Type[BaseType], BaseType, Callable]

Expand Down
27 changes: 11 additions & 16 deletions slothql/types/enum.py
@@ -1,17 +1,14 @@
import graphql
from typing import Type

from graphql.type import GraphQLEnumValue

from .base import BaseType, TypeMeta, TypeOptions


class EnumOptions(TypeOptions):
__slots__ = 'values',
__slots__ = 'enum_values',

def __init__(self, attrs: dict):
super().__init__(attrs)
assert self.abstract or self.values, f'"{self.name}" is missing valid `Enum` values'
def __init__(self, **kwargs):
super().__init__(**kwargs)
assert self.abstract or self.enum_values, f'"{self.name}" is missing valid `Enum` values'


class EnumMeta(TypeMeta):
Expand All @@ -21,20 +18,18 @@ def __new__(mcs, *args, options_class: Type[EnumOptions] = EnumOptions, **kwargs
@classmethod
def get_option_attrs(mcs, name: str, base_attrs: dict, attrs: dict, meta_attrs: dict):
return {**super().get_option_attrs(name, base_attrs, attrs, meta_attrs), **{
'values': {field_name: field for field_name, field in attrs.items() if isinstance(field, EnumValue)},
'enum_values': {field_name: field for field_name, field in attrs.items() if isinstance(field, EnumValue)},
}}


class EnumValue(GraphQLEnumValue):
pass
class EnumValue:
__slots__ = 'value', 'description'

def __init__(self, value, description: str = None):
self.value = value
self.description = description


class Enum(BaseType, metaclass=EnumMeta):
class Meta:
abstract = True

def __init__(self):
super().__init__(type_=graphql.GraphQLEnumType(
name=self._meta.name,
values=self._meta.values,
))

0 comments on commit b2dfe9d

Please sign in to comment.