├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── epoxy ├── __init__.py ├── bases │ ├── __init__.py │ ├── class_type_creator.py │ ├── input_type.py │ ├── mutation.py │ ├── object_type.py │ └── scalar.py ├── contrib │ ├── __init__.py │ └── relay │ │ ├── __init__.py │ │ ├── connections │ │ ├── __init__.py │ │ ├── cursor.py │ │ └── sorted_collection.py │ │ ├── data_source │ │ ├── __init__.py │ │ ├── base.py │ │ └── memory.py │ │ ├── metaclasses │ │ ├── __init__.py │ │ └── mutation.py │ │ ├── mixin.py │ │ └── utils.py ├── metaclasses │ ├── __init__.py │ ├── input_type.py │ ├── interface.py │ ├── mutation.py │ ├── object_type.py │ ├── scalar.py │ └── union.py ├── registry.py ├── types │ ├── __init__.py │ ├── argument.py │ └── field.py └── utils │ ├── __init__.py │ ├── enum_to_graphql_enum.py │ ├── first_of.py │ ├── gen_id.py │ ├── get_declared_fields.py │ ├── make_default_resolver.py │ ├── maybe_callable.py │ ├── maybe_t.py │ ├── method_dispatch.py │ ├── no_implementation_registration.py │ ├── thunk.py │ ├── to_camel_case.py │ ├── weak_ref_holder.py │ ├── wrap_resolver_translating_arguments.py │ └── yank_potential_fields.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_arguments.py ├── test_can_be.py ├── test_declarative_definition.py ├── test_graphql_object_interoperability.py ├── test_input_type.py ├── test_interfaces.py ├── test_mutation.py ├── test_object_type_as_data.py ├── test_register_enum.py ├── test_register_reserved_name.py ├── test_registration.py ├── test_relay │ ├── __init__.py │ ├── test_relay_connections.py │ ├── test_relay_mutation.py │ └── test_relay_node.py ├── test_resolver_execution.py ├── test_scalar.py ├── test_schema_creation.py ├── test_starwars │ ├── __init__.py │ ├── data.py │ ├── schema.py │ └── test_query.py ├── test_subscription.py └── test_unions.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea 3 | .cache 4 | .tox 5 | *.egg-info 6 | .coverage -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | matrix: 4 | include: 5 | - python: "pypy" 6 | env: TOX_ENV=pypy 7 | - python: "2.7" 8 | env: TOX_ENV=py27 9 | - python: "3.3" 10 | env: TOX_ENV=py33 11 | - python: "3.4" 12 | env: TOX_ENV=py34 13 | - python: "3.5" 14 | env: TOX_ENV=py35,import-order,flake8 15 | 16 | cache: 17 | directories: 18 | - $HOME/.cache/pip 19 | - $TRAVIS_BUILD_DIR/.tox 20 | 21 | install: 22 | - pip install tox coveralls 23 | 24 | script: 25 | - tox -e $TOX_ENV -- --cov=epoxy 26 | after_success: 27 | - coveralls -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jacob Heinz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Epoxy [![Build Status](https://travis-ci.org/graphql-python/graphql-epoxy.svg?branch=v0.1a0)](https://travis-ci.org/graphql-python/graphql-epoxy) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphql-epoxy/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphql-epoxy?branch=master) [![PyPI version](https://badge.fury.io/py/graphql-epoxy.svg)](https://badge.fury.io/py/graphql-epoxy) 2 | 3 | Epoxy is a magical tool for rapid development of GraphQL types, schemas, resolvers, mutations quickly & pragmatically. 4 | 5 | * **Minimal Boilerplate**: You can create a GraphQL schema and execute it in less than 5 lines of code. 6 | * **Definition Ordering**: It doesn't matter. Define your objects in any order you want. Epoxy will take care of the rest. 7 | * **Quick**: Once you create your schema, epoxy doesn't get in the way. Your resolvers will be called directly by 8 | `graphql-core` with no additional indirection. 9 | 10 | ## Installation 11 | 12 | Epoxy is available on pypi under the package name `graphql-epoxy`, you can get it by running: 13 | 14 | ```sh 15 | pip install graphql-epoxy 16 | ``` 17 | 18 | ## Usage 19 | 20 | Defining a GraphQL Schema using Epoxy is as simple as creating a `TypeRegistry` and using it to create types for you. 21 | 22 | ```python 23 | 24 | from epoxy import TypeRegistry 25 | R = TypeRegistry() 26 | 27 | class Character(R.Interface): 28 | id = R.ID 29 | name = R.String 30 | friends = R.Character.List 31 | 32 | 33 | class Human(R.Implements.Character): 34 | home_planet = R.String.NonNull 35 | 36 | 37 | class Query(R.ObjectType): 38 | human = R.Human 39 | foo = R.Foo # This is defined below! Ordering doesn't matter! 40 | 41 | def resolve_human(self, obj, args, info): 42 | """This will be used as the description of the field Query.human.""" 43 | return Human(id=5, name='Bob', friends=[Human(id=6, name='Bill')] 44 | 45 | ``` 46 | 47 | You can even have `epoxy` learn about your already defined Python enums. 48 | 49 | ```python 50 | class MoodStatus(enums.Enum): 51 | HAPPY = 1 52 | SAD = 2 53 | MELANCHOLY = 3 54 | 55 | 56 | R(MoodStatus) 57 | 58 | ``` 59 | 60 | And then use it in an ObjectType: 61 | 62 | ```python 63 | class Foo(R.ObjectType): 64 | mood = R.MoodStatus 65 | # or 66 | mood = R.Field(R.MoodStatus, description="Describing the mood of Foo, is sometimes pretty hard.") 67 | 68 | def resolve_mood(self, *args): 69 | return MoodStatus.HAPPY.value 70 | 71 | ``` 72 | 73 | Schema is a `GraphQLSchema` object. You can now use it with graphql: 74 | 75 | ```python 76 | schema = R.schema(R.Query) 77 | 78 | result = graphql(schema, ''' 79 | { 80 | human { 81 | id 82 | name 83 | homePlanet 84 | friends { 85 | name 86 | homePlanet 87 | } 88 | 89 | } 90 | } 91 | ''') 92 | 93 | ``` 94 | 95 | The schema is now defined as: 96 | 97 | ```graphql 98 | 99 | enum MoodStatus { 100 | HAPPY 101 | SAD 102 | MELANCHOLY 103 | } 104 | 105 | interface Character { 106 | id: ID 107 | name: String 108 | friends: [Character] 109 | } 110 | 111 | type Human implements Character { 112 | id: ID 113 | name: String 114 | friends: [Character] 115 | homePlanet: String! 116 | } 117 | 118 | type Foo { 119 | mood: MoodStatus 120 | } 121 | 122 | type Query { 123 | human: Human 124 | foo: Foo 125 | } 126 | ``` 127 | 128 | Notice that `epoxy` converted snake_cased fields to camelCase in the GraphQL Schema. 129 | 130 | ## ObjectTypes become containers 131 | 132 | You can bring your own objects, (like a Django or SQLAlchemy model), or you can use the class you just created: 133 | 134 | ```python 135 | 136 | me = Human(id=2, name='Jake', home_planet='Earth', friends=[Human(id=3, name='Syrus', home_planet='Earth')]) 137 | 138 | print(me) # ]]> 139 | print(me.name) # Jake 140 | ``` 141 | 142 | Epoxy will automatically resolve the runtime types of your objects if class that you created from `R.ObjectType`, but 143 | if you want to bring your own `Human` (i.e. a model.Model from Django), just tell Epoxy about it! And if you don't want 144 | to, you can just override the `is_type_of` function inside `Human` to something more to your liking. 145 | 146 | ### `my_app/models.py` 147 | ```python 148 | from django.db import models 149 | from my_app.graphql import R 150 | 151 | @R.Human.CanBe 152 | class RealHumanBean(models.Model): 153 | """ 154 | And a real hero. 155 | """ 156 | name = models.CharField(name=Name) 157 | 158 | 159 | # Or if you don't want to use the decorator: 160 | R.Human.CanBe(Human) 161 | ``` 162 | 163 | 164 | ## Mutations 165 | 166 | Epoxy also supports defining mutations. Making a Mutation a Relay mutation is as simple as changing `R.Mutation` to 167 | `Relay.Mutation`. 168 | 169 | 170 | ```python 171 | 172 | class AddFriend(R.Mutation): 173 | class Input: 174 | human_to_add = R.ID.NonNull 175 | 176 | class Output: 177 | new_friends_list = R.Human.List 178 | 179 | @R.resolve_with_args 180 | def resolve(self, obj, human_to_add): 181 | obj.add_friend(human_to_add) 182 | return self.Output(new_friends_list=obj.friends) 183 | 184 | 185 | schema = R.schema(R.Query, R.Mutations) 186 | 187 | ``` 188 | 189 | You can then execute the query: 190 | 191 | 192 | ```graphql 193 | mutation AddFriend { 194 | addFriend(input: {humanToAdd: 6}) { 195 | newFriendsList { 196 | id 197 | name 198 | homePlanet 199 | } 200 | } 201 | } 202 | ``` 203 | 204 | ## Defining custom scalar types: 205 | 206 | 207 | ```python 208 | class DateTime(R.Scalar): 209 | @staticmethod 210 | def serialize(dt): 211 | return dt.isoformat() 212 | 213 | @staticmethod 214 | def parse_literal(node): 215 | if isinstance(node, ast.StringValue): 216 | return datetime.datetime.strptime(node.value, "%Y-%m-%dT%H:%M:%S.%f") 217 | 218 | @staticmethod 219 | def parse_value(value): 220 | return datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f") 221 | 222 | ``` 223 | 224 | ## Defining input types: 225 | 226 | ```python 227 | class SimpleInput(R.InputType): 228 | a = R.Int 229 | b = R.Int 230 | some_underscore = R.String 231 | some_from_field = R.String(default_value='Hello World') 232 | 233 | ``` 234 | 235 | ## Defining an Enum (using `enum` module) 236 | 237 | ```python 238 | 239 | from enum import Enum 240 | 241 | @R 242 | class MyEnum(Enum): 243 | FOO = 1 244 | BAR = 2 245 | BAZ = 3 246 | 247 | ``` 248 | 249 | ### Starwars?! 250 | Use the force, check out how we've defined the 251 | [schema](https://github.com/graphql-python/graphql-epoxy/blob/master/tests/test_starwars/schema.py) 252 | for the starwars tests, and compare them to the reference implementation's 253 | [schema](https://github.com/graphql/graphql-js/blob/master/src/__tests__/starWarsSchema.js). 254 | 255 | 256 | ## Relay Support 257 | 258 | At this point, Epoxy has rudimentary `relay` support. Enable support for `Relay` by mixing in the `RelayMixin` using 259 | `TypeResolver.Mixin`. 260 | 261 | ```python 262 | from epoxy.contrib.relay import RelayMixin 263 | from epoxy.contrib.relay.data_source.memory import InMemoryDataSource 264 | 265 | # Epoxy provides an "in memory" data source, that implements `epoxy.contrib.relay.data_source.BaseDataSource`, 266 | # which can be used to easily create a mock data source. In practice, you'd implement your own data source. 267 | data_source = InMemoryDataSource() 268 | 269 | R = TypeRegistry() 270 | Relay = R.Mixin(RelayMixin, data_source) 271 | ``` 272 | 273 | ### Node Definition 274 | Once `RelayMixin` has been mixed into the Registry, things can subclass `Node` automatically! 275 | 276 | ```python 277 | 278 | class Pet(R.Implements[Relay.Node]): 279 | name = R.String 280 | 281 | ``` 282 | 283 | ### Connection Definition & `NodeField` 284 | Connections can be defined upon any object type. Here we'll make a `Query` root node that provides a connection 285 | to a list of pets & a node field to resolve an indivudal node. 286 | 287 | ```python 288 | 289 | class Query(R.ObjectType): 290 | pets = Relay.Connection('Pet', R.Pet) # The duplicate 'Pet' definition is just temporary and will be removed. 291 | node = Relay.NodeField 292 | 293 | ``` 294 | 295 | ### Mutations 296 | ```python 297 | class SimpleAddition(Relay.Mutation): 298 | class Input: 299 | a = R.Int 300 | b = R.Int 301 | 302 | class Output: 303 | sum = R.Int 304 | 305 | def execute(self, obj, input, info): 306 | return self.Output(sum=input.a + input.b) 307 | 308 | ``` 309 | 310 | ### Adding some data! 311 | Let's add some pets to the `data_source` and query them! 312 | 313 | ```python 314 | 315 | # Schema has to be defined so that all thunks are resolved before we can use `Pet` as a container. 316 | Schema = R.Schema(R.Query) 317 | 318 | pet_names = ["Max", "Buddy", "Charlie", "Jack", "Cooper", "Rocky"] 319 | 320 | for i, pet_name in enumerate(pet_names, 1): 321 | data_source.add(Pet(id=i, name=pet_name)) 322 | 323 | ``` 324 | 325 | 326 | ### Running Relay Connection Query: 327 | 328 | ```python 329 | 330 | result = graphql(Schema, ''' 331 | { 332 | pets(first: 5) { 333 | edges { 334 | node { 335 | id 336 | name 337 | } 338 | cursor 339 | } 340 | pageInfo { 341 | hasPreviousPage 342 | hasNextPage 343 | startCursor 344 | endCursor 345 | } 346 | } 347 | node(id: "UGV0OjU=") { 348 | id 349 | ... on Pet { 350 | name 351 | } 352 | } 353 | } 354 | ''') 355 | ``` 356 | -------------------------------------------------------------------------------- /epoxy/__init__.py: -------------------------------------------------------------------------------- 1 | from .registry import TypeRegistry 2 | 3 | __all__ = ['TypeRegistry'] 4 | -------------------------------------------------------------------------------- /epoxy/bases/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'jake' 2 | -------------------------------------------------------------------------------- /epoxy/bases/class_type_creator.py: -------------------------------------------------------------------------------- 1 | from ..utils.thunk import ResolveThunk, ThunkList 2 | 3 | 4 | class ClassTypeCreator(object): 5 | def __init__(self, registry, class_type_creator): 6 | self._registry = registry 7 | self._class_type_creator = class_type_creator 8 | 9 | def __getattr__(self, item): 10 | return self[item] 11 | 12 | def __getitem__(self, item): 13 | if isinstance(item, tuple): 14 | type_thunk = ThunkList([ResolveThunk(self._registry._resolve_type, i) for i in item]) 15 | 16 | else: 17 | type_thunk = ThunkList([ResolveThunk(self._registry._resolve_type, item)]) 18 | 19 | return self._class_type_creator(type_thunk) 20 | -------------------------------------------------------------------------------- /epoxy/bases/input_type.py: -------------------------------------------------------------------------------- 1 | class InputTypeBase(object): 2 | T = None 3 | _field_attr_map = None 4 | 5 | def __init__(self, arg_value=None): 6 | if arg_value is None: 7 | return 8 | 9 | if self._field_attr_map is None: 10 | raise RuntimeError("You cannot construct type {} until it is used in a created Schema.".format( 11 | self.T 12 | )) 13 | 14 | for attr_name, (field_name, field) in self._field_attr_map.items(): 15 | if field_name in arg_value: 16 | setattr(self, attr_name, arg_value[field_name]) 17 | 18 | else: 19 | setattr(self, attr_name, field.default_value) 20 | 21 | def __repr__(self): 22 | if self._field_attr_map is None: 23 | return '<{}>'.format(self.T) 24 | 25 | return '<{} {}>'.format( 26 | self.T, 27 | ' '.join('{}={!r}'.format(field_name, getattr(self, field_name)) 28 | for field_name in self._field_attr_map.keys()) 29 | ) 30 | -------------------------------------------------------------------------------- /epoxy/bases/mutation.py: -------------------------------------------------------------------------------- 1 | class MutationBase(object): 2 | def __init__(self): 3 | pass 4 | -------------------------------------------------------------------------------- /epoxy/bases/object_type.py: -------------------------------------------------------------------------------- 1 | class ObjectTypeBase(object): 2 | T = None 3 | _field_attr_map = None 4 | 5 | def __init__(self, **kwargs): 6 | field_map_init = kwargs.pop('__field_map_init', False) 7 | if field_map_init: 8 | return 9 | 10 | if self._field_attr_map is None: 11 | raise RuntimeError("You cannot construct type {} until it is used in a created Schema.".format( 12 | self.T 13 | )) 14 | 15 | # Todo: Maybe some type checking? Probably not tho. 16 | for field_name in self._field_attr_map.keys(): 17 | if field_name in kwargs: 18 | setattr(self, field_name, kwargs.pop(field_name)) 19 | 20 | else: 21 | setattr(self, field_name, None) 22 | 23 | if kwargs: 24 | raise TypeError('Type {} received unexpected keyword argument(s): {}.'.format( 25 | self.T, 26 | ', '.join(kwargs.keys()) 27 | )) 28 | 29 | def __repr__(self): 30 | if self._field_attr_map is None: 31 | return '<{}>'.format(self.T) 32 | 33 | return '<{} {}>'.format( 34 | self.T, 35 | ' '.join('{}={!r}'.format(field_name, getattr(self, field_name)) 36 | for field_name in self._field_attr_map.keys()) 37 | ) 38 | -------------------------------------------------------------------------------- /epoxy/bases/scalar.py: -------------------------------------------------------------------------------- 1 | class ScalarBase(object): 2 | pass 3 | -------------------------------------------------------------------------------- /epoxy/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GraphQL-python-archive/graphql-epoxy/17e67d96f503758273e7bc2f2baa6ba925052c92/epoxy/contrib/__init__.py -------------------------------------------------------------------------------- /epoxy/contrib/relay/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'jake' 2 | 3 | from .mixin import RelayMixin 4 | 5 | __all__ = ['RelayMixin'] 6 | -------------------------------------------------------------------------------- /epoxy/contrib/relay/connections/__init__.py: -------------------------------------------------------------------------------- 1 | from graphql.core.type import GraphQLInt, GraphQLString 2 | 3 | from ....types.argument import Argument 4 | 5 | __author__ = 'jake' 6 | 7 | connection_args = { 8 | 'before': Argument(GraphQLString), 9 | 'after': Argument(GraphQLString), 10 | 'first': Argument(GraphQLInt), 11 | 'last': Argument(GraphQLInt), 12 | } 13 | -------------------------------------------------------------------------------- /epoxy/contrib/relay/connections/cursor.py: -------------------------------------------------------------------------------- 1 | from ..utils import base64, unbase64 2 | 3 | 4 | class CursorFactory(object): 5 | def __init__(self, prefix): 6 | self.prefix = prefix 7 | self.cursor_type = int 8 | self.max_cursor_length = 10 9 | 10 | def from_offset(self, offset): 11 | """ 12 | Creates the cursor string from an offset. 13 | """ 14 | return base64(self.prefix + str(offset)) 15 | 16 | def to_offset(self, cursor): 17 | """ 18 | Rederives the offset from the cursor string. 19 | """ 20 | try: 21 | return self.cursor_type(unbase64(cursor)[len(self.prefix):len(self.prefix) + self.max_cursor_length]) 22 | except: 23 | return None 24 | 25 | def get_offset(self, cursor, default_offset=0): 26 | """ 27 | Given an optional cursor and a default offset, returns the offset 28 | to use; if the cursor contains a valid offset, that will be used, 29 | otherwise it will be the default. 30 | """ 31 | if cursor is None: 32 | return default_offset 33 | 34 | offset = self.to_offset(cursor) 35 | try: 36 | return self.cursor_type(offset) 37 | except: 38 | return default_offset 39 | -------------------------------------------------------------------------------- /epoxy/contrib/relay/connections/sorted_collection.py: -------------------------------------------------------------------------------- 1 | from bisect import bisect_left, bisect_right 2 | from .cursor import CursorFactory 3 | 4 | cursor = CursorFactory('sc:') 5 | 6 | 7 | class SortedCollection(object): 8 | def __init__(self, key=None): 9 | self._given_key = key 10 | key = (lambda x: x) if key is None else key 11 | self._keys = [] 12 | self._items = [] 13 | self._key = key 14 | 15 | def clear(self): 16 | self._keys = [] 17 | self._items = [] 18 | 19 | def copy(self): 20 | cls = self.__class__(key=self._key) 21 | cls._items = self._items[:] 22 | cls._keys = self._keys[:] 23 | 24 | def __len__(self): 25 | return len(self._items) 26 | 27 | def __getitem__(self, i): 28 | return self._items[i] 29 | 30 | def __iter__(self): 31 | return iter(self._items) 32 | 33 | def __reversed__(self): 34 | return reversed(self._items) 35 | 36 | def __repr__(self): 37 | return '%s(%r, key=%s)' % ( 38 | self.__class__.__name__, 39 | self._items, 40 | getattr(self._given_key, '__name__', repr(self._given_key)) 41 | ) 42 | 43 | def __reduce__(self): 44 | return self.__class__, (self._items, self._given_key) 45 | 46 | def __contains__(self, item): 47 | k = self._key(item) 48 | i = bisect_left(self._keys, k) 49 | j = bisect_right(self._keys, k) 50 | return item in self._items[i:j] 51 | 52 | def index(self, item): 53 | """Find the position of an item. Raise ValueError if not found.'""" 54 | k = self._key(item) 55 | i = bisect_left(self._keys, k) 56 | j = bisect_right(self._keys, k) 57 | return self._items[i:j].index(item) + i 58 | 59 | def count(self, item): 60 | """Return number of occurrences of item'""" 61 | k = self._key(item) 62 | i = bisect_left(self._keys, k) 63 | j = bisect_right(self._keys, k) 64 | return self._items[i:j].count(item) 65 | 66 | def insert(self, item): 67 | """Insert a new item. If equal keys are found, add to the left'""" 68 | k = self._key(item) 69 | i = bisect_left(self._keys, k) 70 | if i != len(self) and self._keys[i] == k: 71 | raise ValueError(u'An item with the same key {} already exists in this collection.'.format(k)) 72 | 73 | self._keys.insert(i, k) 74 | self._items.insert(i, item) 75 | 76 | def remove(self, item): 77 | """Remove first occurrence of item. Raise ValueError if not found'""" 78 | i = self.index(item) 79 | del self._keys[i] 80 | del self._items[i] 81 | 82 | def bisect_left(self, k): 83 | return bisect_left(self._keys, k) 84 | 85 | def bisect_right(self, k): 86 | return bisect_right(self._keys, k) 87 | 88 | @staticmethod 89 | def empty_connection(relay, type_name): 90 | Connection, Edge = relay.get_connection_and_edge_types(type_name) 91 | 92 | return Connection( 93 | edges=[], 94 | page_info=relay.PageInfo( 95 | start_cursor=None, 96 | end_cursor=None, 97 | has_previous_page=False, 98 | has_next_page=False, 99 | ) 100 | ) 101 | 102 | def get_edge(self, relay, type_name, node): 103 | Connection, Edge = relay.get_connection_and_edge_types(type_name) 104 | return Edge(node=node, cursor=cursor.from_offset(self._key(node))) 105 | 106 | def get_connection(self, relay, type_name, args): 107 | Connection, Edge = relay.get_connection_and_edge_types(type_name) 108 | before = args.get('before') 109 | after = args.get('after') 110 | first = args.get('first') 111 | last = args.get('last') 112 | 113 | count = len(self) 114 | if not count: 115 | return self.empty_connection(relay, type_name) 116 | 117 | begin_key = cursor.get_offset(after, None) 118 | end_key = cursor.get_offset(before, None) 119 | 120 | lower_bound = begin = self.bisect_left(begin_key) + 1 if begin_key else 0 121 | upper_bound = end = self.bisect_right(end_key) - 1 if end_key else count 122 | 123 | if upper_bound < count and self._keys[upper_bound] != end_key: 124 | upper_bound = end = count 125 | 126 | if first is not None: 127 | end = min(begin + first, end) 128 | if last is not None: 129 | begin = max(end - last, begin) 130 | 131 | sliced_data = self._items[begin:end] 132 | 133 | edges = [Edge(node=node, cursor=cursor.from_offset(self._key(node))) for node in sliced_data] 134 | first_edge = edges[0] if edges else None 135 | last_edge = edges[-1] if edges else None 136 | 137 | return Connection( 138 | edges=edges, 139 | page_info=relay.PageInfo( 140 | start_cursor=first_edge and first_edge.cursor, 141 | end_cursor=last_edge and last_edge.cursor, 142 | has_previous_page=begin > lower_bound, 143 | has_next_page=end < upper_bound 144 | ) 145 | ) 146 | -------------------------------------------------------------------------------- /epoxy/contrib/relay/data_source/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'jake' 2 | -------------------------------------------------------------------------------- /epoxy/contrib/relay/data_source/base.py: -------------------------------------------------------------------------------- 1 | class BaseDataSource(object): 2 | def fetch_node(self, object_type, id, resolve_info): 3 | raise NotImplementedError('Must implement fetch_node to resolve node by ID.') 4 | 5 | def make_connection_resolver(self, relay, object_type_thunk): 6 | raise NotImplementedError('Must implement make_connection_resolver so that RelayMixin can automatically ' 7 | 'create connection resolvers') 8 | -------------------------------------------------------------------------------- /epoxy/contrib/relay/data_source/memory.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from operator import attrgetter 3 | from six import text_type 4 | 5 | from ..connections.sorted_collection import SortedCollection 6 | from .base import BaseDataSource 7 | 8 | 9 | class InMemoryDataSource(BaseDataSource): 10 | def __init__(self): 11 | self.objects_by_type_and_id = defaultdict(dict) 12 | self.objects_by_type = defaultdict(lambda: SortedCollection(key=attrgetter('id'))) 13 | 14 | def add(self, obj): 15 | self.objects_by_type_and_id[obj.T][text_type(obj.id)] = obj 16 | self.objects_by_type[obj.T].insert(obj) 17 | 18 | def remove(self, obj): 19 | del self.objects_by_type_and_id[obj.T][text_type(obj.id)] 20 | self.objects_by_type[obj.T].remove(obj) 21 | 22 | def get_edge(self, relay, obj): 23 | return self.objects_by_type[obj.T].get_edge(relay, obj.T.name, obj) 24 | 25 | def fetch_node(self, object_type, id, resolve_info): 26 | return self.objects_by_type_and_id[object_type].get(text_type(id)) 27 | 28 | def make_connection_resolver(self, relay, object_type_thunk): 29 | def resolver(obj, args, info): 30 | object_type = relay.R[object_type_thunk]() 31 | return self.objects_by_type[object_type].get_connection(relay, object_type.name, args) 32 | 33 | return resolver 34 | -------------------------------------------------------------------------------- /epoxy/contrib/relay/metaclasses/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GraphQL-python-archive/graphql-epoxy/17e67d96f503758273e7bc2f2baa6ba925052c92/epoxy/contrib/relay/metaclasses/__init__.py -------------------------------------------------------------------------------- /epoxy/contrib/relay/metaclasses/mutation.py: -------------------------------------------------------------------------------- 1 | from ....metaclasses.mutation import MutationMeta 2 | 3 | 4 | class RelayMutationMeta(MutationMeta): 5 | @staticmethod 6 | def _process_input_attrs(registry, input_attrs): 7 | input_attrs['client_mutation_id'] = registry.String 8 | return input_attrs 9 | 10 | @staticmethod 11 | def _process_output_attrs(registry, output_attrs): 12 | output_attrs['client_mutation_id'] = registry.String 13 | return output_attrs 14 | 15 | @staticmethod 16 | def _process_resolver(resolver, input_class, obj, args, info): 17 | input_obj = input_class(args.get('input')) 18 | result = resolver(obj, input_obj, info) 19 | result.client_mutation_id = input_obj.client_mutation_id 20 | return result 21 | -------------------------------------------------------------------------------- /epoxy/contrib/relay/mixin.py: -------------------------------------------------------------------------------- 1 | from graphql.core.type.definition import GraphQLObjectType 2 | import six 3 | from ...bases.mutation import MutationBase 4 | from .connections import connection_args 5 | from .metaclasses.mutation import RelayMutationMeta 6 | from .utils import base64, unbase64 7 | 8 | 9 | class RelayMixin(object): 10 | def __init__(self, registry, data_source): 11 | self.R = registry 12 | self.data_source = data_source 13 | self._node_field = None 14 | self._connections = {} 15 | self.Mutation = self._create_mutation_type_class() 16 | 17 | @property 18 | def NodeField(self): 19 | return self.R.Field( 20 | self.R.Node, 21 | description='Fetches an object given its ID', 22 | args={ 23 | 'id': self.R.ID.NonNull(description='The ID of an object') 24 | }, 25 | resolver=lambda obj, args, info: self.fetch_node(args.get('id'), info) 26 | ) 27 | 28 | def get_connection_and_edge_types(self, type_name): 29 | return self._connections[type_name] 30 | 31 | def register_types(self): 32 | R = self.R 33 | 34 | class Node(R.Interface): 35 | id = R.ID.NonNull(description='The id of the object.') 36 | 37 | resolve_id = self._resolve_node_id 38 | 39 | class PageInfo(R.ObjectType): 40 | has_next_page = R.Boolean.NonNull(description='When paginating forwards, are there more items?') 41 | has_previous_page = R.Boolean.NonNull(description='When paginating backwards, are there more items?') 42 | start_cursor = R.String(description='When paginating backwards, the cursor to continue.') 43 | end_cursor = R.String(description='When paginating forwards, the cursor to continue.') 44 | 45 | self.Node = Node 46 | self.PageInfo = PageInfo 47 | 48 | def fetch_node(self, id, info): 49 | object_type_name, object_id = unbase64(id).split(':', 1) 50 | object_type = self.R[object_type_name]() 51 | assert isinstance(object_type, GraphQLObjectType) 52 | return self.data_source.fetch_node(object_type, object_id, info) 53 | 54 | def _resolve_node_id(self, obj, args, info): 55 | return self.node_id_for(obj, info) 56 | 57 | def node_id_for(self, obj, info=None): 58 | object_type = self.Node.T.resolve_type(obj, info) 59 | return base64('%s:%s' % (object_type, obj.id)) 60 | 61 | def connection_definitions(self, name, object_type): 62 | R = self.R 63 | 64 | if name in self._connections: 65 | return self._connections[name] 66 | 67 | class Edge(R.ObjectType): 68 | _name = '{}Edge'.format(name) 69 | node = R[object_type](description='The item at the end of the edge') 70 | cursor = R.String.NonNull(description='A cursor for use in pagination') 71 | 72 | class Connection(R.ObjectType): 73 | _name = '{}Connection'.format(name) 74 | 75 | page_info = R.PageInfo.NonNull 76 | edges = R[Edge].List 77 | 78 | self._connections[name] = Connection, Edge 79 | return Connection, Edge 80 | 81 | def Connection(self, name, object_type, args=None, resolver=None, **kwargs): 82 | args = args or {} 83 | args.update(connection_args) 84 | if not resolver: 85 | resolver = self.data_source.make_connection_resolver(self, object_type) 86 | 87 | field = self.R.Field(self.connection_definitions(name, object_type)[0], args=args, resolver=resolver, **kwargs) 88 | return field 89 | 90 | def _create_mutation_type_class(self): 91 | registry = self.R 92 | 93 | class RelayRegistryMutationMeta(RelayMutationMeta): 94 | @staticmethod 95 | def _register(mutation_name, mutation): 96 | registry._register_mutation(mutation_name, mutation) 97 | 98 | @staticmethod 99 | def _get_registry(): 100 | return registry 101 | 102 | @six.add_metaclass(RelayRegistryMutationMeta) 103 | class Mutation(MutationBase): 104 | abstract = True 105 | 106 | return Mutation 107 | -------------------------------------------------------------------------------- /epoxy/contrib/relay/utils.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode as _unbase64, b64encode as _base64 2 | 3 | try: 4 | str_type = basestring 5 | base64 = _base64 6 | unbase64 = _unbase64 7 | except NameError: 8 | def base64(s): 9 | return _base64(bytes(s, 'utf-8')).decode('utf-8') 10 | 11 | def unbase64(s): 12 | return _unbase64(s).decode('utf-8') 13 | -------------------------------------------------------------------------------- /epoxy/metaclasses/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GraphQL-python-archive/graphql-epoxy/17e67d96f503758273e7bc2f2baa6ba925052c92/epoxy/metaclasses/__init__.py -------------------------------------------------------------------------------- /epoxy/metaclasses/input_type.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from functools import partial 3 | 4 | from graphql.core.type.definition import GraphQLInputObjectType 5 | 6 | from ..types.field import InputField 7 | from ..utils.get_declared_fields import get_declared_fields 8 | from ..utils.weak_ref_holder import WeakRefHolder 9 | from ..utils.yank_potential_fields import yank_potential_fields 10 | 11 | 12 | class InputTypeMeta(type): 13 | def __new__(mcs, name, bases, attrs): 14 | if attrs.pop('abstract', False): 15 | return super(InputTypeMeta, mcs).__new__(mcs, name, bases, attrs) 16 | 17 | name = attrs.pop('_name', name) 18 | class_ref = WeakRefHolder() 19 | declared_fields = get_declared_fields(name, yank_potential_fields(attrs, bases, InputField), InputField) 20 | interface = GraphQLInputObjectType( 21 | name, 22 | fields=partial(mcs._build_field_map, class_ref, declared_fields), 23 | description=attrs.get('__doc__'), 24 | ) 25 | 26 | mcs._register(interface) 27 | cls = super(InputTypeMeta, mcs).__new__(mcs, name, bases, attrs) 28 | cls.T = interface 29 | cls._registry = mcs._get_registry() 30 | class_ref.set(cls) 31 | 32 | return cls 33 | 34 | @staticmethod 35 | def _register(object_type): 36 | raise NotImplementedError('_register must be implemented in the sub-metaclass') 37 | 38 | @staticmethod 39 | def _get_registry(): 40 | raise NotImplementedError('_get_registry must be implemented in the sub-metaclass') 41 | 42 | @staticmethod 43 | def _build_field_map(class_ref, fields): 44 | cls = class_ref.get() 45 | if not cls: 46 | return 47 | 48 | registry = cls._registry 49 | field_map = OrderedDict() 50 | field_attr_map = OrderedDict() 51 | 52 | for field_attr_name, field in fields: 53 | graphql_field = field_map[field.name] = field.to_field(registry) 54 | 55 | if field_attr_name in field_attr_map: 56 | del field_attr_map[field_attr_name] 57 | 58 | field_attr_map[field_attr_name] = (field.name, graphql_field) 59 | 60 | cls._field_attr_map = field_attr_map 61 | return field_map 62 | -------------------------------------------------------------------------------- /epoxy/metaclasses/interface.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from functools import partial 3 | from graphql.core.type.definition import GraphQLInterfaceType 4 | from ..utils.get_declared_fields import get_declared_fields 5 | from ..utils.make_default_resolver import make_default_resolver 6 | from ..utils.weak_ref_holder import WeakRefHolder 7 | from ..utils.yank_potential_fields import yank_potential_fields 8 | 9 | 10 | class InterfaceMeta(type): 11 | def __new__(mcs, name, bases, attrs): 12 | if attrs.pop('abstract', False): 13 | return super(InterfaceMeta, mcs).__new__(mcs, name, bases, attrs) 14 | 15 | class_ref = WeakRefHolder() 16 | declared_fields = get_declared_fields(name, yank_potential_fields(attrs, bases)) 17 | interface = GraphQLInterfaceType( 18 | name, 19 | fields=partial(mcs._build_field_map, class_ref, declared_fields), 20 | description=attrs.get('__doc__'), 21 | ) 22 | 23 | mcs._register(interface, declared_fields) 24 | cls = super(InterfaceMeta, mcs).__new__(mcs, name, bases, attrs) 25 | cls.T = interface 26 | cls._registry = mcs._get_registry() 27 | class_ref.set(cls) 28 | 29 | return cls 30 | 31 | @staticmethod 32 | def _register(object_type, declared_fields): 33 | raise NotImplementedError('_register must be implemented in the sub-metaclass') 34 | 35 | @staticmethod 36 | def _get_registry(): 37 | raise NotImplementedError('_get_registry must be implemented in the sub-metaclass') 38 | 39 | @staticmethod 40 | def _build_field_map(class_ref, fields): 41 | cls = class_ref.get() 42 | if not cls: 43 | return 44 | 45 | instance = cls() 46 | registry = cls._registry 47 | 48 | field_map = OrderedDict() 49 | 50 | for field_attr_name, field in fields: 51 | interface_resolve_fn = ( 52 | field.resolver or 53 | getattr(instance, 'resolve_{}'.format(field_attr_name), None) 54 | ) 55 | 56 | if interface_resolve_fn: 57 | field._interface_resolver = interface_resolve_fn 58 | 59 | resolve_fn = interface_resolve_fn or make_default_resolver(field_attr_name) 60 | 61 | field_map[field.name] = field.to_field(registry, resolve_fn) 62 | 63 | return field_map 64 | -------------------------------------------------------------------------------- /epoxy/metaclasses/mutation.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from graphql.core.type import GraphQLField, GraphQLNonNull 3 | from graphql.core.type.definition import GraphQLArgument 4 | 5 | 6 | class MutationMeta(type): 7 | def __new__(mcs, name, bases, attrs): 8 | if attrs.pop('abstract', False): 9 | return super(MutationMeta, mcs).__new__(mcs, name, bases, attrs) 10 | 11 | registry = mcs._get_registry() 12 | 13 | input = attrs.pop('Input') 14 | output = attrs.pop('Output') 15 | 16 | assert input and not hasattr(input, 'T'), 'A mutation must define a class named "Input" inside of it that ' \ 17 | 'does not subclass an R.InputType' 18 | assert output and not hasattr(output, 'T'), 'A mutation must define a class named "Output" inside of it that ' \ 19 | 'does not subclass an R.ObjectType' 20 | 21 | input_attrs = mcs._process_input_attrs(registry, dict(vars(input))) 22 | output_attrs = mcs._process_output_attrs(registry, dict(vars(output))) 23 | 24 | Input = type(name + 'Input', (registry.InputType,), input_attrs) 25 | Output = type(name + 'Payload', (registry.ObjectType,), output_attrs) 26 | attrs['Input'] = Input 27 | attrs['Output'] = Output 28 | 29 | cls = super(MutationMeta, mcs).__new__(mcs, name, bases, attrs) 30 | cls._registry = registry 31 | instance = cls() 32 | resolver = getattr(instance, 'execute') 33 | assert resolver and callable(resolver), 'A mutation must define a function named "execute" that will execute ' \ 34 | 'the mutation.' 35 | 36 | mutation_name = name[0].lower() + name[1:] 37 | 38 | mcs._register(mutation_name, registry.with_resolved_types(lambda R: GraphQLField( 39 | type=R[Output], 40 | args={ 41 | 'input': GraphQLArgument(GraphQLNonNull(R[Input])) 42 | }, 43 | resolver=functools.partial(mcs._process_resolver, resolver, Input), 44 | description=attrs.get('__doc__', None) 45 | ))) 46 | 47 | @staticmethod 48 | def _register(mutation_name, mutation): 49 | raise NotImplementedError('_register must be implemented in the sub-metaclass') 50 | 51 | @staticmethod 52 | def _get_registry(): 53 | raise NotImplementedError('_get_registry must be implemented in the sub-metaclass') 54 | 55 | @staticmethod 56 | def _process_input_attrs(registry, input_attrs): 57 | return input_attrs 58 | 59 | @staticmethod 60 | def _process_output_attrs(registry, output_attrs): 61 | return output_attrs 62 | 63 | @staticmethod 64 | def _process_resolver(resolver, input_class, obj, args, info): 65 | return resolver(obj, input_class(args.get('input')), info) 66 | -------------------------------------------------------------------------------- /epoxy/metaclasses/object_type.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from functools import partial 3 | from graphql.core.type import GraphQLObjectType 4 | from ..utils.get_declared_fields import get_declared_fields 5 | from ..utils.make_default_resolver import make_default_resolver 6 | from ..utils.no_implementation_registration import no_implementation_registration 7 | from ..utils.weak_ref_holder import WeakRefHolder 8 | from ..utils.yank_potential_fields import yank_potential_fields 9 | 10 | 11 | class ObjectTypeMeta(type): 12 | def __new__(mcs, name, bases, attrs): 13 | if attrs.pop('abstract', False): 14 | return super(ObjectTypeMeta, mcs).__new__(mcs, name, bases, attrs) 15 | 16 | name = attrs.pop('_name', name) 17 | class_ref = WeakRefHolder() 18 | registry = mcs._get_registry() 19 | 20 | declared_fields = get_declared_fields(name, yank_potential_fields(attrs, bases)) 21 | 22 | with no_implementation_registration(): 23 | object_type = GraphQLObjectType( 24 | name, 25 | fields=partial(mcs._build_field_map, class_ref, declared_fields), 26 | description=attrs.get('__doc__'), 27 | interfaces=mcs._get_interfaces() 28 | ) 29 | 30 | object_type.is_type_of = registry._create_is_type_of(object_type) 31 | 32 | cls = super(ObjectTypeMeta, mcs).__new__(mcs, name, bases, attrs) 33 | mcs._register(object_type, cls) 34 | cls._registry = registry 35 | cls.T = object_type 36 | class_ref.set(cls) 37 | 38 | return cls 39 | 40 | @staticmethod 41 | def _register(object_type, cls): 42 | raise NotImplementedError('_register must be implemented in the sub-metaclass') 43 | 44 | @staticmethod 45 | def _get_registry(): 46 | raise NotImplementedError('_get_registry must be implemented in the sub-metaclass') 47 | 48 | @staticmethod 49 | def _build_field_map(class_ref, declared_fields): 50 | cls = class_ref.get() 51 | if not cls: 52 | return 53 | 54 | instance = cls(__field_map_init=True) 55 | type = cls.T 56 | registry = cls._registry 57 | interfaces = type.get_interfaces() 58 | fields = [] 59 | 60 | known_interface_resolvers = {} 61 | 62 | for interface in interfaces: 63 | interface.get_fields() # This triggers the interface to resolve the field thunks. 64 | fields += registry._get_interface_declared_fields(interface) 65 | 66 | for field_attr_name, field in fields: 67 | if field._interface_resolver and field.name not in known_interface_resolvers: 68 | known_interface_resolvers[field.name] = field._interface_resolver 69 | 70 | fields += declared_fields 71 | field_map = OrderedDict() 72 | field_attr_map = OrderedDict() 73 | 74 | for field_attr_name, field in fields: 75 | resolve_fn = ( 76 | field.resolver or 77 | getattr(instance, 'resolve_{}'.format(field_attr_name), None) or 78 | field._interface_resolver or 79 | known_interface_resolvers.get(field.name) or 80 | make_default_resolver(field_attr_name) 81 | ) 82 | 83 | # In the case where field definitions are duplicated, we are going to use the latest definition. 84 | # We delete, so that when inserted into the OrderedMap again, it will be ordered last, instead 85 | # of in the position of the previous one. 86 | if field.name in field_map: 87 | del field_map[field.name] 88 | 89 | graphql_field = field.to_field(registry, resolve_fn) 90 | field_map[field.name] = graphql_field 91 | 92 | if field_attr_name in field_attr_map: 93 | del field_attr_map[field_attr_name] 94 | 95 | field_attr_map[field_attr_name] = graphql_field 96 | 97 | cls._field_attr_map = field_attr_map 98 | return field_map 99 | 100 | @staticmethod 101 | def _get_interfaces(): 102 | return None 103 | -------------------------------------------------------------------------------- /epoxy/metaclasses/scalar.py: -------------------------------------------------------------------------------- 1 | from graphql.core.type import GraphQLScalarType 2 | 3 | 4 | class ScalarMeta(type): 5 | def __new__(mcs, name, bases, attrs): 6 | if attrs.pop('abstract', False): 7 | return super(ScalarMeta, mcs).__new__(mcs, name, bases, attrs) 8 | 9 | registry = mcs._get_registry() 10 | 11 | cls = super(ScalarMeta, mcs).__new__(mcs, name, bases, attrs) 12 | cls._registry = registry 13 | instance = cls() 14 | serialize = getattr(instance, 'serialize') 15 | parse_literal = getattr(instance, 'parse_literal') 16 | parse_value = getattr(instance, 'parse_value') 17 | scalar = GraphQLScalarType( 18 | name=name, 19 | description=attrs.get('__doc__', None), 20 | serialize=serialize, 21 | parse_value=parse_value, 22 | parse_literal=parse_literal 23 | ) 24 | cls.T = scalar 25 | mcs._register(scalar) 26 | 27 | @staticmethod 28 | def _register(scalar): 29 | raise NotImplementedError('_register must be implemented in the sub-metaclass') 30 | 31 | @staticmethod 32 | def _get_registry(): 33 | raise NotImplementedError('_get_registry must be implemented in the sub-metaclass') 34 | -------------------------------------------------------------------------------- /epoxy/metaclasses/union.py: -------------------------------------------------------------------------------- 1 | from graphql.core.type.definition import GraphQLUnionType 2 | 3 | 4 | class UnionMeta(type): 5 | def __new__(mcs, name, bases, attrs): 6 | if attrs.pop('abstract', False): 7 | return super(UnionMeta, mcs).__new__(mcs, name, bases, attrs) 8 | 9 | union_type = GraphQLUnionType( 10 | name, 11 | types=mcs._get_types(), 12 | description=attrs.get('__doc__'), 13 | ) 14 | mcs._register(union_type) 15 | cls = super(UnionMeta, mcs).__new__(mcs, name, bases, attrs) 16 | cls.T = union_type 17 | cls._registry = mcs._get_registry() 18 | 19 | return cls 20 | 21 | @staticmethod 22 | def _register(union_type): 23 | raise NotImplementedError('_register must be implemented in the sub-metaclass') 24 | 25 | @staticmethod 26 | def _get_registry(): 27 | raise NotImplementedError('_get_registry must be implemented in the sub-metaclass') 28 | 29 | @staticmethod 30 | def _get_types(): 31 | raise NotImplementedError('_get_types must be implemented in the sub-metaclass') 32 | -------------------------------------------------------------------------------- /epoxy/registry.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict, defaultdict 2 | from enum import Enum 3 | from functools import partial 4 | from operator import itemgetter 5 | from graphql.core.type import ( 6 | GraphQLBoolean, 7 | GraphQLEnumType, 8 | GraphQLFloat, 9 | GraphQLID, 10 | GraphQLInputObjectType, 11 | GraphQLInt, 12 | GraphQLInterfaceType, 13 | GraphQLObjectType, 14 | GraphQLScalarType, 15 | GraphQLSchema, 16 | GraphQLString, 17 | GraphQLUnionType 18 | ) 19 | from graphql.core.type.definition import GraphQLType, get_named_type 20 | import six 21 | from .bases.class_type_creator import ClassTypeCreator 22 | from .bases.input_type import InputTypeBase 23 | from .bases.mutation import MutationBase 24 | from .bases.object_type import ObjectTypeBase 25 | from .bases.scalar import ScalarBase 26 | from .metaclasses.input_type import InputTypeMeta 27 | from .metaclasses.interface import InterfaceMeta 28 | from .metaclasses.mutation import MutationMeta 29 | from .metaclasses.object_type import ObjectTypeMeta 30 | from .metaclasses.scalar import ScalarMeta 31 | from .metaclasses.union import UnionMeta 32 | from .types.argument import Argument 33 | from .types.field import Field, InputField 34 | from .utils.enum_to_graphql_enum import enum_to_graphql_enum 35 | from .utils.maybe_t import maybe_t 36 | from .utils.method_dispatch import method_dispatch 37 | from .utils.thunk import AttributeTypeThunk, IdentityTypeThunk, RootTypeThunk, ThunkList, TransformThunkList 38 | 39 | builtin_scalars = [ 40 | GraphQLBoolean, 41 | GraphQLFloat, 42 | GraphQLID, 43 | GraphQLInt, 44 | GraphQLString 45 | ] 46 | 47 | 48 | class TypeRegistry(object): 49 | _reserved_names = frozenset([ 50 | # Types 51 | 'ObjectType', 'InputType', 'Union' 'Interface', 'Implements', 'Scalar', 52 | # Functions 53 | 'Schema', 'Register', 'Mixin', 54 | # Mutations 55 | 'Mutation', 'Mutations', 56 | # Fields 57 | 'Field', 'InputField', 'Argument' 58 | ]) 59 | 60 | Field = Field 61 | InputField = InputField 62 | Argument = Argument 63 | 64 | def __init__(self): 65 | self._registered_types = OrderedDict() 66 | self._added_impl_types = set() 67 | self._interface_declared_fields = {} 68 | self._registered_types_can_be = defaultdict(set) 69 | self._pending_types_can_be = defaultdict(set) 70 | self._proxy = ResolvedRegistryProxy(self) 71 | self._mutations = OrderedDict() 72 | self.ObjectType = self._create_object_type_class() 73 | self.InputType = self._create_input_type_class() 74 | self.Implements = ClassTypeCreator(self, self._create_object_type_class) 75 | self.Union = ClassTypeCreator(self, self._create_union_type_class) 76 | self.Interface = self._create_interface_type_class() 77 | self.Mutation = self._create_mutation_type_class() 78 | self.Scalar = self._create_scalar_type_class() 79 | 80 | for type in builtin_scalars: 81 | self.Register(type) 82 | 83 | @method_dispatch 84 | def Register(self, t): 85 | # Can't use dispatch, as it's not an instance, but a subclass of. 86 | if issubclass(t, Enum): 87 | self.Register(enum_to_graphql_enum(t)) 88 | return t 89 | 90 | raise NotImplementedError('Unable to register {}.'.format(t)) 91 | 92 | @Register.register(GraphQLObjectType) 93 | @Register.register(GraphQLUnionType) 94 | @Register.register(GraphQLEnumType) 95 | @Register.register(GraphQLInterfaceType) 96 | @Register.register(GraphQLInputObjectType) 97 | @Register.register(GraphQLScalarType) 98 | def register_(self, t): 99 | if t.name in self._registered_types and self._registered_types[t.name] is t: 100 | return t 101 | 102 | assert not t.name.startswith('_'), \ 103 | 'Registered type name cannot start with an "_".' 104 | assert t.name not in self._reserved_names, \ 105 | 'You cannot register a type named "{}".'.format(t.name) 106 | assert t.name not in self._registered_types, \ 107 | 'There is already a registered type named "{}".'.format(t.name) 108 | 109 | self._registered_types[t.name] = t 110 | return t 111 | 112 | def _resolve_type(self, item): 113 | if item is None: 114 | return None 115 | 116 | if not isinstance(item, str): 117 | item = maybe_t(item) 118 | assert isinstance(item, GraphQLType), \ 119 | 'Attempted to resolve an item "{}" that is not a GraphQLType'.format(item) 120 | 121 | named_type = get_named_type(item) 122 | known_type = self._registered_types.get(named_type.name) 123 | # Happens when we attempt to resolve an un-registered type. 124 | assert known_type and known_type not in self._reserved_names, \ 125 | 'Attempted to resolve a type "{}" that is not registered with this Registry.'.format(item) 126 | 127 | # Happens when we attempt to resolve a type that is already registered, but isn't the same type. 128 | assert known_type is named_type, \ 129 | 'Attempted to resolve a type "{}" that does not match the already registered type.'.format(item) 130 | 131 | return item 132 | 133 | value = self._registered_types.get(item) 134 | assert value, 'Type "{}" was requested, but was not registered.'.format(item) 135 | return value 136 | 137 | def __getattr__(self, item): 138 | if item.startswith('_'): 139 | raise AttributeError(item) 140 | 141 | return RootTypeThunk(self, self._resolve_type, item) 142 | 143 | def __getitem__(self, item): 144 | if isinstance(item, tuple): 145 | return ThunkList([AttributeTypeThunk(self._resolve_type, i) for i in item]) 146 | 147 | return RootTypeThunk(self, self._resolve_type, item) 148 | 149 | def __call__(self, t): 150 | return self.Register(t) 151 | 152 | def _create_object_type_class(self, interface_thunk=None): 153 | registry = self 154 | 155 | class RegistryObjectTypeMeta(ObjectTypeMeta): 156 | @staticmethod 157 | def _register(object_type, type_class): 158 | registry.Register(object_type) 159 | registry._registered_types_can_be[object_type].add(type_class) 160 | 161 | @staticmethod 162 | def _get_registry(): 163 | return registry 164 | 165 | @staticmethod 166 | def _get_interfaces(): 167 | if interface_thunk is not None: 168 | return TransformThunkList(interface_thunk, get_named_type) 169 | 170 | return None 171 | 172 | @six.add_metaclass(RegistryObjectTypeMeta) 173 | class ObjectType(ObjectTypeBase): 174 | abstract = True 175 | 176 | return ObjectType 177 | 178 | def _create_interface_type_class(self): 179 | registry = self 180 | 181 | class RegistryInterfaceMeta(InterfaceMeta): 182 | @staticmethod 183 | def _register(interface, declared_fields): 184 | registry.Register(interface) 185 | registry._add_interface_declared_fields(interface, declared_fields) 186 | 187 | @staticmethod 188 | def _get_registry(): 189 | return registry 190 | 191 | class Interface(six.with_metaclass(RegistryInterfaceMeta)): 192 | abstract = True 193 | 194 | return Interface 195 | 196 | def _create_union_type_class(self, types_thunk): 197 | registry = self 198 | 199 | class RegistryUnionMeta(UnionMeta): 200 | @staticmethod 201 | def _register(union): 202 | registry.Register(union) 203 | 204 | @staticmethod 205 | def _get_registry(): 206 | return registry 207 | 208 | @staticmethod 209 | def _get_types(): 210 | return TransformThunkList(types_thunk, get_named_type) 211 | 212 | class Union(six.with_metaclass(RegistryUnionMeta)): 213 | abstract = True 214 | 215 | return Union 216 | 217 | def _create_input_type_class(self): 218 | registry = self 219 | 220 | class RegistryInputTypeMeta(InputTypeMeta): 221 | @staticmethod 222 | def _register(input_type): 223 | registry.Register(input_type) 224 | 225 | @staticmethod 226 | def _get_registry(): 227 | return registry 228 | 229 | @six.add_metaclass(RegistryInputTypeMeta) 230 | class InputType(InputTypeBase): 231 | abstract = True 232 | 233 | return InputType 234 | 235 | def _create_scalar_type_class(self): 236 | registry = self 237 | 238 | class RegistryScalarMeta(ScalarMeta): 239 | @staticmethod 240 | def _register(scalar): 241 | registry.Register(scalar) 242 | 243 | @staticmethod 244 | def _get_registry(): 245 | return registry 246 | 247 | @six.add_metaclass(RegistryScalarMeta) 248 | class Scalar(ScalarBase): 249 | abstract = True 250 | 251 | return Scalar 252 | 253 | def _create_mutation_type_class(self): 254 | registry = self 255 | 256 | class RegistryMutationMeta(MutationMeta): 257 | @staticmethod 258 | def _register(mutation_name, mutation): 259 | registry._register_mutation(mutation_name, mutation) 260 | 261 | @staticmethod 262 | def _get_registry(): 263 | return registry 264 | 265 | @six.add_metaclass(RegistryMutationMeta) 266 | class Mutation(MutationBase): 267 | abstract = True 268 | 269 | return Mutation 270 | 271 | def _register_mutation(self, mutation_name, mutation): 272 | assert mutation_name not in self._mutations, \ 273 | 'There is already a registered mutation named "{}".'.format(mutation_name) 274 | 275 | self._mutations[mutation_name] = mutation 276 | 277 | @property 278 | def Mutations(self): 279 | if not self._mutations: 280 | raise TypeError("No mutations have been registered.") 281 | 282 | existing_mutation_type = self._registered_types.get('Mutations') 283 | if existing_mutation_type: 284 | return IdentityTypeThunk(existing_mutation_type) 285 | 286 | mutations = GraphQLObjectType( 287 | name='Mutations', 288 | fields=lambda: OrderedDict([(k, v()) for k, v in sorted(self._mutations.items(), key=itemgetter(0))]) 289 | ) 290 | self._registered_types[mutations.name] = mutations 291 | return IdentityTypeThunk(mutations) 292 | 293 | def _create_is_type_of(self, type): 294 | return partial(self._is_type_of, type) 295 | 296 | def _is_type_of(self, type, obj, info): 297 | return obj.__class__ in self._registered_types_can_be[type] 298 | 299 | def _add_interface_declared_fields(self, interface, attrs): 300 | self._interface_declared_fields[interface] = attrs 301 | 302 | def _get_interface_declared_fields(self, interface): 303 | return self._interface_declared_fields.get(interface, {}) 304 | 305 | def _register_possible_type_for(self, type_name, klass): 306 | type = self._registered_types.get(type_name) 307 | if type: 308 | self._registered_types_can_be[type].add(klass) 309 | 310 | else: 311 | self._pending_types_can_be[type_name].add(klass) 312 | 313 | def _add_impl_to_interfaces(self): 314 | for type in self._registered_types.values(): 315 | if not isinstance(type, GraphQLObjectType): 316 | continue 317 | 318 | if type.name in self._pending_types_can_be: 319 | self._registered_types_can_be[type] |= self._pending_types_can_be.pop(type.name) 320 | 321 | if type in self._added_impl_types: 322 | continue 323 | 324 | self._added_impl_types.add(type) 325 | for interface in type.get_interfaces(): 326 | if type in interface._impls: 327 | continue 328 | 329 | interface._impls.append(type) 330 | 331 | def Schema(self, query, mutation=None, subscription=None): 332 | query = self[query]() 333 | mutation = self[mutation]() 334 | subscription = self[subscription]() 335 | self._add_impl_to_interfaces() 336 | return GraphQLSchema(query=query, mutation=mutation, subscription=subscription) 337 | 338 | def Mixin(self, mixin_cls, *args, **kwargs): 339 | mixin = mixin_cls(self, *args, **kwargs) 340 | mixin.register_types() 341 | return mixin 342 | 343 | def type(self, name): 344 | return self[name]() 345 | 346 | def types(self, *names): 347 | return self[names] 348 | 349 | def with_resolved_types(self, thunk): 350 | assert callable(thunk) 351 | return partial(thunk, self._proxy) 352 | 353 | 354 | class ResolvedRegistryProxy(object): 355 | def __init__(self, registry): 356 | self._registry = registry 357 | 358 | def __getitem__(self, item): 359 | return self._registry[item]() 360 | 361 | def __getattr__(self, item): 362 | if item.startswith('_'): 363 | raise AttributeError(item) 364 | 365 | return self._registry[item]() 366 | 367 | 368 | __all__ = ['TypeRegistry'] 369 | -------------------------------------------------------------------------------- /epoxy/types/__init__.py: -------------------------------------------------------------------------------- 1 | from .argument import Argument 2 | from .field import Field, InputField 3 | 4 | __all__ = [ 5 | 'Argument', 6 | 'Field', 7 | 'InputField' 8 | ] 9 | -------------------------------------------------------------------------------- /epoxy/types/argument.py: -------------------------------------------------------------------------------- 1 | from graphql.core.type import GraphQLArgument 2 | from ..utils.gen_id import gen_id 3 | 4 | 5 | class Argument(object): 6 | def __init__(self, type, description=None, default_value=None, name=None, _counter=None): 7 | self.name = name 8 | self.type = type 9 | self.description = description 10 | self.default_value = default_value 11 | self._counter = _counter or gen_id() 12 | 13 | def to_argument(self, registry): 14 | return GraphQLArgument(registry[self.type](), self.default_value, self.description) 15 | -------------------------------------------------------------------------------- /epoxy/types/field.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from graphql.core.type import GraphQLField, GraphQLInputObjectField 3 | from ..types.argument import Argument 4 | from ..utils.gen_id import gen_id 5 | from ..utils.thunk import TypeThunk 6 | from ..utils.to_camel_case import to_camel_case 7 | from ..utils.wrap_resolver_translating_arguments import wrap_resolver_translating_arguments 8 | 9 | 10 | class Field(object): 11 | def __init__(self, type, description=None, args=None, name=None, resolver=None, _counter=None, 12 | _interface_resolver=None): 13 | self.name = name 14 | self.type = type 15 | self.description = description 16 | self.args = args 17 | self.resolver = resolver 18 | self._interface_resolver = _interface_resolver 19 | self._counter = _counter or gen_id() 20 | 21 | def to_field(self, registry, resolver): 22 | args, arguments_to_original_case = self.get_arguments(registry) 23 | 24 | if arguments_to_original_case: 25 | resolver = wrap_resolver_translating_arguments(resolver, arguments_to_original_case) 26 | 27 | return GraphQLField(registry[self.type](), args=args, resolver=resolver) 28 | 29 | def get_arguments(self, registry): 30 | if not self.args: 31 | return None, None 32 | 33 | arguments = [] 34 | arguments_to_original_case = {} 35 | 36 | for k, argument in self.args.items(): 37 | if isinstance(argument, TypeThunk): 38 | argument = Argument(argument, _counter=argument._counter, **(argument._kwargs or {})) 39 | 40 | elif not isinstance(argument, Argument): 41 | raise ValueError('Unknown argument value type %r' % argument) 42 | 43 | camel_cased_name = to_camel_case(k) 44 | if camel_cased_name in arguments_to_original_case: 45 | raise ValueError( 46 | 'Argument %s already exists as %s' % 47 | (k, arguments_to_original_case[camel_cased_name]) 48 | ) 49 | 50 | arguments_to_original_case[camel_cased_name] = k 51 | arguments.append(( 52 | camel_cased_name, argument 53 | )) 54 | 55 | if not isinstance(self.args, OrderedDict): 56 | arguments.sort( 57 | key=lambda i: i[1]._counter 58 | ) 59 | 60 | # Remove things that wouldn't perform any meaningful translation. 61 | for k, v in list(arguments_to_original_case.items()): 62 | if k == v: 63 | del arguments_to_original_case[k] 64 | 65 | return OrderedDict([(k, v.to_argument(registry)) for k, v in arguments]), arguments_to_original_case 66 | 67 | 68 | class InputField(object): 69 | def __init__(self, type, description=None, default_value=None, name=None, _counter=None): 70 | self.name = name 71 | self.type = type 72 | self.description = description 73 | self.default_value = default_value 74 | self._counter = _counter or gen_id() 75 | 76 | def to_field(self, registry): 77 | return GraphQLInputObjectField(registry[self.type](), default_value=self.default_value, 78 | description=self.description) 79 | -------------------------------------------------------------------------------- /epoxy/utils/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'jake' 2 | -------------------------------------------------------------------------------- /epoxy/utils/enum_to_graphql_enum.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from graphql.core.type import GraphQLEnumType, GraphQLEnumValue 3 | 4 | 5 | def enum_to_graphql_enum(enumeration): 6 | return GraphQLEnumType( 7 | name=enumeration.__name__, 8 | values=OrderedDict([(it.name, GraphQLEnumValue(it.value)) for it in enumeration]), 9 | description=enumeration.__doc__ 10 | ) 11 | -------------------------------------------------------------------------------- /epoxy/utils/first_of.py: -------------------------------------------------------------------------------- 1 | from .maybe_callable import maybe_callable 2 | 3 | 4 | def first_of(*args): 5 | for arg in args: 6 | arg = maybe_callable(arg) 7 | if arg: 8 | return arg 9 | -------------------------------------------------------------------------------- /epoxy/utils/gen_id.py: -------------------------------------------------------------------------------- 1 | _prev_id = 0 2 | 3 | 4 | def gen_id(): 5 | global _prev_id 6 | 7 | _prev_id += 1 8 | next_id = _prev_id 9 | 10 | return next_id 11 | -------------------------------------------------------------------------------- /epoxy/utils/get_declared_fields.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from ..types import Field 4 | from .first_of import first_of 5 | from .maybe_callable import maybe_callable 6 | from .maybe_t import maybe_t 7 | from .thunk import TypeThunk 8 | from .to_camel_case import to_camel_case 9 | 10 | 11 | def get_declared_fields(type_name, attrs, field_class=Field): 12 | fields = [] 13 | 14 | for field_attr_name, obj in list(attrs.items()): 15 | if isinstance(obj, field_class): 16 | field = copy.copy(obj) 17 | field.name = first_of(field.name, to_camel_case(field_attr_name)) 18 | # Bind field.type to the maybe scope. 19 | field.type = (lambda field_type: lambda: maybe_t(maybe_callable(field_type)))(field.type) 20 | fields.append((field_attr_name, field)) 21 | 22 | elif isinstance(obj, TypeThunk): 23 | counter = obj._counter 24 | 25 | field = field_class(obj, name=to_camel_case(field_attr_name), _counter=counter, **(obj._kwargs or {})) 26 | fields.append((field_attr_name, field)) 27 | 28 | fields.sort(key=lambda f: f[1]._counter) 29 | 30 | seen_field_names = set() 31 | for field_attr_name, field in fields: 32 | assert field.name not in seen_field_names, 'Duplicate field definition for name "{}" in type "{}.{}".'.format( 33 | field.name, type_name, field_attr_name 34 | ) 35 | seen_field_names.add(field.name) 36 | 37 | return fields 38 | -------------------------------------------------------------------------------- /epoxy/utils/make_default_resolver.py: -------------------------------------------------------------------------------- 1 | def make_default_resolver(field_attr_name): 2 | def resolver(source, args, info): 3 | property = getattr(source, field_attr_name, None) 4 | if callable(property): 5 | return property() 6 | 7 | return property 8 | 9 | resolver.__name__ = 'resolve_{}'.format(field_attr_name) 10 | return resolver 11 | -------------------------------------------------------------------------------- /epoxy/utils/maybe_callable.py: -------------------------------------------------------------------------------- 1 | def maybe_callable(obj): 2 | if callable(obj) and not hasattr(obj, 'T'): 3 | return obj() 4 | 5 | return obj 6 | -------------------------------------------------------------------------------- /epoxy/utils/maybe_t.py: -------------------------------------------------------------------------------- 1 | from graphql.core.type.definition import GraphQLType 2 | 3 | 4 | def maybe_t(obj): 5 | if isinstance(getattr(obj, 'T', None), GraphQLType): 6 | return obj.T 7 | 8 | return obj 9 | -------------------------------------------------------------------------------- /epoxy/utils/method_dispatch.py: -------------------------------------------------------------------------------- 1 | try: 2 | from functools import singledispatch 3 | except ImportError: 4 | from singledispatch import singledispatch 5 | 6 | from functools import update_wrapper 7 | 8 | 9 | def method_dispatch(func): 10 | dispatcher = singledispatch(func) 11 | 12 | def wrapper(*args, **kw): 13 | return dispatcher.dispatch(args[1].__class__)(*args, **kw) 14 | 15 | wrapper.register = dispatcher.register 16 | update_wrapper(wrapper, func) 17 | return wrapper 18 | -------------------------------------------------------------------------------- /epoxy/utils/no_implementation_registration.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from graphql.core.type import definition 3 | 4 | 5 | @contextmanager 6 | def no_implementation_registration(): 7 | old_definition = definition.add_impl_to_interfaces 8 | definition.add_impl_to_interfaces = lambda type: None 9 | try: 10 | yield 11 | 12 | finally: 13 | definition.add_impl_to_interfaces = old_definition 14 | -------------------------------------------------------------------------------- /epoxy/utils/thunk.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from graphql.core.type.definition import GraphQLList, GraphQLNonNull 3 | from .gen_id import gen_id 4 | from .maybe_callable import maybe_callable 5 | 6 | 7 | def clone_with_kwargs(thunk, kwargs): 8 | old_kwargs = thunk._kwargs 9 | if old_kwargs: 10 | kwargs = old_kwargs.copy().update(kwargs) 11 | 12 | clone = copy.copy(thunk) 13 | clone._kwargs = kwargs 14 | return clone 15 | 16 | 17 | class TypeThunk(object): 18 | _kwargs = None 19 | 20 | def __init__(self): 21 | self._counter = gen_id() 22 | 23 | def __call__(self): 24 | raise NotImplementedError() 25 | 26 | 27 | class ContainerTypeMixin(object): 28 | @property 29 | def NonNull(self): 30 | return AttributeTypeThunk(GraphQLNonNull, self) 31 | 32 | @property 33 | def List(self): 34 | return AttributeTypeThunk(GraphQLList, self) 35 | 36 | 37 | class IdentityTypeThunk(TypeThunk, ContainerTypeMixin): 38 | def __init__(self, item): 39 | super(IdentityTypeThunk, self).__init__() 40 | self.item = item 41 | 42 | def __call__(self, **kwargs): 43 | if kwargs: 44 | return clone_with_kwargs(self, kwargs) 45 | 46 | return self.item 47 | 48 | 49 | class ResolveThunkMixin(object): 50 | _kwargs = None 51 | 52 | def __init__(self, getter, item): 53 | self.getter = getter 54 | self.item = item 55 | 56 | def _resolve(self, item): 57 | if callable(item) and not hasattr(item, 'T'): 58 | return self._resolve(item()) 59 | 60 | return maybe_callable(self.getter(item)) 61 | 62 | def __call__(self, **kwargs): 63 | if kwargs: 64 | return clone_with_kwargs(self, kwargs) 65 | 66 | return self._resolve(self.item) 67 | 68 | 69 | class ResolveThunk(ResolveThunkMixin, TypeThunk): 70 | pass 71 | 72 | 73 | class AttributeTypeThunk(ResolveThunkMixin, ContainerTypeMixin, TypeThunk): 74 | def __init__(self, getter, item): 75 | ResolveThunkMixin.__init__(self, getter, item) 76 | ContainerTypeMixin.__init__(self) 77 | TypeThunk.__init__(self) 78 | 79 | if isinstance(item, TypeThunk): 80 | self._kwargs = item._kwargs 81 | 82 | def __repr__(self): 83 | return ''.format(self.item) 84 | 85 | 86 | class RootTypeThunk(AttributeTypeThunk): 87 | def __init__(self, registry, getter, item): 88 | AttributeTypeThunk.__init__(self, getter, item) 89 | self.registry = registry 90 | 91 | def __call__(self, **kwargs): 92 | if kwargs: 93 | return clone_with_kwargs(self, kwargs) 94 | 95 | return self._resolve(self.item) 96 | 97 | # noinspection PyPep8Naming 98 | def CanBe(self, klass): 99 | self.registry._register_possible_type_for(self.item, klass) 100 | return klass 101 | 102 | 103 | class ThunkList(object): 104 | def __init__(self, items): 105 | self.items = items 106 | 107 | def __call__(self): 108 | return [maybe_callable(item) for item in self.items] 109 | 110 | 111 | class TransformThunkList(object): 112 | def __init__(self, items, transform): 113 | self.items = items 114 | self.transform = transform 115 | 116 | def __call__(self): 117 | return [self.transform(item) for item in maybe_callable(self.items)] 118 | -------------------------------------------------------------------------------- /epoxy/utils/to_camel_case.py: -------------------------------------------------------------------------------- 1 | def to_camel_case(snake_str): 2 | components = snake_str.split('_') 3 | return components[0] + ''.join(x.title() for x in components[1:]) 4 | -------------------------------------------------------------------------------- /epoxy/utils/weak_ref_holder.py: -------------------------------------------------------------------------------- 1 | from weakref import ReferenceType, ref 2 | 3 | 4 | class WeakRefHolder(object): 5 | __slots__ = 'ref', 6 | 7 | def __init__(self, ref=None): 8 | if ref is not None: 9 | self.set(ref) 10 | else: 11 | self.ref = None 12 | 13 | def _delete_ref(self, ref): 14 | if ref is self.ref: 15 | self.ref = None 16 | 17 | def get(self): 18 | if isinstance(self.ref, ReferenceType): 19 | return self.ref() 20 | 21 | def set(self, item): 22 | self.ref = ref(item, self._delete_ref) 23 | -------------------------------------------------------------------------------- /epoxy/utils/wrap_resolver_translating_arguments.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from six import wraps 4 | 5 | 6 | def wrap_resolver_translating_arguments(resolver, arguments_to_original_case): 7 | translate_key = arguments_to_original_case.get 8 | 9 | @wraps(resolver) 10 | def wrapped(obj, args, info): 11 | new_args = OrderedDict() if isinstance(args, OrderedDict) else {} 12 | for k in args: 13 | new_args[translate_key(k, k)] = args[k] 14 | 15 | return resolver(obj, new_args, info) 16 | 17 | return wrapped 18 | -------------------------------------------------------------------------------- /epoxy/utils/yank_potential_fields.py: -------------------------------------------------------------------------------- 1 | from ..types.field import Field 2 | from .thunk import TypeThunk 3 | 4 | 5 | def yank_potential_fields(attrs, bases, field_class=Field): 6 | field_attrs = {} 7 | potential_types = (field_class, TypeThunk) 8 | 9 | for klass in reversed(bases): 10 | for field_attr_name, obj in klass.__dict__.items(): 11 | if field_attr_name == 'T': 12 | continue 13 | 14 | if isinstance(obj, potential_types): 15 | field_attrs[field_attr_name] = obj 16 | 17 | for field_attr_name, obj in list(attrs.items()): 18 | if field_attr_name == 'T': 19 | continue 20 | 21 | if isinstance(obj, potential_types): 22 | field_attrs[field_attr_name] = attrs.pop(field_attr_name) 23 | 24 | return field_attrs 25 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = tests,scripts,setup.py,docs 3 | max-line-length = 160 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import sys 3 | 4 | required_packages = ['graphql-core>=0.4.12'] 5 | 6 | if sys.version_info <= (2, 7, 0): 7 | required_packages.append('enum34>=1.0.4') 8 | 9 | if sys.version_info <= (3, 4, 0): 10 | required_packages.append('singledispatch>=3.4.0') 11 | 12 | setup( 13 | name='graphql-epoxy', 14 | version='0.3.2', 15 | description='GraphQL implementation for Python', 16 | url='https://github.com/graphql-python/graphql-core', 17 | download_url='https://github.com/graphql-python/graphql-core/releases', 18 | author='Jake Heinz', 19 | author_email='me' '@' 'jh.gg', 20 | license='MIT', 21 | classifiers=[ 22 | 'Development Status :: 5 - Production/Stable', 23 | 'Intended Audience :: Developers', 24 | 'Topic :: Software Development :: Libraries', 25 | 'Programming Language :: Python :: 2', 26 | 'Programming Language :: Python :: 2.7', 27 | 'Programming Language :: Python :: 3', 28 | 'Programming Language :: Python :: 3.3', 29 | 'Programming Language :: Python :: 3.4', 30 | 'Programming Language :: Python :: 3.5', 31 | 'Programming Language :: Python :: Implementation :: PyPy', 32 | 'License :: OSI Approved :: MIT License', 33 | ], 34 | 35 | keywords='api graphql protocol rest', 36 | packages=find_packages(exclude=['tests']), 37 | install_requires=required_packages, 38 | tests_require=['pytest>=2.7.3'], 39 | ) 40 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GraphQL-python-archive/graphql-epoxy/17e67d96f503758273e7bc2f2baa6ba925052c92/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_arguments.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from pytest import raises 3 | from graphql.core import graphql 4 | from graphql.core.type import GraphQLString, GraphQLInt, GraphQLID, GraphQLNonNull 5 | from epoxy.registry import TypeRegistry 6 | from epoxy.types.argument import Argument 7 | 8 | make_args = lambda R: { 9 | 'a': R.Int, 10 | 'b_cool': R.String, 11 | 'c': R.ID.NonNull, 12 | 'd': Argument(R.String, default_value="hello world"), 13 | 'z': R.String(default_value="hello again", description="This is a description"), 14 | 'x': R.Int(default_value=7), 15 | 'y': Argument(GraphQLString), 16 | 'q': R.TestInputType, 17 | 'w': Argument(R.TestInputType) 18 | } 19 | 20 | make_ordered_dict_args = lambda R: OrderedDict([ 21 | ('a', R.Int), 22 | ('b_cool', R.String), 23 | ('c', R.ID.NonNull), 24 | ('d', Argument(R.String, default_value="hello world")), 25 | ('z', R.String(default_value="hello again", description="This is a description")), 26 | ('x', R.Int(default_value=7)), 27 | ('y', Argument(GraphQLString)), 28 | ('q', R.TestInputType), 29 | ('w', Argument(R.TestInputType)), 30 | ]) 31 | 32 | 33 | def check_args(test_input_type, args): 34 | expected_keys = ['a', 'bCool', 'c', 'd', 'z', 'x', 'y', 'q', 'w'] 35 | keys = [a.name for a in args] 36 | 37 | assert keys == expected_keys 38 | 39 | a, b, c, d, z, x, y, q, w = args 40 | 41 | assert a.type is GraphQLInt 42 | assert b.type is GraphQLString 43 | assert isinstance(c.type, GraphQLNonNull) 44 | assert c.type.of_type is GraphQLID 45 | assert d.type is GraphQLString 46 | assert d.default_value == "hello world" 47 | assert z.type is GraphQLString 48 | assert z.default_value == "hello again" 49 | assert z.description == "This is a description" 50 | assert x.type is GraphQLInt 51 | assert x.default_value == 7 52 | assert y.type is GraphQLString 53 | assert q.type is test_input_type 54 | assert w.type is test_input_type 55 | 56 | 57 | def test_args_will_magically_order(): 58 | R = TypeRegistry() 59 | 60 | class TestInputType(R.InputType): 61 | a = R.Int 62 | b = R.Int 63 | 64 | class Query(R.ObjectType): 65 | int = R.Int( 66 | args=make_args(R) 67 | ) 68 | int_from_field = R.Field(R.Int, args=make_args(R)) 69 | 70 | query_type = R.Query() 71 | check_args(TestInputType.T, query_type.get_fields()['int'].args) 72 | check_args(TestInputType.T, query_type.get_fields()['intFromField'].args) 73 | 74 | 75 | def test_args_can_also_use_ordered_dict(): 76 | R = TypeRegistry() 77 | 78 | class TestInputType(R.InputType): 79 | a = R.Int 80 | b = R.Int 81 | 82 | class Query(R.ObjectType): 83 | int = R.Int( 84 | args=make_ordered_dict_args(R) 85 | ) 86 | int_from_field = R.Field(R.Int, args=make_ordered_dict_args(R)) 87 | 88 | query_type = R.Query() 89 | check_args(TestInputType.T, query_type.get_fields()['int'].args) 90 | check_args(TestInputType.T, query_type.get_fields()['intFromField'].args) 91 | 92 | 93 | def test_resolved_args_will_be_translated_to_original_casing(): 94 | R = TypeRegistry() 95 | 96 | class Query(R.ObjectType): 97 | argument_keys = R.String.List(args={ 98 | 'foo': R.String, 99 | 'foo_bar': R.String 100 | }) 101 | 102 | def resolve_argument_keys(self, obj, args, info): 103 | return list(sorted(args.keys())) 104 | 105 | Schema = R.Schema(R.Query) 106 | result = graphql(Schema, ''' 107 | { 108 | argumentKeys(foo: "Hello", fooBar: "World") 109 | } 110 | ''') 111 | 112 | assert not result.errors 113 | 114 | assert result.data == { 115 | 'argumentKeys': ['foo', 'foo_bar'] 116 | } 117 | 118 | 119 | def test_will_recognize_casing_conversion_conflicts(): 120 | R = TypeRegistry() 121 | 122 | class Query(R.ObjectType): 123 | argument_keys = R.String.List(args={ 124 | 'foo_bar': R.String, 125 | 'fooBar': R.String 126 | }) 127 | 128 | def resolve_argument_keys(self, obj, args, info): 129 | return list(sorted(args.keys())) 130 | 131 | with raises(ValueError) as excinfo: 132 | Schema = R.Schema(R.Query) 133 | 134 | assert str(excinfo.value) in ( 135 | 'Argument foo_bar already exists as fooBar', 136 | 'Argument fooBar already exists as foo_bar', 137 | ) 138 | -------------------------------------------------------------------------------- /tests/test_can_be.py: -------------------------------------------------------------------------------- 1 | from graphql.core import graphql 2 | from epoxy import TypeRegistry 3 | 4 | 5 | def test_can_be(): 6 | R = TypeRegistry() 7 | 8 | @R.Cat.CanBe 9 | class MyCat(object): 10 | def __init__(self, name, meow): 11 | self.name = name 12 | self.meow = meow 13 | 14 | class Pet(R.Interface): 15 | name = R.String 16 | 17 | class Dog(R.Implements.Pet): 18 | bark = R.String 19 | 20 | class Cat(R.Implements.Pet): 21 | meow = R.String 22 | 23 | class Bird(R.Implements.Pet): 24 | tweet = R.String 25 | 26 | class Query(R.ObjectType): 27 | pets = R.Pet.List 28 | 29 | @R.Dog.CanBe 30 | class MyDog(object): 31 | def __init__(self, name, bark): 32 | self.name = name 33 | self.bark = bark 34 | 35 | schema = R.Schema(Query) 36 | 37 | @R.Bird.CanBe 38 | class MyBird(object): 39 | def __init__(self, name, tweet): 40 | self.name = name 41 | self.tweet = tweet 42 | 43 | data = Query(pets=[ 44 | MyDog(name='Clifford', bark='Really big bark, because it\'s a really big dog.'), 45 | MyCat(name='Garfield', meow='Lasagna'), 46 | MyBird(name='Tweetie', tweet='#yolo'), 47 | 48 | Dog(name='OTClifford', bark='Really big bark, because it\'s a really big dog.'), 49 | Cat(name='OTGarfield', meow='Lasagna'), 50 | Bird(name='OTTweetie', tweet='#yolo'), 51 | ]) 52 | 53 | result = graphql(schema, ''' 54 | { 55 | pets { 56 | name 57 | __typename 58 | ... on Dog { 59 | bark 60 | } 61 | 62 | ... on Cat { 63 | meow 64 | } 65 | 66 | ... on Bird { 67 | tweet 68 | } 69 | } 70 | } 71 | 72 | ''', data) 73 | assert not result.errors 74 | assert result.data == { 75 | 'pets': [ 76 | {'__typename': 'Dog', 'bark': "Really big bark, because it's a really big dog.", 'name': 'Clifford'}, 77 | {'__typename': 'Cat', 'meow': 'Lasagna', 'name': 'Garfield'}, 78 | {'__typename': 'Bird', 'tweet': '#yolo', 'name': 'Tweetie'}, 79 | {'__typename': 'Dog', 'bark': "Really big bark, because it's a really big dog.", 'name': 'OTClifford'}, 80 | {'__typename': 'Cat', 'meow': 'Lasagna', 'name': 'OTGarfield'}, 81 | {'__typename': 'Bird', 'tweet': '#yolo', 'name': 'OTTweetie'}, 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /tests/test_declarative_definition.py: -------------------------------------------------------------------------------- 1 | from graphql.core.type.definition import GraphQLObjectType, GraphQLNonNull, GraphQLList, GraphQLField 2 | from graphql.core.type.scalars import GraphQLString 3 | from epoxy.registry import TypeRegistry 4 | from pytest import raises 5 | 6 | 7 | def check_dog(R, Dog): 8 | assert isinstance(Dog.T, GraphQLObjectType) 9 | assert R.type('Dog') is Dog.T 10 | 11 | fields = Dog.T.get_fields() 12 | assert list(fields.keys()) == ['name'] 13 | assert fields['name'].type == GraphQLString 14 | assert fields['name'].name == 'name' 15 | 16 | def test_register_single_type(): 17 | R = TypeRegistry() 18 | 19 | class Dog(R.ObjectType): 20 | name = R.Field(R.String) 21 | 22 | check_dog(R, Dog) 23 | 24 | 25 | def test_register_single_type_using_string(): 26 | R = TypeRegistry() 27 | 28 | class Dog(R.ObjectType): 29 | name = R.Field('String') 30 | 31 | check_dog(R, Dog) 32 | 33 | 34 | def test_register_type_can_declare_builtin_scalar_types_directly(): 35 | R = TypeRegistry() 36 | 37 | class Dog(R.ObjectType): 38 | name = R.String 39 | 40 | check_dog(R, Dog) 41 | 42 | 43 | def test_register_type_can_use_builtin_graphql_types_in_field(): 44 | R = TypeRegistry() 45 | 46 | class Dog(R.ObjectType): 47 | name = R.Field(GraphQLString) 48 | 49 | check_dog(R, Dog) 50 | 51 | 52 | def test_can_use_mixins(): 53 | R = TypeRegistry() 54 | 55 | class DogMixin(): 56 | name = R.String 57 | 58 | class Dog(R.ObjectType, DogMixin): 59 | pass 60 | 61 | check_dog(R, Dog) 62 | 63 | 64 | def test_register_type_can_declare_builtin_scalar_type_as_non_null(): 65 | R = TypeRegistry() 66 | 67 | class Dog(R.ObjectType): 68 | name = R.String.NonNull 69 | 70 | fields = Dog.T.get_fields() 71 | assert list(fields.keys()) == ['name'] 72 | assert str(fields['name'].type) == 'String!' 73 | 74 | 75 | def test_register_type_can_declare_other_registered_types_directly(): 76 | R = TypeRegistry() 77 | 78 | class Dog(R.ObjectType): 79 | friend = R.Dog 80 | 81 | fields = Dog.T.get_fields() 82 | assert list(fields.keys()) == ['friend'] 83 | assert fields['friend'].type == Dog.T 84 | assert fields['friend'].name == 'friend' 85 | 86 | 87 | def test_register_type_can_declare_other_registered_types_directly_as_non_null(): 88 | R = TypeRegistry() 89 | 90 | class Dog(R.ObjectType): 91 | friend = R.Dog.NonNull 92 | 93 | fields = Dog.T.get_fields() 94 | assert list(fields.keys()) == ['friend'] 95 | type = fields['friend'].type 96 | assert isinstance(type, GraphQLNonNull) 97 | assert type.of_type == Dog.T 98 | assert fields['friend'].name == 'friend' 99 | assert str(type) == 'Dog!' 100 | 101 | 102 | def test_register_type_can_declare_other_registered_types_directly_as_list(): 103 | R = TypeRegistry() 104 | 105 | class Dog(R.ObjectType): 106 | friend = R.Dog.List 107 | 108 | fields = Dog.T.get_fields() 109 | assert list(fields.keys()) == ['friend'] 110 | type = fields['friend'].type 111 | assert isinstance(type, GraphQLList) 112 | assert type.of_type == Dog.T 113 | assert fields['friend'].name == 'friend' 114 | assert str(type) == '[Dog]' 115 | 116 | 117 | def test_register_type_can_declare_other_registered_types_directly_as_list_of_non_null(): 118 | R = TypeRegistry() 119 | 120 | class Dog(R.ObjectType): 121 | friend = R.Dog.NonNull.List 122 | 123 | fields = Dog.T.get_fields() 124 | assert list(fields.keys()) == ['friend'] 125 | assert fields['friend'].name == 'friend' 126 | type = fields['friend'].type 127 | assert str(type) == '[Dog!]' 128 | 129 | assert isinstance(type, GraphQLList) 130 | type = type.of_type 131 | assert isinstance(type, GraphQLNonNull) 132 | assert type.of_type == Dog.T 133 | 134 | 135 | def test_register_type_can_declare_other_registered_types_directly_as_non_null_list_of_non_null(): 136 | R = TypeRegistry() 137 | 138 | class Dog(R.ObjectType): 139 | friend = R.Dog.NonNull.List.NonNull 140 | 141 | fields = Dog.T.get_fields() 142 | assert list(fields.keys()) == ['friend'] 143 | assert fields['friend'].name == 'friend' 144 | type = fields['friend'].type 145 | assert str(type) == '[Dog!]!' 146 | 147 | assert isinstance(type, GraphQLNonNull) 148 | type = type.of_type 149 | assert isinstance(type, GraphQLList) 150 | type = type.of_type 151 | assert isinstance(type, GraphQLNonNull) 152 | assert type.of_type == Dog.T 153 | 154 | 155 | def test_rejects_object_type_definition_with_duplicated_field_names(): 156 | R = TypeRegistry() 157 | 158 | with raises(AssertionError) as excinfo: 159 | class Dog(R.ObjectType): 160 | friend = R.Dog.NonNull 161 | friend_aliased = R.Field(R.Dog, name='friend') 162 | 163 | assert str(excinfo.value) == 'Duplicate field definition for name "friend" in type "Dog.friend_aliased".' 164 | 165 | 166 | def test_rejects_interface_type_definition_with_duplicated_field_names(): 167 | R = TypeRegistry() 168 | 169 | with raises(AssertionError) as excinfo: 170 | class Dog(R.Interface): 171 | friend = R.Dog.NonNull 172 | friend_aliased = R.Field(R.Dog, name='friend') 173 | 174 | assert str(excinfo.value) == 'Duplicate field definition for name "friend" in type "Dog.friend_aliased".' 175 | 176 | 177 | def test_orders_fields_in_order_declared(): 178 | R = TypeRegistry() 179 | 180 | class Dog(R.ObjectType): 181 | id = R.ID 182 | name = R.Field('String') 183 | dog = R.Dog 184 | some_other_field = R.Field(R.Int) 185 | some_other_dog = R.Field('Dog') 186 | foo = R.String 187 | bar = R.String 188 | aaa = R.String 189 | 190 | field_order = list(Dog.T.get_fields().keys()) 191 | assert field_order == ['id', 'name', 'dog', 'someOtherField', 'someOtherDog', 'foo', 'bar', 'aaa'] 192 | 193 | 194 | def test_cannot_resolve_unregistered_type(): 195 | R = TypeRegistry() 196 | 197 | Dog = GraphQLObjectType( 198 | name='Dog', 199 | fields={ 200 | 'a': GraphQLField(GraphQLString) 201 | } 202 | ) 203 | 204 | with raises(AssertionError) as excinfo: 205 | R[Dog]() 206 | 207 | assert str(excinfo.value) == 'Attempted to resolve a type "Dog" that is not registered with this Registry.' 208 | 209 | R(Dog) 210 | assert R[Dog]() is Dog 211 | 212 | 213 | def test_cannot_resolve_type_of_same_name_that_is_mismatched(): 214 | R = TypeRegistry() 215 | 216 | class Dog(R.ObjectType): 217 | a = R.String 218 | 219 | SomeOtherDog = GraphQLObjectType( 220 | name='Dog', 221 | fields={ 222 | 'a': GraphQLField(GraphQLString) 223 | } 224 | ) 225 | 226 | with raises(AssertionError) as excinfo: 227 | R[SomeOtherDog]() 228 | 229 | assert str(excinfo.value) == 'Attempted to resolve a type "Dog" that does not match the already registered type.' 230 | -------------------------------------------------------------------------------- /tests/test_graphql_object_interoperability.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from graphql.core import graphql 3 | from graphql.core.type import GraphQLObjectType, GraphQLField, GraphQLString 4 | from epoxy.registry import TypeRegistry 5 | 6 | 7 | def test_resolves_regular_graphql_type(): 8 | BuiltInType = GraphQLObjectType( 9 | name='BuiltInType', 10 | fields={ 11 | 'someString': GraphQLField(GraphQLString) 12 | } 13 | ) 14 | 15 | BuiltInTypeTuple = namedtuple('BuiltInTypeData', 'someString') 16 | 17 | R = TypeRegistry() 18 | 19 | class Query(R.ObjectType): 20 | built_in_type = R.Field(BuiltInType) 21 | 22 | def resolve_built_in_type(self, obj, args, info): 23 | return BuiltInTypeTuple('Hello World. I am a string.') 24 | 25 | R(BuiltInType) 26 | 27 | schema = R.Schema(R.Query) 28 | result = graphql(schema, '{ builtInType { someString } }') 29 | assert not result.errors 30 | assert result.data == {'builtInType': {'someString': 'Hello World. I am a string.'}} -------------------------------------------------------------------------------- /tests/test_input_type.py: -------------------------------------------------------------------------------- 1 | from graphql.core import graphql 2 | from graphql.core.type import GraphQLArgument, GraphQLInputObjectType, GraphQLString 3 | from graphql.core.type.scalars import GraphQLInt 4 | from epoxy import TypeRegistry 5 | 6 | 7 | def test_input_type_creation(): 8 | R = TypeRegistry() 9 | 10 | class SimpleInput(R.InputType): 11 | a = R.Int 12 | b = R.Int 13 | some_underscore = R.String 14 | some_from_field = R.InputField(R.String, default_value='Hello World') 15 | default_value_from_thunk = R.String(default_value='Hello World') 16 | 17 | input_type = SimpleInput.T 18 | assert input_type is R.SimpleInput() 19 | assert isinstance(input_type, GraphQLInputObjectType) 20 | fields = input_type.get_fields() 21 | 22 | expected_keys = ['a', 'b', 'someUnderscore', 'someFromField', 'defaultValueFromThunk'] 23 | 24 | assert list(fields.keys()) == expected_keys 25 | assert [field.name for field in fields.values()] == expected_keys 26 | assert fields['a'].type == GraphQLInt 27 | assert fields['b'].type == GraphQLInt 28 | assert fields['someUnderscore'].type == GraphQLString 29 | assert fields['someFromField'].type == GraphQLString 30 | assert fields['someFromField'].default_value == 'Hello World' 31 | assert fields['defaultValueFromThunk'].type == GraphQLString 32 | assert fields['defaultValueFromThunk'].default_value == 'Hello World' 33 | 34 | input_value = SimpleInput({ 35 | 'a': 1, 36 | 'someUnderscore': 'hello', 37 | }) 38 | 39 | assert input_value.a == 1 40 | assert input_value.b is None 41 | assert input_value.some_underscore == 'hello' 42 | assert input_value.some_from_field == 'Hello World' 43 | assert input_value.default_value_from_thunk == 'Hello World' 44 | 45 | 46 | def test_input_type(): 47 | R = TypeRegistry() 48 | 49 | class SimpleInput(R.InputType): 50 | a = R.Int 51 | b = R.Int 52 | 53 | class Query(R.ObjectType): 54 | f = R.String(args={ 55 | 'input': R.SimpleInput 56 | }) 57 | 58 | def resolve_f(self, obj, args, info): 59 | input = SimpleInput(args['input']) 60 | return "I was given {i.a} and {i.b}".format(i=input) 61 | 62 | Schema = R.Schema(R.Query) 63 | query = ''' 64 | { 65 | f(input: {a: 1, b: 2}) 66 | } 67 | ''' 68 | 69 | result = graphql(Schema, query) 70 | assert not result.errors 71 | assert result.data == { 72 | 'f': "I was given 1 and 2" 73 | } 74 | -------------------------------------------------------------------------------- /tests/test_interfaces.py: -------------------------------------------------------------------------------- 1 | from graphql.core import graphql 2 | from epoxy.registry import TypeRegistry 3 | 4 | 5 | def test_register_interface(): 6 | R = TypeRegistry() 7 | 8 | class Character(R.Interface): 9 | id = R.ID 10 | name = R.String 11 | friends_with = R.Character.List 12 | lives_remaining = R.Field(R.Int) 13 | 14 | character = Character.T 15 | fields = character.get_fields() 16 | assert list(fields.keys()) == ['id', 'name', 'friendsWith', 'livesRemaining'] 17 | 18 | 19 | def test_register_single_type(): 20 | R = TypeRegistry() 21 | 22 | class Character(R.Interface): 23 | id = R.ID 24 | name = R.String 25 | friends_with = R.Character.List 26 | lives_remaining = R.Field(R.Int) 27 | 28 | class Human(R.Implements.Character): 29 | home_planet = R.String 30 | 31 | human = Human.T 32 | fields = human.get_fields() 33 | assert list(fields.keys()) == ['id', 'name', 'friendsWith', 'livesRemaining', 'homePlanet'] 34 | 35 | 36 | def test_implements_multiple_interfaces_via_r(): 37 | R = TypeRegistry() 38 | 39 | class Character(R.Interface): 40 | id = R.ID 41 | name = R.String 42 | friends_with = R.Character.List 43 | lives_remaining = R.Field(R.Int) 44 | 45 | class Bean(R.Interface): 46 | real = R.Boolean 47 | 48 | class Human(R.Implements[R.Character, R.Bean]): 49 | home_planet = R.String 50 | 51 | human = Human.T 52 | fields = human.get_fields() 53 | assert list(fields.keys()) == ['id', 'name', 'friendsWith', 'livesRemaining', 'real', 'homePlanet'] 54 | 55 | 56 | def test_implements_multiple_interfaces_via_class(): 57 | R = TypeRegistry() 58 | 59 | class Character(R.Interface): 60 | id = R.ID 61 | name = R.String 62 | friends_with = R.Character.List 63 | lives_remaining = R.Field(R.Int) 64 | 65 | class Bean(R.Interface): 66 | real = R.Boolean 67 | 68 | class Human(R.Implements[Character, Bean]): 69 | home_planet = R.String 70 | 71 | human = Human.T 72 | fields = human.get_fields() 73 | assert list(fields.keys()) == ['id', 'name', 'friendsWith', 'livesRemaining', 'real', 'homePlanet'] 74 | 75 | 76 | def test_implements_multiple_interfaces_via_string(): 77 | R = TypeRegistry() 78 | 79 | class Character(R.Interface): 80 | id = R.ID 81 | name = R.String 82 | friends_with = R.Character.List 83 | lives_remaining = R.Field(R.Int) 84 | 85 | class Bean(R.Interface): 86 | real = R.Boolean 87 | 88 | class Human(R.Implements['Character', 'Bean']): 89 | home_planet = R.String 90 | 91 | human = Human.T 92 | fields = human.get_fields() 93 | assert list(fields.keys()) == ['id', 'name', 'friendsWith', 'livesRemaining', 'real', 'homePlanet'] 94 | 95 | 96 | def test_is_sensitive_to_implementation_order(): 97 | R = TypeRegistry() 98 | 99 | class Character(R.Interface): 100 | id = R.ID 101 | name = R.String 102 | friends_with = R.Character.List 103 | lives_remaining = R.Field(R.Int) 104 | 105 | class Bean(R.Interface): 106 | real = R.Boolean 107 | hero = R.Boolean 108 | 109 | class Human(R.Implements[R.Bean, R.Character]): 110 | home_planet = R.String 111 | 112 | human = Human.T 113 | fields = human.get_fields() 114 | assert list(fields.keys()) == ['real', 'hero', 'id', 'name', 'friendsWith', 'livesRemaining', 'homePlanet'] 115 | 116 | 117 | def test_definition_order_wont_affect_field_order(): 118 | R = TypeRegistry() 119 | 120 | class Bean(R.Interface): 121 | real = R.Boolean 122 | hero = R.Boolean 123 | 124 | class Character(R.Interface): 125 | id = R.ID 126 | name = R.String 127 | friends_with = R.Character.List 128 | lives_remaining = R.Field(R.Int) 129 | 130 | class Human(R.Implements[R.Character, Bean]): 131 | home_planet = R.String 132 | 133 | human = Human.T 134 | fields = human.get_fields() 135 | assert list(fields.keys()) == ['id', 'name', 'friendsWith', 'livesRemaining', 'real', 'hero', 'homePlanet'] 136 | 137 | 138 | def test_runtime_type_resolution(): 139 | R = TypeRegistry() 140 | 141 | class Pet(R.Interface): 142 | name = R.String 143 | 144 | class Dog(R.Implements.Pet): 145 | bark = R.String 146 | 147 | class Cat(R.Implements.Pet): 148 | meow = R.String 149 | 150 | class Query(R.ObjectType): 151 | pets = R.Pet.List 152 | 153 | schema = R.Schema(Query) 154 | 155 | data = Query(pets=[ 156 | Dog(name='Clifford', bark='Really big bark, because it\'s a really big dog.'), 157 | Cat(name='Garfield', meow='Lasagna') 158 | ]) 159 | 160 | result = graphql(schema, ''' 161 | { 162 | pets { 163 | name 164 | __typename 165 | ... on Dog { 166 | bark 167 | } 168 | 169 | ... on Cat { 170 | meow 171 | } 172 | } 173 | } 174 | 175 | ''', data) 176 | assert not result.errors 177 | assert result.data == { 178 | 'pets': [{'__typename': 'Dog', 'bark': "Really big bark, because it's a really big dog.", 'name': 'Clifford'}, 179 | {'__typename': 'Cat', 'meow': 'Lasagna', 'name': 'Garfield'}] 180 | } 181 | -------------------------------------------------------------------------------- /tests/test_mutation.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from functools import reduce 3 | from graphql.core import graphql 4 | from epoxy import TypeRegistry 5 | 6 | 7 | def test_simple_mutation(): 8 | R = TypeRegistry() 9 | 10 | class SimpleAddition(R.Mutation): 11 | class Input: 12 | a = R.Int 13 | b = R.Int 14 | 15 | class Output: 16 | sum = R.Int 17 | 18 | def execute(self, obj, input, info): 19 | return self.Output(sum=input.a + input.b) 20 | 21 | class SimpleMultiplication(R.Mutation): 22 | class Input: 23 | a = R.Int.List 24 | 25 | class Output: 26 | product = R.Int 27 | input = R.Int.List 28 | 29 | def execute(self, obj, input, info): 30 | return self.Output( 31 | input=input.a, 32 | product=reduce(operator.mul, input.a[1:], input.a[0]) 33 | ) 34 | 35 | # Dummy query -- does nothing. 36 | class Query(R.ObjectType): 37 | foo = R.String 38 | 39 | Schema = R.Schema(R.Query, R.Mutations) 40 | 41 | mutation_query = ''' 42 | mutation testSimpleAdd { 43 | simpleAddition(input: {a: 5, b: 10}) { 44 | sum 45 | } 46 | simpleMultiplication(input: {a: [1, 2, 3, 4, 5]}) { 47 | product 48 | input 49 | } 50 | } 51 | ''' 52 | 53 | result = graphql(Schema, mutation_query) 54 | assert not result.errors 55 | assert result.data == { 56 | 'simpleAddition': { 57 | 'sum': 15 58 | }, 59 | 'simpleMultiplication': { 60 | 'product': 120, 61 | 'input': [1, 2, 3, 4, 5] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/test_object_type_as_data.py: -------------------------------------------------------------------------------- 1 | from graphql.core import graphql 2 | from epoxy.registry import TypeRegistry 3 | from pytest import raises 4 | 5 | R = TypeRegistry() 6 | 7 | 8 | class Human(R.ObjectType): 9 | name = R.String 10 | favorite_color = R.String 11 | 12 | 13 | Schema = R.Schema(R.Human) 14 | 15 | 16 | def test_object_type_as_data(): 17 | jake = Human(name='Jake', favorite_color='Red') 18 | assert jake.name == 'Jake' 19 | assert jake.favorite_color == 'Red' 20 | assert repr(jake) == ''.format(jake.name, jake.favorite_color) 21 | 22 | result = graphql(Schema, '{ name favoriteColor }', jake) 23 | assert not result.errors 24 | assert result.data == {'name': 'Jake', 'favoriteColor': 'Red'} 25 | 26 | 27 | def test_object_type_as_data_with_partial_fields_provided(): 28 | jake = Human(name='Jake') 29 | assert jake.name == 'Jake' 30 | assert jake.favorite_color is None 31 | assert repr(jake) == ''.format(jake.name, jake.favorite_color) 32 | 33 | result = graphql(Schema, '{ name favoriteColor }', jake) 34 | assert not result.errors 35 | assert result.data == {'name': 'Jake', 'favoriteColor': None} 36 | 37 | 38 | def test_object_type_giving_unexpected_key(): 39 | with raises(TypeError) as excinfo: 40 | Human(after_all=True) 41 | 42 | assert str(excinfo.value) == 'Type Human received unexpected keyword argument(s): after_all.' 43 | -------------------------------------------------------------------------------- /tests/test_register_enum.py: -------------------------------------------------------------------------------- 1 | from graphql.core.type.definition import GraphQLEnumType 2 | from epoxy.registry import TypeRegistry 3 | from enum import Enum 4 | 5 | 6 | def test_register_builtin_enum(): 7 | R = TypeRegistry() 8 | 9 | @R 10 | class MyEnum(Enum): 11 | FOO = 1 12 | BAR = 2 13 | BAZ = 3 14 | 15 | enum = R.type('MyEnum') 16 | assert isinstance(enum, GraphQLEnumType) 17 | values = enum.get_values() 18 | assert [v.name for v in values] == ['FOO', 'BAR', 'BAZ'] 19 | assert [v.value for v in values] == [MyEnum.FOO.value, MyEnum.BAR.value, MyEnum.BAZ.value] 20 | -------------------------------------------------------------------------------- /tests/test_register_reserved_name.py: -------------------------------------------------------------------------------- 1 | from graphql.core.type import GraphQLField, GraphQLObjectType 2 | from graphql.core.type import GraphQLString 3 | from epoxy import TypeRegistry 4 | from pytest import raises 5 | 6 | 7 | def test_reserved_names(): 8 | R = TypeRegistry() 9 | 10 | for name in R._reserved_names: 11 | type = GraphQLObjectType( 12 | name=name, 13 | fields={'a': GraphQLField(GraphQLString)} 14 | ) 15 | with raises(AssertionError) as excinfo: 16 | R(type) 17 | 18 | assert str(excinfo.value) == 'You cannot register a type named "{}".'.format(name) 19 | -------------------------------------------------------------------------------- /tests/test_registration.py: -------------------------------------------------------------------------------- 1 | from graphql.core.type.definition import GraphQLObjectType, GraphQLField 2 | from graphql.core.type.scalars import GraphQLString 3 | from pytest import raises 4 | 5 | from epoxy import TypeRegistry 6 | 7 | 8 | def test_will_disallow_duplicate_type_names_from_being_registered(): 9 | type = GraphQLObjectType(name='Query', fields={ 10 | 'a': GraphQLField(GraphQLString) 11 | }) 12 | 13 | type_duplicated = GraphQLObjectType(name='Query', fields={ 14 | 'a': GraphQLField(GraphQLString) 15 | }) 16 | 17 | R = TypeRegistry() 18 | R(type) 19 | 20 | with raises(AssertionError) as excinfo: 21 | R(type_duplicated) 22 | 23 | assert str(excinfo.value) == 'There is already a registered type named "Query".' 24 | 25 | 26 | def test_will_allow_the_same_type_to_be_registered_more_than_once(): 27 | type = GraphQLObjectType(name='Query', fields={ 28 | 'a': GraphQLField(GraphQLString) 29 | }) 30 | 31 | R = TypeRegistry() 32 | assert R(type) 33 | assert R(type) 34 | 35 | 36 | def test_cannot_register_type_starting_with_an_underscore(): 37 | type = GraphQLObjectType(name='_Query', fields={ 38 | 'a': GraphQLField(GraphQLString) 39 | }) 40 | 41 | R = TypeRegistry() 42 | 43 | with raises(AssertionError) as excinfo: 44 | R(type) 45 | 46 | assert str(excinfo.value) == 'Registered type name cannot start with an "_".' 47 | 48 | 49 | def test_cannot_register_type_thats_using_reserved_name(): 50 | R = TypeRegistry() 51 | for name in TypeRegistry._reserved_names: 52 | type = GraphQLObjectType(name=name, fields={ 53 | 'a': GraphQLField(GraphQLString) 54 | }) 55 | with raises(AssertionError) as excinfo: 56 | R(type) 57 | 58 | assert str(excinfo.value) == 'You cannot register a type named "{}".'.format(name) 59 | -------------------------------------------------------------------------------- /tests/test_relay/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'jake' 2 | -------------------------------------------------------------------------------- /tests/test_relay/test_relay_connections.py: -------------------------------------------------------------------------------- 1 | from graphql.core import graphql 2 | from epoxy.contrib.relay import RelayMixin 3 | from epoxy.contrib.relay.data_source.memory import InMemoryDataSource 4 | from epoxy.contrib.relay.utils import base64 5 | from epoxy.registry import TypeRegistry 6 | 7 | letter_chars = ['A', 'B', 'C', 'D', 'E'] 8 | 9 | data_source = InMemoryDataSource() 10 | 11 | R = TypeRegistry() 12 | Relay = R.Mixin(RelayMixin, data_source) 13 | 14 | 15 | class Letter(R.Implements[Relay.Node]): 16 | letter = R.String 17 | 18 | 19 | class Query(R.ObjectType): 20 | letters = Relay.Connection('Letter', R.Letter) 21 | node = Relay.NodeField 22 | 23 | 24 | Schema = R.Schema(R.Query) 25 | 26 | letters = {} 27 | for i, letter in enumerate(letter_chars, 1): 28 | l = Letter(id=i, letter=letter) 29 | letters[letter] = l 30 | data_source.add(l) 31 | 32 | 33 | def edges(selected_letters): 34 | return [ 35 | { 36 | 'node': { 37 | 'id': base64('Letter:%s' % l.id), 38 | 'letter': l.letter 39 | }, 40 | 'cursor': base64('sc:%s' % l.id) 41 | } 42 | for l in [letters[i] for i in selected_letters] 43 | ] 44 | 45 | 46 | def cursor_for(ltr): 47 | l = letters[ltr] 48 | return base64('sc:%s' % l.id) 49 | 50 | 51 | def execute(args=''): 52 | if args: 53 | args = '(' + args + ')' 54 | 55 | return graphql(Schema, ''' 56 | { 57 | letters%s { 58 | edges { 59 | node { 60 | id 61 | letter 62 | } 63 | cursor 64 | } 65 | pageInfo { 66 | hasPreviousPage 67 | hasNextPage 68 | startCursor 69 | endCursor 70 | } 71 | } 72 | } 73 | ''' % args) 74 | 75 | 76 | def check(args, letters, has_previous_page=False, has_next_page=False): 77 | result = execute(args) 78 | expected_edges = edges(letters) 79 | expected_page_info = { 80 | 'hasPreviousPage': has_previous_page, 81 | 'hasNextPage': has_next_page, 82 | 'endCursor': expected_edges[-1]['cursor'] if expected_edges else None, 83 | 'startCursor': expected_edges[0]['cursor'] if expected_edges else None 84 | } 85 | 86 | assert not result.errors 87 | assert result.data == { 88 | 'letters': { 89 | 'edges': expected_edges, 90 | 'pageInfo': expected_page_info 91 | } 92 | } 93 | 94 | 95 | def test_returns_all_elements_without_filters(): 96 | check('', 'ABCDE') 97 | 98 | 99 | def test_respects_a_smaller_first(): 100 | check('first: 2', 'AB', has_next_page=True) 101 | 102 | 103 | def test_respects_an_overly_large_first(): 104 | check('first: 10', 'ABCDE') 105 | 106 | 107 | def test_respects_a_smaller_last(): 108 | check('last: 2', 'DE', has_previous_page=True) 109 | 110 | 111 | def test_respects_an_overly_large_last(): 112 | check('last: 10', 'ABCDE') 113 | 114 | 115 | def test_respects_first_and_after(): 116 | check('first: 2, after: "{}"'.format(cursor_for('B')), 'CD', has_next_page=True) 117 | 118 | 119 | def test_respects_first_and_after_with_long_first(): 120 | check('first: 10, after: "{}"'.format(cursor_for('B')), 'CDE') 121 | 122 | 123 | def test_respects_last_and_before(): 124 | check('last: 2, before: "{}"'.format(cursor_for('D')), 'BC', has_previous_page=True) 125 | 126 | 127 | def test_respects_last_and_before_with_long_last(): 128 | check('last: 10, before: "{}"'.format(cursor_for('D')), 'ABC') 129 | 130 | 131 | def test_respects_first_and_after_and_before_too_few(): 132 | check('first: 2, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), 'BC', has_next_page=True) 133 | 134 | 135 | def test_respects_first_and_after_and_before_too_many(): 136 | check('first: 4, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), 'BCD') 137 | 138 | 139 | def test_respects_first_and_after_and_before_exactly_right(): 140 | check('first: 3, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), "BCD") 141 | 142 | 143 | def test_respects_last_and_after_and_before_too_few(): 144 | check('last: 2, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), 'CD', has_previous_page=True) 145 | 146 | 147 | def test_respects_last_and_after_and_before_too_many(): 148 | check('last: 4, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), 'BCD') 149 | 150 | 151 | def test_respects_last_and_after_and_before_exactly_right(): 152 | check('last: 3, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), 'BCD') 153 | 154 | 155 | def test_returns_no_elements_if_first_is_0(): 156 | check('first: 0', '', has_next_page=True) 157 | 158 | 159 | def test_returns_all_elements_if_cursors_are_invalid(): 160 | check('before: "invalid" after: "invalid"', 'ABCDE') 161 | 162 | 163 | def test_returns_all_elements_if_cursors_are_on_the_outside(): 164 | check('before: "{}" after: "{}"'.format(base64('sc:%s' % 6), base64('sc:%s' % 0)), 'ABCDE') 165 | 166 | 167 | def test_returns_no_elements_if_cursors_cross(): 168 | check('before: "{}" after: "{}"'.format(base64('sc:%s' % 2), base64('sc:%s' % 4)), '') 169 | -------------------------------------------------------------------------------- /tests/test_relay/test_relay_mutation.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from functools import reduce 3 | from graphql.core import graphql 4 | from epoxy import TypeRegistry 5 | from epoxy.contrib.relay import RelayMixin 6 | 7 | 8 | def test_simple_mutation(): 9 | R = TypeRegistry() 10 | Relay = R.Mixin(RelayMixin, None) 11 | 12 | class SimpleAddition(Relay.Mutation): 13 | class Input: 14 | a = R.Int 15 | b = R.Int 16 | 17 | class Output: 18 | sum = R.Int 19 | 20 | def execute(self, obj, input, info): 21 | return self.Output(sum=input.a + input.b) 22 | 23 | class SimpleMultiplication(Relay.Mutation): 24 | class Input: 25 | a = R.Int.List 26 | 27 | class Output: 28 | product = R.Int 29 | input = R.Int.List 30 | 31 | def execute(self, obj, input, info): 32 | return self.Output( 33 | input=input.a, 34 | product=reduce(operator.mul, input.a[1:], input.a[0]) 35 | ) 36 | 37 | # Dummy query -- does nothing. 38 | class Query(R.ObjectType): 39 | foo = R.String 40 | 41 | Schema = R.Schema(R.Query, R.Mutations) 42 | 43 | mutation_query = ''' 44 | mutation testSimpleAdd { 45 | simpleAddition(input: {a: 5, b: 10, clientMutationId: "test123"}) { 46 | clientMutationId 47 | sum 48 | } 49 | simpleMultiplication(input: {a: [1, 2, 3, 4, 5], clientMutationId: "0xdeadbeef"}) { 50 | clientMutationId 51 | product 52 | input 53 | } 54 | } 55 | ''' 56 | 57 | result = graphql(Schema, mutation_query) 58 | assert not result.errors 59 | assert result.data == { 60 | 'simpleAddition': { 61 | 'sum': 15, 62 | 'clientMutationId': 'test123' 63 | }, 64 | 'simpleMultiplication': { 65 | 'clientMutationId': '0xdeadbeef', 66 | 'product': 120, 67 | 'input': [1, 2, 3, 4, 5] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/test_relay/test_relay_node.py: -------------------------------------------------------------------------------- 1 | from graphql.core import graphql 2 | from epoxy.contrib.relay import RelayMixin 3 | from epoxy.contrib.relay.data_source.memory import InMemoryDataSource 4 | from epoxy.registry import TypeRegistry 5 | 6 | 7 | def test_relay_node_definition(): 8 | R = TypeRegistry() 9 | Relay = R.Mixin(RelayMixin, InMemoryDataSource()) 10 | 11 | class Pet(R.Implements[R.Node]): 12 | name = R.String 13 | 14 | class Query(R.ObjectType): 15 | pets = R.Pet.List 16 | node = Relay.NodeField 17 | 18 | schema = R.Schema(R.Query) 19 | 20 | pets = { 21 | 5: Pet(id=5, name='Garfield'), 22 | 6: Pet(id=6, name='Odis') 23 | } 24 | 25 | data = Query(pets=[pets[5], pets[6]]) 26 | result = graphql(schema, '{ pets { id, name } }', data) 27 | assert result.data == {'pets': [{'id': 'UGV0OjU=', 'name': 'Garfield'}, {'id': 'UGV0OjY=', 'name': 'Odis'}]} 28 | assert not result.errors 29 | 30 | 31 | def test_relay_node_definition_using_custom_type(): 32 | R = TypeRegistry() 33 | Relay = R.Mixin(RelayMixin, InMemoryDataSource()) 34 | 35 | class Pet(R.Implements[R.Node]): 36 | name = R.String 37 | 38 | class Query(R.ObjectType): 39 | pets = R.Pet.List 40 | node = Relay.NodeField 41 | 42 | schema = R.Schema(R.Query) 43 | 44 | @R.Pet.CanBe 45 | class MyPet(object): 46 | def __init__(self, id, name): 47 | self.id = id 48 | self.name = name 49 | 50 | pets = { 51 | 5: MyPet(id=5, name='Garfield'), 52 | 6: MyPet(id=6, name='Odis') 53 | } 54 | 55 | data = Query(pets=[pets[5], pets[6]]) 56 | result = graphql(schema, '{ pets { id, name } }', data) 57 | assert result.data == {'pets': [{'id': 'UGV0OjU=', 'name': 'Garfield'}, {'id': 'UGV0OjY=', 'name': 'Odis'}]} 58 | assert not result.errors 59 | 60 | 61 | def test_relay_node_field_resolver(): 62 | data_source = InMemoryDataSource() 63 | 64 | R = TypeRegistry() 65 | Relay = R.Mixin(RelayMixin, data_source) 66 | 67 | class Pet(R.Implements[R.Node]): 68 | name = R.String 69 | 70 | class Query(R.ObjectType): 71 | pets = R.Pet.List 72 | node = Relay.NodeField 73 | 74 | schema = R.Schema(R.Query) 75 | 76 | data_source.add(Pet(id=5, name='Garfield')) 77 | data_source.add(Pet(id=6, name='Odis')) 78 | 79 | result = graphql(schema, ''' 80 | { 81 | node(id: "UGV0OjU=") { 82 | id, 83 | ... on Pet { 84 | name 85 | } 86 | } 87 | } 88 | ''') 89 | 90 | assert not result.errors 91 | assert result.data == {'node': {'id': 'UGV0OjU=', 'name': 'Garfield'}} 92 | -------------------------------------------------------------------------------- /tests/test_resolver_execution.py: -------------------------------------------------------------------------------- 1 | from graphql.core import graphql 2 | 3 | from epoxy.registry import TypeRegistry 4 | 5 | 6 | def test_resolves_from_interface(): 7 | R = TypeRegistry() 8 | 9 | class Pet(R.Interface): 10 | make_noise = R.String 11 | 12 | def resolve_make_noise(self, *args): 13 | return 'I am a pet, hear me roar!' 14 | 15 | class Dog(R.Implements.Pet): 16 | pass 17 | 18 | class Query(R.ObjectType): 19 | dog = R.Dog 20 | 21 | def resolve_dog(self, *args): 22 | return Dog() 23 | 24 | schema = R.Schema(R.Query) 25 | result = graphql(schema, '{ dog { makeNoise } }') 26 | assert not result.errors 27 | assert result.data == {'dog': {'makeNoise': 'I am a pet, hear me roar!'}} 28 | 29 | 30 | def test_field_re_definition_wont_override(): 31 | R = TypeRegistry() 32 | 33 | class Pet(R.Interface): 34 | make_noise = R.String 35 | 36 | def resolve_make_noise(self, *args): 37 | return 'I am a pet, hear me roar!' 38 | 39 | class Dog(R.Implements.Pet): 40 | make_noise = R.String 41 | 42 | class Query(R.ObjectType): 43 | dog = R.Dog 44 | 45 | def resolve_dog(self, *args): 46 | return Dog() 47 | 48 | schema = R.Schema(R.Query) 49 | result = graphql(schema, '{ dog { makeNoise } }') 50 | assert not result.errors 51 | assert result.data == {'dog': {'makeNoise': 'I am a pet, hear me roar!'}} 52 | 53 | 54 | def test_will_choose_first_resolver_of_first_defined_interface(): 55 | R = TypeRegistry() 56 | 57 | class Pet(R.Interface): 58 | make_noise = R.String 59 | 60 | def resolve_make_noise(self, *args): 61 | return 'I am a pet, hear me roar!' 62 | 63 | class Barker(R.Interface): 64 | make_noise = R.String 65 | 66 | def resolve_make_noise(self, *args): 67 | return 'Woof, woof!!' 68 | 69 | class Dog(R.Implements[Barker, Pet]): 70 | make_noise = R.String 71 | 72 | class Query(R.ObjectType): 73 | dog = R.Dog 74 | 75 | def resolve_dog(self, *args): 76 | return Dog() 77 | 78 | schema = R.Schema(R.Query) 79 | result = graphql(schema, '{ dog { makeNoise } }') 80 | assert not result.errors 81 | assert result.data == {'dog': {'makeNoise': 'Woof, woof!!'}} 82 | 83 | 84 | def test_object_type_can_override_interface_resolver(): 85 | R = TypeRegistry() 86 | 87 | class Pet(R.Interface): 88 | make_noise = R.String 89 | 90 | def resolve_make_noise(self, *args): 91 | return 'I am a pet, hear me roar!' 92 | 93 | class Dog(R.Implements.Pet): 94 | make_noise = R.String 95 | 96 | def resolve_make_noise(self, *args): 97 | return 'Woof woof! Bark bark!' 98 | 99 | class Query(R.ObjectType): 100 | dog = R.Dog 101 | 102 | def resolve_dog(self, *args): 103 | return Dog() 104 | 105 | schema = R.Schema(R.Query) 106 | 107 | result = graphql(schema, '{ dog { makeNoise } }') 108 | assert not result.errors 109 | assert result.data == {'dog': {'makeNoise': 'Woof woof! Bark bark!'}} 110 | -------------------------------------------------------------------------------- /tests/test_scalar.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from graphql.core import graphql 3 | from graphql.core.language import ast 4 | from graphql.core.type import GraphQLScalarType 5 | from epoxy.registry import TypeRegistry 6 | 7 | 8 | def test_custom_scalar_type(): 9 | R = TypeRegistry() 10 | 11 | def serialize_date_time(dt): 12 | assert isinstance(dt, datetime.datetime) 13 | return dt.isoformat() 14 | 15 | def parse_literal(node): 16 | if isinstance(node, ast.StringValue): 17 | return datetime.datetime.strptime(node.value, "%Y-%m-%dT%H:%M:%S.%f") 18 | 19 | def parse_value(value): 20 | return datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f") 21 | 22 | DateTimeType = GraphQLScalarType(name='DateTime', serialize=serialize_date_time, 23 | parse_literal=parse_literal, 24 | parse_value=parse_value) 25 | R(DateTimeType) 26 | 27 | class Query(R.ObjectType): 28 | datetime = R.DateTime(args={ 29 | 'in': R.DateTime 30 | }) 31 | 32 | def resolve_datetime(self, obj, args, info): 33 | return args.get('in') 34 | 35 | now = datetime.datetime.now() 36 | isoformat = now.isoformat() 37 | 38 | Schema = R.Schema(R.Query) 39 | 40 | response = graphql(Schema, ''' 41 | { 42 | datetime(in: "%s") 43 | } 44 | 45 | ''' % isoformat) 46 | 47 | assert not response.errors 48 | assert response.data == { 49 | 'datetime': isoformat 50 | } 51 | 52 | response = graphql(Schema, ''' 53 | query Test($date: DateTime) { 54 | datetime(in: $date) 55 | } 56 | 57 | ''', args={ 58 | 'date': isoformat 59 | }) 60 | 61 | assert not response.errors 62 | assert response.data == { 63 | 'datetime': isoformat 64 | } 65 | 66 | 67 | def test_register_scalar_type(): 68 | R = TypeRegistry() 69 | 70 | class DateTime(R.Scalar): 71 | @staticmethod 72 | def serialize(dt): 73 | assert isinstance(dt, datetime.datetime) 74 | return dt.isoformat() 75 | 76 | @staticmethod 77 | def parse_literal(node): 78 | if isinstance(node, ast.StringValue): 79 | return datetime.datetime.strptime(node.value, "%Y-%m-%dT%H:%M:%S.%f") 80 | 81 | @staticmethod 82 | def parse_value(value): 83 | return datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f") 84 | 85 | class Query(R.ObjectType): 86 | datetime = R.DateTime(args={ 87 | 'in': R.DateTime 88 | }) 89 | 90 | def resolve_datetime(self, obj, args, info): 91 | return args.get('in') 92 | 93 | now = datetime.datetime.now() 94 | isoformat = now.isoformat() 95 | 96 | Schema = R.Schema(R.Query) 97 | 98 | response = graphql(Schema, ''' 99 | { 100 | datetime(in: "%s") 101 | } 102 | 103 | ''' % isoformat) 104 | 105 | assert not response.errors 106 | assert response.data == { 107 | 'datetime': isoformat 108 | } 109 | 110 | response = graphql(Schema, ''' 111 | query Test($date: DateTime) { 112 | datetime(in: $date) 113 | } 114 | 115 | ''', args={ 116 | 'date': isoformat 117 | }) 118 | 119 | assert not response.errors 120 | assert response.data == { 121 | 'datetime': isoformat 122 | } 123 | -------------------------------------------------------------------------------- /tests/test_schema_creation.py: -------------------------------------------------------------------------------- 1 | from graphql.core import graphql 2 | 3 | from epoxy.registry import TypeRegistry 4 | 5 | R = TypeRegistry() 6 | 7 | 8 | class Dog(R.ObjectType): 9 | name = R.String 10 | 11 | @staticmethod 12 | def resolve_name(obj, args, info): 13 | return 'Yes, this is dog.' 14 | 15 | 16 | class Character(R.ObjectType): 17 | id = R.ID 18 | name = R.String 19 | friends = R.Character.List 20 | 21 | 22 | def test_schema_creation_using_r_attr(): 23 | schema = R.Schema(R.Dog) 24 | result = graphql(schema, '{ name }') 25 | assert not result.errors 26 | assert result.data == {'name': 'Yes, this is dog.'} 27 | 28 | 29 | def test_schema_creation_using_string(): 30 | schema = R.Schema('Dog') 31 | result = graphql(schema, '{ name }') 32 | assert not result.errors 33 | assert result.data == {'name': 'Yes, this is dog.'} 34 | 35 | 36 | def test_schema_creation_using_r_item(): 37 | schema = R.Schema(R['Dog']) 38 | result = graphql(schema, '{ name }') 39 | assert not result.errors 40 | assert result.data == {'name': 'Yes, this is dog.'} 41 | 42 | 43 | def test_schema_creation_using_r_item_r_attr(): 44 | schema = R.Schema(R[R.Dog]) 45 | result = graphql(schema, '{ name }') 46 | assert not result.errors 47 | assert result.data == {'name': 'Yes, this is dog.'} 48 | 49 | 50 | def test_schema_creation_using_object_type_class(): 51 | schema = R.Schema(Dog) 52 | result = graphql(schema, '{ name }') 53 | assert not result.errors 54 | assert result.data == {'name': 'Yes, this is dog.'} 55 | -------------------------------------------------------------------------------- /tests/test_starwars/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GraphQL-python-archive/graphql-epoxy/17e67d96f503758273e7bc2f2baa6ba925052c92/tests/test_starwars/__init__.py -------------------------------------------------------------------------------- /tests/test_starwars/data.py: -------------------------------------------------------------------------------- 1 | from .schema import Human, Droid, Episode 2 | 3 | luke = Human( 4 | id='1000', 5 | name='Luke Skywalker', 6 | friends=['1002', '1003', '2000', '2001'], 7 | appears_in=[4, 5, 6], 8 | home_planet='Tatooine' 9 | ) 10 | 11 | vader = Human( 12 | id='1001', 13 | name='Darth Vader', 14 | friends=['1004'], 15 | appears_in=[4, 5, 6], 16 | home_planet=['Tatooine'] 17 | ) 18 | 19 | han = Human( 20 | id='1002', 21 | name='Han Solo', 22 | friends=['1000', '1003', '2001'], 23 | appears_in=[4, 5, 6] 24 | ) 25 | 26 | leia = Human( 27 | id='1003', 28 | name='Leia Organa', 29 | friends=['1000', '1002', '2000', '2001'], 30 | appears_in=[4, 5, 6], 31 | home_planet='Alderaan' 32 | ) 33 | 34 | tarkin = Human( 35 | id='1004', 36 | name='Wilhuff Tarkin', 37 | friends=['1001'], 38 | appears_in=[4] 39 | ) 40 | 41 | humans = {i.id: i for i in (luke, vader, han, leia, tarkin)} 42 | 43 | threepio = Droid( 44 | id='2000', 45 | name='C-3PO', 46 | friends=['1000', '1002', '1003', '2001'], 47 | appears_in=[4, 5, 6], 48 | primary_function='Protocol' 49 | ) 50 | 51 | artoo = Droid( 52 | id='2001', 53 | name='R2-D2', 54 | friends=['1000', '1002', '1003'], 55 | appears_in=[4, 5, 6], 56 | primary_function='Astromech' 57 | ) 58 | 59 | droids = {i.id: i for i in (threepio, artoo)} 60 | 61 | 62 | def get_character(id): 63 | return humans.get(id) or droids.get(id) 64 | 65 | 66 | def get_hero(episode): 67 | if episode == 5: 68 | return luke 69 | 70 | return artoo 71 | 72 | 73 | get_human = humans.get 74 | get_droid = droids.get 75 | 76 | 77 | def get_friends(character): 78 | return [get_character(id) for id in character.friends] 79 | -------------------------------------------------------------------------------- /tests/test_starwars/schema.py: -------------------------------------------------------------------------------- 1 | from graphql.core.type.definition import GraphQLArgument 2 | from epoxy.registry import TypeRegistry 3 | import enum 4 | 5 | R = TypeRegistry() 6 | 7 | 8 | @R.Register 9 | class Episode(enum.Enum): 10 | NEWHOPE = 4 11 | EMPIRE = 5 12 | JEDI = 6 13 | 14 | 15 | class Character(R.Interface): 16 | id = R.String.NonNull 17 | name = R.String 18 | friends = R.Character.List 19 | appears_in = R.Episode.List 20 | 21 | def resolve_friends(self, obj, args, info): 22 | from .data import get_friends 23 | return get_friends(obj) 24 | 25 | 26 | class Human(R.Implements.Character): 27 | home_planet = R.String 28 | 29 | 30 | class Droid(R.Implements.Character): 31 | primary_function = R.String 32 | 33 | 34 | class Query(R.ObjectType): 35 | # Args API will change. 36 | hero = R.Character(args={ 37 | 'episode': R.Episode 38 | }) 39 | 40 | human = R.Human(args={ 41 | 'id': R.String 42 | }) 43 | 44 | droid = R.Droid(args={ 45 | 'id': R.String 46 | }) 47 | 48 | def resolve_hero(self, obj, args, info): 49 | from .data import get_hero 50 | return get_hero(args.get('episode')) 51 | 52 | def resolve_human(self, obj, args, info): 53 | from .data import get_human 54 | return get_human(args['id']) 55 | 56 | def resolve_droid(self, obj, args, info): 57 | from .data import get_droid 58 | return get_droid(args['id']) 59 | 60 | 61 | StarWarsSchema = R.Schema(R.Query) 62 | -------------------------------------------------------------------------------- /tests/test_starwars/test_query.py: -------------------------------------------------------------------------------- 1 | from .schema import StarWarsSchema 2 | from graphql.core import graphql 3 | 4 | 5 | def test_hero_name_query(): 6 | query = ''' 7 | query HeroNameQuery { 8 | hero { 9 | name 10 | } 11 | } 12 | ''' 13 | expected = { 14 | 'hero': { 15 | 'name': 'R2-D2' 16 | } 17 | } 18 | result = graphql(StarWarsSchema, query) 19 | assert not result.errors 20 | assert result.data == expected 21 | 22 | 23 | def test_hero_name_and_friends_query(): 24 | query = ''' 25 | query HeroNameAndFriendsQuery { 26 | hero { 27 | id 28 | name 29 | friends { 30 | name 31 | } 32 | } 33 | } 34 | ''' 35 | expected = { 36 | 'hero': { 37 | 'id': '2001', 38 | 'name': 'R2-D2', 39 | 'friends': [ 40 | {'name': 'Luke Skywalker'}, 41 | {'name': 'Han Solo'}, 42 | {'name': 'Leia Organa'}, 43 | ] 44 | } 45 | } 46 | result = graphql(StarWarsSchema, query) 47 | assert not result.errors 48 | assert result.data == expected 49 | 50 | 51 | def test_nested_query(): 52 | query = ''' 53 | query NestedQuery { 54 | hero { 55 | name 56 | friends { 57 | name 58 | appearsIn 59 | friends { 60 | name 61 | } 62 | } 63 | } 64 | } 65 | ''' 66 | expected = { 67 | 'hero': { 68 | 'name': 'R2-D2', 69 | 'friends': [ 70 | { 71 | 'name': 'Luke Skywalker', 72 | 'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI'], 73 | 'friends': [ 74 | { 75 | 'name': 'Han Solo', 76 | }, 77 | { 78 | 'name': 'Leia Organa', 79 | }, 80 | { 81 | 'name': 'C-3PO', 82 | }, 83 | { 84 | 'name': 'R2-D2', 85 | }, 86 | ] 87 | }, 88 | { 89 | 'name': 'Han Solo', 90 | 'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI'], 91 | 'friends': [ 92 | { 93 | 'name': 'Luke Skywalker', 94 | }, 95 | { 96 | 'name': 'Leia Organa', 97 | }, 98 | { 99 | 'name': 'R2-D2', 100 | }, 101 | ] 102 | }, 103 | { 104 | 'name': 'Leia Organa', 105 | 'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI'], 106 | 'friends': [ 107 | { 108 | 'name': 'Luke Skywalker', 109 | }, 110 | { 111 | 'name': 'Han Solo', 112 | }, 113 | { 114 | 'name': 'C-3PO', 115 | }, 116 | { 117 | 'name': 'R2-D2', 118 | }, 119 | ] 120 | }, 121 | ] 122 | } 123 | } 124 | result = graphql(StarWarsSchema, query) 125 | assert not result.errors 126 | assert result.data == expected 127 | 128 | 129 | def test_fetch_luke_query(): 130 | query = ''' 131 | query FetchLukeQuery { 132 | human(id: "1000") { 133 | name 134 | } 135 | } 136 | ''' 137 | expected = { 138 | 'human': { 139 | 'name': 'Luke Skywalker', 140 | } 141 | } 142 | result = graphql(StarWarsSchema, query) 143 | assert not result.errors 144 | assert result.data == expected 145 | 146 | 147 | def test_fetch_some_id_query(): 148 | query = ''' 149 | query FetchSomeIDQuery($someId: String!) { 150 | human(id: $someId) { 151 | name 152 | } 153 | } 154 | ''' 155 | params = { 156 | 'someId': '1000', 157 | } 158 | expected = { 159 | 'human': { 160 | 'name': 'Luke Skywalker', 161 | } 162 | } 163 | result = graphql(StarWarsSchema, query, None, params) 164 | assert not result.errors 165 | assert result.data == expected 166 | 167 | 168 | def test_fetch_some_id_query2(): 169 | query = ''' 170 | query FetchSomeIDQuery($someId: String!) { 171 | human(id: $someId) { 172 | name 173 | } 174 | } 175 | ''' 176 | params = { 177 | 'someId': '1002', 178 | } 179 | expected = { 180 | 'human': { 181 | 'name': 'Han Solo', 182 | } 183 | } 184 | result = graphql(StarWarsSchema, query, None, params) 185 | assert not result.errors 186 | assert result.data == expected 187 | 188 | 189 | def test_invalid_id_query(): 190 | query = ''' 191 | query humanQuery($id: String!) { 192 | human(id: $id) { 193 | name 194 | } 195 | } 196 | ''' 197 | params = { 198 | 'id': 'not a valid id', 199 | } 200 | expected = { 201 | 'human': None 202 | } 203 | result = graphql(StarWarsSchema, query, None, params) 204 | assert not result.errors 205 | assert result.data == expected 206 | 207 | 208 | def test_fetch_luke_aliased(): 209 | query = ''' 210 | query FetchLukeAliased { 211 | luke: human(id: "1000") { 212 | name 213 | } 214 | } 215 | ''' 216 | expected = { 217 | 'luke': { 218 | 'name': 'Luke Skywalker', 219 | } 220 | } 221 | result = graphql(StarWarsSchema, query) 222 | assert not result.errors 223 | assert result.data == expected 224 | 225 | 226 | def test_fetch_luke_and_leia_aliased(): 227 | query = ''' 228 | query FetchLukeAndLeiaAliased { 229 | luke: human(id: "1000") { 230 | name 231 | } 232 | leia: human(id: "1003") { 233 | name 234 | } 235 | } 236 | ''' 237 | expected = { 238 | 'luke': { 239 | 'name': 'Luke Skywalker', 240 | }, 241 | 'leia': { 242 | 'name': 'Leia Organa', 243 | } 244 | } 245 | result = graphql(StarWarsSchema, query) 246 | assert not result.errors 247 | assert result.data == expected 248 | 249 | 250 | def test_duplicate_fields(): 251 | query = ''' 252 | query DuplicateFields { 253 | luke: human(id: "1000") { 254 | name 255 | homePlanet 256 | } 257 | leia: human(id: "1003") { 258 | name 259 | homePlanet 260 | } 261 | } 262 | ''' 263 | expected = { 264 | 'luke': { 265 | 'name': 'Luke Skywalker', 266 | 'homePlanet': 'Tatooine', 267 | }, 268 | 'leia': { 269 | 'name': 'Leia Organa', 270 | 'homePlanet': 'Alderaan', 271 | } 272 | } 273 | result = graphql(StarWarsSchema, query) 274 | assert not result.errors 275 | assert result.data == expected 276 | 277 | 278 | def test_use_fragment(): 279 | query = ''' 280 | query UseFragment { 281 | luke: human(id: "1000") { 282 | ...HumanFragment 283 | } 284 | leia: human(id: "1003") { 285 | ...HumanFragment 286 | } 287 | } 288 | fragment HumanFragment on Human { 289 | name 290 | homePlanet 291 | } 292 | ''' 293 | expected = { 294 | 'luke': { 295 | 'name': 'Luke Skywalker', 296 | 'homePlanet': 'Tatooine', 297 | }, 298 | 'leia': { 299 | 'name': 'Leia Organa', 300 | 'homePlanet': 'Alderaan', 301 | } 302 | } 303 | result = graphql(StarWarsSchema, query) 304 | assert not result.errors 305 | assert result.data == expected 306 | 307 | 308 | def test_check_type_of_r2(): 309 | query = ''' 310 | query CheckTypeOfR2 { 311 | hero { 312 | __typename 313 | name 314 | } 315 | } 316 | ''' 317 | expected = { 318 | 'hero': { 319 | '__typename': 'Droid', 320 | 'name': 'R2-D2', 321 | } 322 | } 323 | result = graphql(StarWarsSchema, query) 324 | assert not result.errors 325 | assert result.data == expected 326 | 327 | 328 | def test_check_type_of_luke(): 329 | query = ''' 330 | query CheckTypeOfLuke { 331 | hero(episode: EMPIRE) { 332 | __typename 333 | name 334 | } 335 | } 336 | ''' 337 | expected = { 338 | 'hero': { 339 | '__typename': 'Human', 340 | 'name': 'Luke Skywalker', 341 | } 342 | } 343 | result = graphql(StarWarsSchema, query) 344 | assert not result.errors 345 | assert result.data == expected 346 | -------------------------------------------------------------------------------- /tests/test_subscription.py: -------------------------------------------------------------------------------- 1 | from graphql.core import graphql 2 | 3 | from epoxy.registry import TypeRegistry 4 | 5 | 6 | def test_can_define_and_execute_subscription(): 7 | R = TypeRegistry() 8 | 9 | class Query(R.ObjectType): 10 | a = R.Int 11 | 12 | class Subscription(R.ObjectType): 13 | subscribe_to_foo = R.Boolean(args={'id': R.Int}) 14 | 15 | def resolve_subscribe_to_foo(self, obj, args, info): 16 | return args.get('id') == 1 17 | 18 | Schema = R.Schema(R.Query, subscription=R.Subscription) 19 | 20 | response = graphql(Schema, ''' 21 | subscription { 22 | subscribeToFoo(id: 1) 23 | } 24 | ''') 25 | 26 | assert not response.errors 27 | assert response.data == { 28 | 'subscribeToFoo': True 29 | } 30 | -------------------------------------------------------------------------------- /tests/test_unions.py: -------------------------------------------------------------------------------- 1 | from graphql.core import graphql 2 | from epoxy import TypeRegistry 3 | 4 | 5 | def test_runtime_type_resolution(): 6 | R = TypeRegistry() 7 | 8 | class Pet(R.Union[R.Dog, R.Cat]): 9 | pass 10 | 11 | class Dog(R.ObjectType): 12 | bark = R.String 13 | name = R.String 14 | 15 | class Cat(R.ObjectType): 16 | meow = R.String 17 | name = R.String 18 | 19 | class Query(R.ObjectType): 20 | pets = R.Pet.List 21 | 22 | schema = R.Schema(Query) 23 | 24 | data = Query(pets=[ 25 | Dog(name='Clifford', bark='Really big bark, because it\'s a really big dog.'), 26 | Cat(name='Garfield', meow='Lasagna') 27 | ]) 28 | 29 | result = graphql(schema, ''' 30 | { 31 | pets { 32 | __typename 33 | ... on Dog { 34 | name 35 | bark 36 | } 37 | 38 | ... on Cat { 39 | name 40 | meow 41 | } 42 | } 43 | } 44 | 45 | ''', data) 46 | assert not result.errors 47 | assert result.data == { 48 | 'pets': [{'__typename': 'Dog', 'bark': "Really big bark, because it's a really big dog.", 'name': 'Clifford'}, 49 | {'__typename': 'Cat', 'meow': 'Lasagna', 'name': 'Garfield'}] 50 | } 51 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = flake8,import-order,py35,py27,py33,py34,pypy 3 | skipsdist = true 4 | 5 | [testenv] 6 | whitelist_externals = 7 | dir 8 | deps = 9 | pytest>=2.7.2 10 | graphql-core>=0.4.12 11 | six>=1.10.0 12 | pytest-cov 13 | py{py,27,33}: enum34 14 | py{py,27,33,34}: singledispatch 15 | commands = 16 | py{py,27,33,34,35}: py.test tests {posargs} 17 | 18 | [testenv:flake8] 19 | basepython=python3.5 20 | deps = flake8 21 | commands = 22 | pip install -e . 23 | flake8 epoxy 24 | 25 | [testenv:import-order] 26 | basepython=python3.5 27 | deps = 28 | import-order 29 | graphql-core>=0.4.12 30 | six>=1.10.0 31 | commands = 32 | pip install -e . 33 | import-order epoxy 34 | --------------------------------------------------------------------------------