'''
GraphQL Types in Python
=======================
This module fulfill two purposes:
- declare GraphQL schema in Python, just declare classes inheriting
:class:`Type`, :class:`Interface` and fill them with
:class:`Field` (or base types: ``str``, ``int``, ``float``,
``bool``). You may as well declare :class:`Enum` with
``__choices__`` or :class:`Union` and ``__types__``. Then
``__str__()`` will provide nice printout and ``__repr__()`` will
return the GraphQL declarations (which can be tweaked with
``__to_graphql__()``, giving indent details). ``__bytes__()`` is
also provided, mapping to a compact ``__to_graphql__()`` version,
without indent.
- Interpret GraphQL JSON data, by instantiating the declared classes
with such information. While for scalar types it's just a
pass-thru, for :class:`Type` and :class:`Interface` these will use
the fields to provide native object with attribute or key access
mapping to JSON, instead of ``json_data['key']['other']`` you may
use ``obj.key.other``. Newly declared types, such as ``DateTime``
will take care to generate native Python objects (ie:
``datetime.datetime``). Setting such attributes will also update
the backing store object, including converting back to valid JSON
values.
These two improve usability of GraphQL **a lot**, pretty much like
Django's Model helps to access data bases.
:class:`Field` may be created explicitly, with information such as
target type, arguments and GraphQL name. However, more commonly these
are auto-generated by the container: GraphQL name, usually
``aFieldName`` will be created from Python name, usually
``a_field_name``. Basic types such as ``int``, ``str``, ``float`` or
``bool`` will map to ``Int``, ``String``, ``Float`` and ``Boolean``.
The end-user classes and functions provided by this module are:
- :class:`Schema`: top level object that will contain all
declarations. For single-schema applications, you don't have to
care about this since types declared without an explicit
``__schema__ = SchemaInstance`` member will end in the
``global_schema``.
- :class:`Scalar`: "pass thru" everything received. Base for other
scalar types:
* :class:`Int`: ``int``
* :class:`Float`: ``float``
* :class:`String`: ``str``
* :class:`Boolean`: ``bool``
* :class:`ID`: ``str``
- :class:`Enum`: also handled as a ``str``, but GraphQL syntax needs
them without the quotes, so special handling is done. Validation is
done using ``__choices__`` member, which is either a string (which
will be splitted using ``str.split()``) or a list/tuple of
strings with values.
- :class:`Union`: defines the target type of a field may be one of
the given ``__types__``.
- Container types: :class:`Type`, :class:`Interface` and
:class:`Input`. These are similar in usage, but GraphQL needs them
defined differently. They are composed of :class:`Field`. A field
may have arguments (:class:`ArgDict`), which is a set of
:class:`Arg`. Arguments may contain default values or
:class:`Variable`, which will be sent alongside the query (this
allows to generate the query once and use variables, letting the
server to use both together).
- :func:`non_null()`, maps to GraphQL ``Type!`` and enforces the
object is not ``None``.
- :func:`list_of()`, maps to GraphQL ``[Type]`` and enforces the
object is a list of ``Type``.
This module only provide built-in scalar types. However, three other
modules will extend the behavior for common conventions:
- :mod:`sgqlc.types.datetime` will declare ``DateTime``, ``Date`` and
``Time``, mapping to Python's :mod:`datetime`. This also allows
fields to be declared as ``my_date = datetime.date``,
- :mod:`sgqlc.types.uuid` will declare ``UUID``,
mapping to Python's :mod:`uuid`. This also allows
fields to be declared as ``my_uuid = uuid.UUID``,
- :mod:`sgqlc.types.relay` will declare ``Node`` and ``Connection``,
matching `Relay <https://facebook.github.io/relay/>`_ `Global
Object Identification
<https://facebook.github.io/relay/graphql/objectidentification.htm>`_
and `Cursor Connections
<https://facebook.github.io/relay/graphql/connections.htm>`_, which
are widely used.
Code Generator
--------------
If you already have ``schema.json`` or access to a server with
introspection you may use the ``sgqlc-codegen schema`` to
automatically generate the type definitions for you.
The generated code should be stable and can be committed to repositories,
leading to minimum ``diff`` when updated. It may include docstrings, which
improves development experience at the expense of larger files.
See examples:
- `GitHub
<https://github.com/profusion/sgqlc/blob/master/examples/github/update-schema.sh>`_
downloads the schema using introspection and generates a schema
using GraphQL descriptions as Python docstrings, see the generated
`github_schema.py
<https://github.com/profusion/sgqlc/blob/master/examples/github/github_schema.py>`_.
- `Shopify
<https://github.com/profusion/sgqlc/blob/master/examples/shopify/update-schema.sh>`_
downloads the schema (without descriptions) using introspection and
generates a schema without Python docstrings, see the generated
`shopify_schema.py
<https://github.com/profusion/sgqlc/blob/master/examples/shopify/shopify_schema.py>`_.
Examples
--------
Common Usage
~~~~~~~~~~~~
Common usage is to create :class:`Type` subclasses with fields without
arguments and do not use an explicit ``__schema__``, resulting in the
types being added to the ``global_schema``. Built-in scalars can be
declared using the Python classes, with :mod:`sgqlc.types` classes or
with explicit :class:`Field` instances, the :class:`ContainerTypeMeta`
takes care to make sure they are all instance of :class:`Field` at the
final class:
>>> class TypeUsingPython(Type):
... a_int = int
... a_float = float
... a_string = str
... a_boolean = bool
... a_id = id
... not_a_field = 1 # not a BaseType subclass or mapped python class
...
>>> TypeUsingPython # or repr(TypeUsingPython), prints out GraphQL!
type TypeUsingPython {
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
}
>>> TypeUsingPython.a_int # or repr(Field), prints out GraphQL!
aInt: Int
>>> TypeUsingPython.a_int.name
'a_int'
>>> TypeUsingPython.a_int.graphql_name # auto-generated from name
'aInt'
>>> TypeUsingPython.a_int.type # always a :mod:`sgqlc.types` class
scalar Int
>>> TypeUsingPython.__schema__ is global_schema
True
>>> global_schema # or repr(Schema), prints out GraphQL!
schema {
scalar Int
scalar Float
scalar String
scalar Boolean
scalar ID
type TypeUsingPython {
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
}
}
You can then use some standard Python operators to check fields in a
:class:`Type`:
>>> 'a_float' in TypeUsingPython
True
>>> 'x' in TypeUsingPython
False
>>> for field in TypeUsingPython: # iterates over :class:`Field`
... print(repr(field))
...
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
As mentioned, fields can be created with basic Python types (simpler),
with :mod:`sgqlc.types` or with :class:`Field` directly:
>>> class TypeUsingSGQLC(Type):
... a_int = Int
... a_float = Float
... a_string = String
... a_boolean = Boolean
... a_id = ID
...
>>> TypeUsingSGQLC # or repr(TypeUsingSGQLC), prints out GraphQL!
type TypeUsingSGQLC {
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
}
Using :class:`Field` instances
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Allows for greater control, such as explicitly define the
``graphql_name`` instead of generating one from the Python
``name``. It is also used to declare field arguments:
>>> class TypeUsingFields(Type):
... a_int = Field(int) # can use Python classes
... a_float = Field(float)
... a_string = Field(String) # or sgqlc.types classes
... a_boolean = Field(Boolean)
... a_id = Field(ID, graphql_name='anotherName') # allows customizations
... pow = Field(int, args={'base': int, 'exp': int}) # with arguments
... # more than 3 arguments renders each into new line
... many = Field(int, args={'a': int, 'b': int, 'c': int, 'd': int})
...
>>> TypeUsingFields # or repr(TypeUsingFields), prints out GraphQL!
type TypeUsingFields {
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
anotherName: ID
pow(base: Int, exp: Int): Int
many(
a: Int
b: Int
c: Int
d: Int
): Int
}
Adding types to specific :class:`Schema`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Create a schema instance and assign as ``__schema__`` class
member. Note that previously defined types in ``base_schema`` are
inherited, and by default ``global_schema`` is used as base schema:
>>> my_schema = Schema(global_schema)
>>> class MySchemaType(Type):
... __schema__ = my_schema
... i = int
...
>>> class MyOtherType(Type):
... i = int
...
>>> 'TypeUsingPython' in my_schema
True
>>> 'MySchemaType' in global_schema
False
>>> 'MySchemaType' in my_schema
True
>>> 'MyOtherType' in global_schema
True
>>> 'MyOtherType' in my_schema # added after my_schema was created!
False
>>> my_schema.MySchemaType # access types as schema attributes
type MySchemaType {
i: Int
}
>>> my_schema['MySchemaType'] # access types as schema items
type MySchemaType {
i: Int
}
>>> for t in my_schema: # doctest: +ELLIPSIS
... print(repr(t))
...
scalar Int
scalar Float
...
type MySchemaType {
i: Int
}
Inheritance and Interfaces
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Inheriting another type inherits all fields:
>>> class MySubclass(TypeUsingPython):
... sub_field = int
...
>>> MySubclass
type MySubclass {
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
subField: Int
}
Interfaces are similar, however they emit ``implements IfaceName``:
>>> class MyIface(Interface):
... sub_field = int
...
>>> MyIface
interface MyIface {
subField: Int
}
>>> class MySubclassWithIface(TypeUsingPython, MyIface):
... pass
...
>>> MySubclassWithIface
type MySubclassWithIface implements MyIface {
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
subField: Int
}
Although usually types are declared first, they can be declared after
interfaces as well. Note order of fields respect inheritance order:
>>> class MySubclassWithIface2(MyIface, TypeUsingPython):
... pass
...
>>> MySubclassWithIface2
type MySubclassWithIface2 implements MyIface {
subField: Int
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
}
Cross References (Loops)
~~~~~~~~~~~~~~~~~~~~~~~~
If types link to themselves, declare as strings so they are
lazy-evaluated:
>>> class LinkToItself(Type):
... other = Field('LinkToItself')
... non_null_other = non_null('LinkToItself')
... list_other = list_of('LinkToItself')
... list_other_non_null = list_of(non_null('LinkToItself'))
... non_null_list_other_non_null = non_null(list_of(non_null(
... 'LinkToItself')))
...
>>> LinkToItself
type LinkToItself {
other: LinkToItself
nonNullOther: LinkToItself!
listOther: [LinkToItself]
listOtherNonNull: [LinkToItself!]
nonNullListOtherNonNull: [LinkToItself!]!
}
>>> LinkToItself.other.type
type LinkToItself {
other: LinkToItself
nonNullOther: LinkToItself!
listOther: [LinkToItself]
listOtherNonNull: [LinkToItself!]
nonNullListOtherNonNull: [LinkToItself!]!
}
Also works for two unrelated types:
>>> class CrossLinkA(Type):
... other = Field('CrossLinkB')
... non_null_other = non_null('CrossLinkB')
... list_other = list_of('CrossLinkB')
... list_other_non_null = list_of(non_null('CrossLinkB'))
... non_null_list_other_non_null = non_null(list_of(non_null(
... 'CrossLinkB')))
...
>>> class CrossLinkB(Type):
... other = Field('CrossLinkA')
... non_null_other = non_null('CrossLinkA')
... list_other = list_of('CrossLinkA')
... list_other_non_null = list_of(non_null('CrossLinkA'))
... non_null_list_other_non_null = non_null(list_of(non_null(
... 'CrossLinkA')))
...
>>> CrossLinkA
type CrossLinkA {
other: CrossLinkB
nonNullOther: CrossLinkB!
listOther: [CrossLinkB]
listOtherNonNull: [CrossLinkB!]
nonNullListOtherNonNull: [CrossLinkB!]!
}
>>> CrossLinkB
type CrossLinkB {
other: CrossLinkA
nonNullOther: CrossLinkA!
listOther: [CrossLinkA]
listOtherNonNull: [CrossLinkA!]
nonNullListOtherNonNull: [CrossLinkA!]!
}
>>> CrossLinkA.other.type
type CrossLinkB {
other: CrossLinkA
nonNullOther: CrossLinkA!
listOther: [CrossLinkA]
listOtherNonNull: [CrossLinkA!]
nonNullListOtherNonNull: [CrossLinkA!]!
}
>>> CrossLinkB.other.type
type CrossLinkA {
other: CrossLinkB
nonNullOther: CrossLinkB!
listOther: [CrossLinkB]
listOtherNonNull: [CrossLinkB!]
nonNullListOtherNonNull: [CrossLinkB!]!
}
Special Attribute Names
~~~~~~~~~~~~~~~~~~~~~~~
Attributes starting with ``_`` are ignored, however if for some reason
you must use such attribute name, then declare **ALL** attributes
in that class (no need to repeat inherited attributes from interfaces)
using the ``__field_names__``, which should have a tuple of strings:
>>> class TypeUsingSpecialAttributes(Type):
... __field_names__ = ('_int', '_two_words')
... _int = int
... _two_words = str
... not_handled = float # not declared!
...
>>> TypeUsingSpecialAttributes # or repr(TypeUsingSpecialAttributes)
type TypeUsingSpecialAttributes {
_int: Int
_twoWords: String
}
>>> TypeUsingSpecialAttributes._int # or repr(Field), prints out GraphQL!
_int: Int
>>> TypeUsingSpecialAttributes._int.name
'_int'
>>> TypeUsingSpecialAttributes._int.graphql_name # auto-generated from name
'_int'
Note that while the leading underscores (``_``) are preserved, the rest of
internal underscores are converted to camel case:
>>> TypeUsingSpecialAttributes._two_words
_twoWords: String
>>> TypeUsingSpecialAttributes._two_words.name
'_two_words'
>>> TypeUsingSpecialAttributes._two_words.graphql_name
'_twoWords'
.. note::
Take care with the double underscores ``__`` as Python mangles
the name with the class name in order to "protect" and it will
result in ``AttributeError``
Note that undeclared fields won't be handled, but they still exist as regular
python attributes, in this case it references the ``float`` class:
>>> TypeUsingSpecialAttributes.not_handled
<class 'float'>
Non GraphQL Attributes
~~~~~~~~~~~~~~~~~~~~~~
The ``__field_names__`` may also be used to allow non-GraphQL
attributes that would otherwise be handled as such, this
explicitly limits the scope where SGQLC will handle.
Utilities
~~~~~~~~~
One can obtain fields as container attributes or items:
>>> TypeUsingPython.a_int
aInt: Int
>>> TypeUsingPython['a_int']
aInt: Int
However they raise exceptions if doesn't exist:
>>> TypeUsingPython.does_not_exist
Traceback (most recent call last):
...
AttributeError: TypeUsingPython has no field does_not_exist
>>> TypeUsingPython['does_not_exist']
Traceback (most recent call last):
...
KeyError: 'TypeUsingPython has no field does_not_exist'
Fields show in ``dir()`` alongside with non-fields (sorted):
>>> for name in dir(TypeUsingPython):
... if not name.startswith('_'):
... print(name)
a_boolean
a_float
a_id
a_int
a_string
not_a_field
Unless :func:`non_null` is used, containers can be created for
``None``:
>>> TypeUsingPython(None)
TypeUsingPython()
>>> TypeUsingPython.__to_json_value__(None) # returns None
For instances, field values can be obtained or set attributes or
items, when setting a known field, it also updates the backing store:
>>> json_data = {'aInt': 1}
>>> obj = TypeUsingPython(json_data)
>>> obj.a_int
1
>>> obj['a_int']
1
>>> obj.a_int = 2
>>> json_data['aInt']
2
>>> obj['a_int'] = 3
>>> json_data['aInt']
3
>>> obj['a_float'] = 2.1 # known field!
>>> json_data['aFloat']
2.1
>>> obj.a_float = 3.3 # known field!
>>> json_data['aFloat']
3.3
Unknown fields raise exceptions when obtained, but are allowed to be
set, however doesn't update the backing store:
>>> obj.does_not_exist
Traceback (most recent call last):
...
AttributeError: 'TypeUsingPython' object has no attribute 'does_not_exist'
>>> obj['does_not_exist']
Traceback (most recent call last):
...
KeyError: 'TypeUsingPython(a_int=3, a_float=3.3) has no field does_not_exist'
>>> obj['does_not_exist'] = 'abc' # unknown field, no updates to json_data
>>> json_data['does_not_exist']
Traceback (most recent call last):
...
KeyError: 'does_not_exist'
While ``repr()`` prints out summary in Python-friendly syntax, ``bytes()``
can be used to get compressed JSON with sorted keys:
>>> print(repr(obj))
TypeUsingPython(a_int=3, a_float=3.3)
>>> print(bytes(obj).decode('utf-8'))
{"aFloat":3.3,"aInt":3}
:license: ISC
'''
__docformat__ = 'reStructuredText en'
import json
import keyword
import re
from collections import OrderedDict
__all__ = (
'Schema',
'Scalar',
'Enum',
'Union',
'Variable',
'Arg',
'ArgDict',
'Field',
'Type',
'Interface',
'Input',
'Int',
'Float',
'String',
'Boolean',
'ID',
'non_null',
'list_of',
)
class ODict(OrderedDict):
def __getattr__(self, name):
try:
return self[name]
except KeyError as exc:
raise AttributeError('%s has no field %s' % (self, name)) from exc
[docs]class Schema:
'''The schema will contain declared types.
There is a default schema called ``global_schema``, a singleton
that is automatically assigned to every type that does not provide
its own schema.
Once types are constructed, they are automatically added to the
schema as properties of the same name, for example
:class:`Int` is exposed as ``schema.Int``,
``schema['Int']`` or ``schema.scalar['Int']``.
New schema will inherit the types defined at ``base_schema``,
which defaults to ``global_schema``, at the time of their
creation. However types added to ``base_schema`` after the schema
creation are not automatically picked by existing schema. The copy
happens at construction time.
New types may be added to schema using ``schema += type`` and
removed with ``schema -= type``. However those will not affect
their member ``type.__schema__``, which remains the same (where they
where originally created).
The schema is an iterator that will report all registered types.
'''
__slots__ = (
'__all',
'__kinds',
'__cache__',
'query_type',
'mutation_type',
'subscription_type',
)
[docs] def __init__(self, base_schema=None):
self.__all = OrderedDict()
self.__kinds = {}
self.__cache__ = {}
self.query_type = None
self.mutation_type = None
self.subscription_type = None
if base_schema is None:
try:
# on the first execution, global_schema is created, thus it's
# not available.
base_schema = global_schema
except NameError:
pass
if base_schema is not None:
self.__all.update(base_schema.__all)
for k, v in base_schema.__kinds.items():
self.__kinds.setdefault(k, ODict()).update(v)
[docs] def __contains__(self, key):
'''Checks if the type name is known in this schema.
Considering ``TypeUsingPython``, previously declared in the
module documentation:
>>> 'TypeUsingPython' in global_schema
True
>>> 'UnknownTypeName' in global_schema
False
'''
return key in self.__all
[docs] def __getitem__(self, key):
'''Get the type given its name.
Considering ``TypeUsingPython``, previously declared in the
module documentation:
>>> global_schema['TypeUsingPython']
type TypeUsingPython {
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
}
>>> global_schema['UnknownTypeName']
Traceback (most recent call last):
...
KeyError: 'UnknownTypeName'
'''
return self.__all[key]
[docs] def __getattr__(self, key):
'''Get the type using schema attribute.
Considering ``TypeUsingPython``, previously declared in the
module documentation:
>>> global_schema.TypeUsingPython
type TypeUsingPython {
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
}
>>> global_schema.UnknownTypeName
Traceback (most recent call last):
...
AttributeError: UnknownTypeName
One can use ``Schema.kind.Type`` syntax as well, it exposes an
:class:`ODict` object:
>>> global_schema.scalar.Int
scalar Int
>>> global_schema.scalar['Int']
scalar Int
>>> global_schema.scalar.UnknownTypeName # doctest: +ELLIPSIS
Traceback (most recent call last):
...
AttributeError: ... has no field UnknownTypeName
>>> global_schema.type.TypeUsingPython # doctest: +ELLIPSIS
type TypeUsingPython {
...
>>> for t in global_schema.type.values(): # doctest: +ELLIPSIS
... print(repr(t))
...
type TypeUsingPython {
...
type TypeUsingSGQLC {
...
type TypeUsingFields {
...
type MyOtherType {
...
type MyType {
...
}
'''
try:
return self.__kinds[key] # .type, .scalar, etc...
except KeyError:
pass
try:
return self.__all[key]
except KeyError as exc:
raise AttributeError(key) from exc
[docs] def __iter__(self):
'''Schema provides an iterator over :class:`BaseType`
subclasses:
>>> for t in global_schema: # doctest: +ELLIPSIS
... print(repr(t))
...
scalar Int
scalar Float
...
type MyType {
...
}
'''
return iter(self.__all.values())
[docs] def __iadd__(self, typ):
'''Manually add a type to the schema.
Types are automatically once their class is created. Only use
this if you're copying a type from one schema to another.
Note that the type name ``str(typ)`` must not exist in the
schema, otherwise :class:`ValueError` is raised.
To remove a type, use ``schema -= typ``.
As explained in the :mod:`sgqlc.types` documentation, the
newly created schema will inherit types from the base schema
only at creation time:
>>> my_schema = Schema(global_schema)
>>> class MySchemaType(Type):
... __schema__ = my_schema
... i = int
...
>>> 'MySchemaType' in global_schema
False
>>> 'MySchemaType' in my_schema
True
But ``__iadd__`` and ``__isub__`` can be used to add or remove
types:
>>> global_schema += MySchemaType
>>> 'MySchemaType' in global_schema
True
>>> global_schema -= MySchemaType
>>> 'MySchemaType' in global_schema
False
Note that different type with the same name can't be added:
>>> my_schema2 = Schema(global_schema)
>>> class MySchemaType(Type): # redefining, different schema: ok
... __schema__ = my_schema2
... f = float
...
>>> my_schema += MySchemaType
Traceback (most recent call last):
...
ValueError: Schema already has MySchemaType=MySchemaType
'''
name = typ.__name__
t = self.__all.setdefault(name, typ)
if t is not typ:
raise ValueError(
'%s already has %s=%s' % (self.__class__.__name__, name, typ)
)
self.__kinds.setdefault(typ.__kind__, ODict()).update({name: typ})
if self.query_type is None and name == 'Query':
self.query_type = t
if self.mutation_type is None and name == 'Mutation':
self.mutation_type = t
if self.subscription_type is None and name == 'Subscription':
self.subscription_type = t # pragma: no cover
return self
[docs] def __isub__(self, typ):
'''Remove a type from the schema.
This may be of use to override some type, such as
:class:`sgqlc.types.datetime.Date` or
:class:`sgqlc.types.datetime.DateTime`.
'''
name = typ.__name__
del self.__all[name]
del self.__kinds[typ.__kind__][name]
if self.query_type is typ:
self.query_type = None # pragma: no cover
elif self.mutation_type is typ:
self.mutation_type = None # pragma: no cover
elif self.subscription_type is typ:
self.subscription_type = None # pragma: no cover
return self
[docs] def __str__(self):
'''Short schema, using only type names.
Instead of declaring the whole schema in GraphQL notation as
done by ``repr()``, just list the type names:
>>> print(str(global_schema)) # doctest: +ELLIPSIS
{Int, Float, String, Boolean, ID, ...}
'''
return '{' + ', '.join(str(e) for e in self) + '}'
def __to_graphql__(self, indent=0, indent_string=' '):
prefix = indent_string * indent
s = [prefix + 'schema {']
s.extend(e.__to_graphql__(indent + 1, indent_string) for e in self)
s.append(prefix + '}')
return '\n'.join(s)
[docs] def __repr__(self):
return self.__to_graphql__()
[docs] def __bytes__(self):
'''GraphQL schema without indentation.
>>> print(bytes(global_schema).decode('utf-8')) # doctest: +ELLIPSIS
schema {
scalar Int
scalar Float
scalar String
...
}
'''
return bytes(self.__to_graphql__(indent_string=''), 'utf-8')
global_schema = Schema()
[docs]class BaseType(metaclass=BaseMeta):
'''Base shared by all GraphQL classes.'''
__schema__ = global_schema
__kind__ = None
class BaseMetaWithTypename(BaseMeta):
'BaseMeta with ``__typename`` field (containers and union).'
def __init__(cls, name, bases, namespace):
super(BaseMetaWithTypename, cls).__init__(name, bases, namespace)
if not bases or BaseType in bases or BaseTypeWithTypename in bases:
return
cls.__populate_meta_fields()
def __populate_meta_fields(cls):
field = Field(non_null('String'), '__typename')
field._set_container(cls.__schema__, cls, '__typename__')
cls.__meta_fields__ = {
'__typename__': field,
}
class BaseTypeWithTypename(BaseType, metaclass=BaseMetaWithTypename):
'BaseType with ``__typename`` field (containers and union).'
def get_variable_or_none(v):
if v is None:
return v
if isinstance(v, Variable):
return v
raise ValueError('expected None or Variable, got %s' % (type(v),))
def get_variable_or_none_input(v, indent, indent_string):
if v is None:
return 'null'
if isinstance(v, Variable):
return Variable.__to_graphql_input__(v, indent, indent_string)
raise ValueError('expected None or Variable, got %s' % (type(v),))
def create_realize_type(t):
def realize_type(v, selection_list=None):
if isinstance(v, (t, Variable)):
return v
return t(v, selection_list)
return realize_type
def _create_non_null_wrapper(name, t):
'creates type wrapper for non-null of given type'
realize_type = create_realize_type(t)
def __new__(cls, json_data, selection_list=None):
if json_data is None:
raise ValueError(name + ' received null value')
return realize_type(json_data, selection_list)
def __to_graphql_input__(value, indent=0, indent_string=' '):
value = realize_type(value)
return t.__to_graphql_input__(value, indent, indent_string)
return type(
name,
(t,),
{
'__new__': __new__,
'_%s__auto_register' % name: False,
'__to_graphql_input__': __to_graphql_input__,
},
)
def get_list_input(t, realize_type, value, indent, indent_string):
try:
return get_variable_or_none_input(value, indent, indent_string)
except ValueError:
pass
def convert(v):
return t.__to_graphql_input__(realize_type(v), indent, indent_string)
return '[' + ', '.join(convert(v) for v in value) + ']'
def get_list_json(t, value):
try:
return get_variable_or_none(value)
except ValueError:
pass
return [t.__to_json_value__(v) for v in value]
def _create_list_of_wrapper(name, t):
'creates type wrapper for list of given type'
realize_type = create_realize_type(t)
def __new__(cls, json_data, selection_list=None):
try:
return get_variable_or_none(json_data)
except ValueError:
pass
return [realize_type(v, selection_list) for v in json_data]
def __to_graphql_input__(value, indent=0, indent_string=' '):
return get_list_input(t, realize_type, value, indent, indent_string)
def __to_json_value__(value):
return get_list_json(t, value)
return type(
name,
(t,),
{
'__new__': __new__,
'_%s__auto_register' % name: False,
'__to_graphql_input__': __to_graphql_input__,
'__to_json_value__': __to_json_value__,
},
)
class Lazy:
'''Holds a type name until it's created.
This is used to solve cross reference problems, the fields will
store an instance of this until it's evaluated.
'''
__slots__ = ('name', 'target_name', 'ready_cb', 'inner_lazy')
def __init__(self, name, target_name, ready_cb, inner_lazy=None):
self.name = name
self.target_name = target_name
self.ready_cb = ready_cb
self.inner_lazy = inner_lazy
def __repr__(self):
'''Print out pending Lazy
>>> Lazy('a', 'a!', lambda x: x)
<Lazy name='a' target_name='a!'>
'''
return str(self)
def __str__(self):
return '<Lazy name=%r target_name=%r>' % (self.name, self.target_name)
def resolve(self, schema):
trail = []
lazy = self
while lazy.inner_lazy is not None:
trail.append(lazy)
lazy = lazy.inner_lazy
t = lazy.ready_cb(schema[lazy.name])
while trail:
lazy = trail.pop()
t = lazy.ready_cb(t)
return t
[docs]def non_null(t):
'''Generates non-null type (t!)
>>> class TypeWithNonNullFields(Type):
... a_int = non_null(int)
... a_float = non_null(Float)
... a_string = Field(non_null(String))
...
>>> TypeWithNonNullFields
type TypeWithNonNullFields {
aInt: Int!
aFloat: Float!
aString: String!
}
Giving proper JSON data:
>>> json_data = {'aInt': 1, 'aFloat': 2.1, 'aString': 'hello'}
>>> obj = TypeWithNonNullFields(json_data)
>>> obj
TypeWithNonNullFields(a_int=1, a_float=2.1, a_string='hello')
Giving incorrect JSON data:
>>> json_data = {'aInt': None, 'aFloat': 2.1, 'aString': 'hello'}
>>> obj = TypeWithNonNullFields(json_data) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: TypeWithNonNullFields selection 'a_int': ...
>>> json_data = {'aInt': 1, 'aFloat': None, 'aString': 'hello'}
>>> obj = TypeWithNonNullFields(json_data) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: TypeWithNonNullFields selection 'a_float': ...
>>> json_data = {'aInt': 1, 'aFloat': 2.1, 'aString': None}
>>> obj = TypeWithNonNullFields(json_data) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: TypeWithNonNullFields selection 'a_string': ...
.. note::
Note that **missing** keys in JSON data are not considered
``None``, and they won't show in ``iter(obj)``, ``__str__()``
or ``__repr__()``
>>> json_data = {'aInt': 1, 'aFloat': 2.1}
>>> obj = TypeWithNonNullFields(json_data)
>>> obj # repr()
TypeWithNonNullFields(a_int=1, a_float=2.1)
>>> for field_name in obj:
... print(field_name, repr(obj[field_name]))
...
a_int 1
a_float 2.1
'''
if isinstance(t, str):
return Lazy(t, t + '!', non_null)
elif isinstance(t, Lazy):
return Lazy(t.target_name, t.target_name + '!', non_null, t)
t = BaseType.__ensure__(t)
name = t.__name__ + '!'
try:
return t.__schema__.__cache__[name]
except KeyError:
pass
wrapper = _create_non_null_wrapper(name, t)
t.__schema__.__cache__[name] = wrapper
return wrapper
[docs]def list_of(t):
'''Generates list of types ([t])
The example below highlights the usage including its usage with
lists:
- ``non_null_list_of_int`` means it must be a list, not ``None``,
however list elements may be ``None``, ie: ``[None, 1, None, 2]``;
- ``list_of_non_null_int`` means it may be ``None`` or be a list,
however list elements must not be ``None``, ie: ``None`` or
``[1, 2]``;
- ``non_null_list_of_non_null_int`` means it must be a list, not
``None`` **and** the list elements must not be ``None``, ie:
``[1, 2]``.
>>> class TypeWithListFields(Type):
... list_of_int = list_of(int)
... list_of_float = list_of(Float)
... list_of_string = Field(list_of(String))
... non_null_list_of_int = non_null(list_of(int))
... list_of_non_null_int = list_of(non_null(int))
... non_null_list_of_non_null_int = non_null(list_of(non_null(int)))
...
>>> TypeWithListFields
type TypeWithListFields {
listOfInt: [Int]
listOfFloat: [Float]
listOfString: [String]
nonNullListOfInt: [Int]!
listOfNonNullInt: [Int!]
nonNullListOfNonNullInt: [Int!]!
}
It takes care to enforce proper type, including non-null checking
on its elements when creating instances. Giving proper JSON data:
>>> json_data = {
... 'listOfInt': [1, 2],
... 'listOfFloat': [1.1, 2.1],
... 'listOfString': ['hello', 'world'],
... 'nonNullListOfInt': [None, 1, None, 2],
... 'listOfNonNullInt': [1, 2, 3],
... 'nonNullListOfNonNullInt': [1, 2, 3, 4],
... }
>>> obj = TypeWithListFields(json_data)
>>> for field_name in obj:
... print(field_name, repr(obj[field_name]))
...
list_of_int [1, 2]
list_of_float [1.1, 2.1]
list_of_string ['hello', 'world']
non_null_list_of_int [None, 1, None, 2]
list_of_non_null_int [1, 2, 3]
non_null_list_of_non_null_int [1, 2, 3, 4]
Note that lists that are **not** enclosed in ``non_null()`` can be
``None``:
>>> json_data = {
... 'listOfInt': None,
... 'listOfFloat': None,
... 'listOfString': None,
... 'nonNullListOfInt': [None, 1, None, 2],
... 'listOfNonNullInt': None,
... 'nonNullListOfNonNullInt': [1, 2, 3],
... }
>>> obj = TypeWithListFields(json_data)
>>> for field_name in obj:
... print(field_name, repr(obj[field_name]))
...
list_of_int None
list_of_float None
list_of_string None
non_null_list_of_int [None, 1, None, 2]
list_of_non_null_int None
non_null_list_of_non_null_int [1, 2, 3]
Types will be converted, so although not usual (since GraphQL
gives you the proper JSON type), this can be done:
>>> json_data = {
... 'listOfInt': ['1', '2'],
... 'listOfFloat': [1, '2.1'],
... 'listOfString': ['hello', 2],
... 'nonNullListOfInt': [None, '1', None, 2.1],
... 'listOfNonNullInt': ['1', 2.1, 3],
... 'nonNullListOfNonNullInt': ['1', 2.1, 3, 4],
... }
>>> obj = TypeWithListFields(json_data)
>>> for field_name in obj:
... print(field_name, repr(obj[field_name]))
...
list_of_int [1, 2]
list_of_float [1.0, 2.1]
list_of_string ['hello', '2']
non_null_list_of_int [None, 1, None, 2]
list_of_non_null_int [1, 2, 3]
non_null_list_of_non_null_int [1, 2, 3, 4]
Giving incorrect (nonconvertible) JSON data will raise exceptions:
>>> json_data = { 'listOfInt': 1 }
>>> obj = TypeWithListFields(json_data) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: TypeWithListFields selection 'list_of_int': ...
>>> json_data = { 'listOfInt': ['x'] }
>>> obj = TypeWithListFields(json_data) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: TypeWithListFields selection 'list_of_int': ...
>>> json_data = { 'listOfNonNullInt': [1, None] }
>>> obj = TypeWithListFields(json_data) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: TypeWithListFields selection 'list_of_non_null_int': ...
Lists are usable as input types as well:
>>> class TypeWithListInput(Type):
... a = Field(str, args={'values': Arg(list_of(int), default=[1, 2])})
... b = Field(str, args={'values': Arg(list_of(int))})
...
>>> TypeWithListInput
type TypeWithListInput {
a(values: [Int] = [1, 2]): String
b(values: [Int]): String
}
>>> print(json.dumps(list_of(int).__to_json_value__([1, 2])))
[1, 2]
>>> print(json.dumps(list_of(int).__to_json_value__(None)))
null
Lists can be of complex types, for instance :class:`Input`:
>>> class SomeInput(Input):
... a = int
>>> SomeInputList = list_of(SomeInput)
>>> SomeInputList([{'a': 123}])
[SomeInput(a=123)]
Variables may be given as constructor parameters:
>>> SomeInputList(Variable('lst'))
$lst
Or already realized lists:
>>> SomeInputList(SomeInputList([{'a': 123}]))
[SomeInput(a=123)]
'''
if isinstance(t, str):
return Lazy(t, '[' + t + ']', list_of)
elif isinstance(t, Lazy):
return Lazy(t.target_name, '[' + t.target_name + ']', list_of, t)
t = BaseType.__ensure__(t)
name = '[' + t.__name__ + ']'
try:
return t.__schema__.__cache__[name]
except KeyError:
pass
wrapper = _create_list_of_wrapper(name, t)
t.__schema__.__cache__[name] = wrapper
return wrapper
[docs]class Scalar(BaseType):
'''Basic scalar types, passed thru (no conversion).
This may be used directly if no special checks or conversions are
needed. Otherwise use subclasses, like :class:`Int`,
:class:`Float`, :class:`String`, :class:`Boolean`, :class:`ID`...
Scalar classes will never produce instance of themselves, rather
return the converted value (int, bool...)
>>> class MyTypeWithScalar(Type):
... v = Scalar
...
>>> MyTypeWithScalar({'v': 1}).v
1
>>> MyTypeWithScalar({'v': 'abc'}).v
'abc'
Variables are passed thru:
>>> MyTypeWithScalar({'v': Variable('var')}).v
$var
'''
__kind__ = 'scalar'
__json_dump_args__ = {} # given to json.dumps(obj, **args)
def converter(value):
return value
[docs] def __new__(cls, json_data, selection_list=None):
try:
return get_variable_or_none(json_data)
except ValueError:
pass
return cls.converter(json_data)
@classmethod
def __to_graphql_input__(cls, value, indent=0, indent_string=' '):
if hasattr(value, '__to_graphql_input__'):
return value.__to_graphql_input__(value, indent, indent_string)
return json.dumps(
cls.__to_json_value__(value), **cls.__json_dump_args__
)
@classmethod
def __to_json_value__(cls, value):
return value
class EnumMeta(BaseMeta):
'meta class to set enumeration attributes, __contains__, __iter__...'
def __init__(cls, name, bases, namespace):
super(EnumMeta, cls).__init__(name, bases, namespace)
if isinstance(cls.__choices__, str):
cls.__choices__ = tuple(cls.__choices__.split())
else:
cls.__choices__ = tuple(cls.__choices__)
for v in cls.__choices__:
setattr(cls, v, v)
def __contains__(cls, v):
return v in cls.__choices__
def __iter__(cls):
return iter(cls.__choices__)
def __len__(cls):
return len(cls.__choices__)
def __to_graphql__(cls, indent=0, indent_string=' '):
s = [BaseMeta.__to_graphql__(cls, indent, indent_string) + ' {']
prefix = indent_string * (indent + 1)
for c in cls:
s.append(prefix + str(c))
s.append(indent_string * indent + '}')
return '\n'.join(s)
def __to_graphql_input__(cls, value, indent=0, indent_string=' '):
if value is None:
return 'null'
return value
def __to_json_value__(cls, value):
return value
[docs]class Enum(BaseType, metaclass=EnumMeta):
'''This is an abstract class that enumerations should inherit
and define ``__choices__`` class member with a list of strings
matching the choices allowed by this enumeration. A single string
may also be used, in such case it will be split using
``str.split()``.
Note that ``__choices__`` is not set in the final class, the
metaclass will use that to build members and provide the
``__iter__``, ``__contains__`` and ``__len__`` instead.
The instance constructor will never return instance of
:class:`Enum`, rather the string, if that matches.
Examples:
>>> class Colors(Enum):
... __choices__ = ('RED', 'GREEN', 'BLUE')
...
>>> Colors('RED')
'RED'
>>> Colors(None) # returns None
>>> Colors('MAGENTA')
Traceback (most recent call last):
...
ValueError: Colors does not accept value MAGENTA
Using a string will automatically split and convert to tuple:
>>> class Fruits(Enum):
... __choices__ = 'APPLE ORANGE BANANA'
...
>>> Fruits.__choices__
('APPLE', 'ORANGE', 'BANANA')
>>> len(Fruits)
3
Enumerations have a special syntax in GraphQL, no quotes:
>>> print(Fruits.__to_graphql_input__(Fruits.APPLE))
APPLE
>>> print(Fruits.__to_graphql_input__(None))
null
And for JSON it's a string as well (so JSON encoder adds quotes):
>>> print(json.dumps(Fruits.__to_json_value__(Fruits.APPLE)))
"APPLE"
Variables are passed thru:
>>> Fruits(Variable('var'))
$var
'''
__kind__ = 'enum'
__choices__ = ()
[docs] def __new__(cls, json_data, selection_list=None):
try:
return get_variable_or_none(json_data)
except ValueError:
pass
if json_data not in cls:
raise ValueError('%s does not accept value %s' % (cls, json_data))
return json_data
class UnionMeta(BaseMetaWithTypename):
'meta class to set __types__ as :class:`BaseType` instances'
def __init__(cls, name, bases, namespace):
super(UnionMeta, cls).__init__(name, bases, namespace)
if not cls.__types__ and BaseTypeWithTypename not in bases:
raise ValueError(name + ': missing __types__')
types = []
for t in cls.__types__:
if isinstance(t, str):
t = cls.__schema__[t]
else:
t = BaseType.__ensure__(t)
types.append(t)
cls.__types__ = tuple(types)
cls.__typename_to_type__ = {t.__name__: t for t in types}
def __contains__(cls, name_or_type):
if isinstance(name_or_type, str):
name_or_type = cls.__schema__[name_or_type]
else:
name_or_type = BaseType.__ensure__(name_or_type)
return name_or_type in cls.__types__
def __iter__(cls):
return iter(cls.__types__)
def __len__(cls):
return len(cls.__types__)
def __to_graphql__(cls, indent=0, indent_string=' '):
suffix = ' = ' + ' | '.join(str(c) for c in cls.__types__)
return BaseMeta.__to_graphql__(cls, indent, indent_string) + suffix
def __getitem__(cls, key):
return cls.__meta_fields__[key]
[docs]class Union(BaseTypeWithTypename, metaclass=UnionMeta):
'''This is an abstract class that union of multiple types should
inherit and define ``__types__``, a list of pre-defined
:class:`Type`.
>>> class IntOrFloatOrString(Union):
... __types__ = (Int, float, 'String')
...
>>> IntOrFloatOrString # or repr(), prints out GraphQL!
union IntOrFloatOrString = Int | Float | String
>>> Int in IntOrFloatOrString
True
>>> 'Int' in IntOrFloatOrString # may use type names as well
True
>>> int in IntOrFloatOrString # may use native Python types as well
True
>>> ID in IntOrFloatOrString
False
>>> len(IntOrFloatOrString)
3
>>> for t in IntOrFloatOrString:
... print(repr(t))
scalar Int
scalar Float
scalar String
Failing to define types will raise exception:
>>> class FailureUnion(Union):
... pass
Traceback (most recent call last):
...
ValueError: FailureUnion: missing __types__
Whenever instantiating the type, pass a JSON object with
``__typename`` (done automatically using fragments via ``__as__``):
>>> class TypeA(Type):
... i = int
...
>>> class TypeB(Type):
... s = str
...
>>> class TypeU(Union):
... __types__ = (TypeA, TypeB)
...
>>> data = {'__typename': 'TypeA', 'i': 1}
>>> TypeU(data)
TypeA(i=1)
>>> data = {'__typename': 'TypeB', 's': 'hi'}
>>> TypeU(data)
TypeB(s='hi')
It nicely handles unknown types:
>>> data = {'v': 123}
>>> TypeU(data) # no __typename
UnknownType()
>>> data = {'__typename': 'TypeUnknown', 'v': 123}
>>> TypeU(data) # auto-generates empty types
TypeUnknown()
>>> data = None
>>> TypeU(data)
Variables are passed thru:
>>> TypeU(Variable('var'))
$var
'''
__kind__ = 'union'
__types__ = ()
[docs] def __new__(cls, json_data, selection_list=None):
try:
return get_variable_or_none(json_data)
except ValueError:
pass
type_name = json_data.get('__typename')
if not type_name:
t = UnknownType
else:
t = cls.__typename_to_type__.get(type_name)
if t is None:
t = type(type_name, (UnknownType,), {})
cls.__typename_to_type__[type_name] = t
return t(json_data, selection_list)
[docs]class ContainerType(BaseTypeWithTypename, metaclass=ContainerTypeMeta):
'''Container of :class:`Field`.
For ease of use, fields can be declared by sub classes in the
following ways:
- ``name = str`` to create a simple string field. Other basic
types are allowed as well: ``int``, ``float``, ``str``,
``bool``, ``uuid.UUID``, ``datetime.time``,
``datetime.date`` and ``datetime.datetime``. These are
only used as identifiers to translate using
``map_python_to_graphql`` dict. Note that
``id``, although is not a type, maps to ``ID``.
- ``name = TypeName`` for subclasses of ``BaseType``, such
as pre-defined scalars (:class:`Int`, etc) or your own defined
types, from :class:`Type`.
- ``name = Field(TypeName, graphql_name='differentName',
args={...})`` to explicitly define more field information,
such as GraphQL JSON name, query parameters, etc.
The metaclass :class:`ContainerTypeMeta` will normalize all of those
members to be instances of :class:`Field`, as well as provide
useful container protocol such as ``__contains__``,
``__getitem__``, ``__iter__`` and so on.
Fields from all bases (interfaces, etc) are merged.
Members started with underscore (``_``) are not processed.
'''
__json_dump_args__ = {
# given to json.dumps() in __bytes__()
'sort_keys': True,
'separators': (',', ':'),
}
[docs] def __init__(self, json_data, selection_list=None):
assert json_data is None or isinstance(
json_data, dict
), '%r (%s) is not a JSON Object' % (
json_data,
type(json_data).__name__,
)
object.__setattr__(self, '__selection_list__', selection_list)
self.__populate_fields(json_data)
def __populate_fields(self, json_data):
cache = OrderedDict()
object.__setattr__(self, '__fields_cache__', cache)
if json_data is None:
# backing store, changed by setattr()
object.__setattr__(self, '__json_data__', {})
return
if self.__selection_list__ is not None:
self.__populate_fields_from_selection_list(
self.__selection_list__, json_data
)
else:
for field in self.__class__:
self.__populate_field_data(field, field.type, None, json_data)
# backing store, changed by setattr()
object.__setattr__(self, '__json_data__', json_data)
def __populate_field_data(self, field, ftype, sel, json_data):
name = field.name
graphql_name = field.graphql_name
if graphql_name not in json_data:
return
value = None
try:
value = json_data[graphql_name]
value = ftype(value, sel)
setattr(self, name, value)
self.__fields_cache__[name] = field
except Exception as exc:
raise ValueError(
'%s selection %r: %r (%s)' % (self.__class__, name, value, exc)
) from exc
def __populate_fields_from_selection_list(self, sl, json_data):
selections = sl.__get_selections_or_auto_select__()
for sel in selections:
field = sel.__field__
ftype = self.__get_type_for_selection(sel, json_data)
if sel.__alias__ is not None:
alias = sel.__alias__
field = Field(ftype, alias, field.args)
field._set_container(self.__schema__, self, alias)
self.__populate_field_data(field, ftype, sel, json_data)
if isinstance(selections, list):
# sl is SelectionList or subclass (InlineFragmentSelectionList)
casts = sl.__casts__
fragments = sl.__fragments__
else:
# sl is Selection, use possibly auto-selected fields
casts = selections.__casts__
fragments = selections.__fragments__
if casts:
tname = json_data.get('__typename')
csl = casts.get(tname)
if csl:
self.__populate_fields_from_selection_list(csl, json_data)
if fragments:
tname = json_data.get('__typename')
fl = fragments.get(tname)
if fl:
for f in fl:
self.__populate_fields_from_selection_list(f, json_data)
@staticmethod
def __get_type_for_selection(sel, json_data):
field = sel.__field__
ftype = field.type
casts = sel.__casts__
if not casts:
return ftype
graphql_name = field.graphql_name
try:
tname = json_data.get(graphql_name, {}).get('__typename')
except AttributeError:
# The selection returned something other than a dict, e.g. a list.
# Nothing to worry about, it just means this object doesn't contain
# a typename.
tname = None
if not tname:
return ftype
sl = casts.get(tname)
if sl is None:
return ftype
return sl.__type__
[docs] def __setattr__(self, name, value):
'''Sets the attribute value, if a :class:`Field` updates backing store.
Considering ``TypeUsingPython``, previously declared in the
module documentation:
>>> json_data = {'aInt': 1, 'aFloat': 2.1}
>>> obj = global_schema.TypeUsingPython(json_data)
>>> obj.a_int, obj.a_float
(1, 2.1)
>>> obj.a_int = 123
>>> obj.a_int, obj.a_float
(123, 2.1)
>>> json_data['aInt']
123
However that's valid for known :class:`Field` for the given
:class:`ContainerType` subclasses:
>>> obj.new_attr = 'some value' # no field, no backing store updates
>>> obj.new_attr
'some value'
>>> json_data['new_attr']
Traceback (most recent call last):
...
KeyError: 'new_attr'
>>> json_data['newAttr']
Traceback (most recent call last):
...
KeyError: 'newAttr'
'''
object.__setattr__(self, name, value)
if not hasattr(self, '__json_data__'): # still populating
return
# apply changes to json backing store, if name is known
field = self.__fields_cache__.get(name)
if field is None:
field = getattr(self.__class__, name, None)
if field is None:
return
self.__fields_cache__[name] = field
json_value = field.type.__to_json_value__(value)
self.__json_data__[field.graphql_name] = json_value
[docs] def __getitem__(self, name):
'''Get the field given its name.
Considering ``TypeUsingPython``, previously declared in the
module documentation:
>>> global_schema.TypeUsingPython['a_int']
aInt: Int
>>> global_schema.TypeUsingPython['unknown_field']
Traceback (most recent call last):
...
KeyError: 'TypeUsingPython has no field unknown_field'
'''
try:
return getattr(self, name)
except AttributeError as exc:
raise KeyError('%s has no field %s' % (self, name)) from exc
[docs] def __setitem__(self, name, value):
'''Set the item, maps to ``setattr(self, name, value)``'''
setattr(self, name, value)
[docs] def __iter__(self):
'''Iterate over known fields of the **instance**.
Unlike ``iter(SubclassOfType)``, which iterates over all
declared fields, this iterator matches only fields that exist
in the object, based on the ``json_data`` used to create the
object, and the one that provides the backing store:
>>> json_data = { 'aInt': 1, 'aFloat': 2.1 }
>>> obj = global_schema.TypeUsingPython(json_data)
>>> for field_name in obj:
... print(field_name, repr(obj[field_name]))
a_int 1
a_float 2.1
>>> for field in obj.__class__:
... print(repr(field))
aInt: Int
aFloat: Float
aString: String
aBoolean: Boolean
aId: ID
After it's set for the given instance, then it's included in
the iterator:
>>> obj.a_string = 'hello world' # known field
>>> for field_name in obj:
... print(field_name, repr(obj[field_name]))
a_int 1
a_float 2.1
a_string 'hello world'
However that's valid for known :class:`Field` for the given
:class:`ContainerType` subclasses:
>>> obj.new_attr = 'some value' # unknown field, not in 'iter'
>>> for field_name in obj:
... print(field_name, repr(obj[field_name]))
a_int 1
a_float 2.1
a_string 'hello world'
'''
return iter(self.__fields_cache__.keys())
[docs] def __contains__(self, name):
'''Checks if for a known field name in the **instance**.
Unlike ``name in SubclassOfType``, which checks amongst all
declared fields, this matches only fields that exist
in the object, based on the ``json_data`` used to create the
object, and the one that provides the backing store:
>>> json_data = { 'aInt': 1, 'aFloat': 2.1 }
>>> obj = global_schema.TypeUsingPython(json_data)
>>> 'a_int' in obj
True
>>> 'a_float' in obj
True
>>> 'a_string' in obj # in class, but not instance
False
>>> 'a_string' in obj.__class__
True
After it's set for the given instance, then becomes true:
>>> obj.a_string = 'hello world' # known field
>>> 'a_string' in obj # now in instance
True
'''
return hasattr(self, name)
[docs] def __len__(self):
'''Checks how many fields are set in the **instance**.
>>> json_data = { 'aInt': 1, 'aFloat': 2.1 }
>>> obj = global_schema.TypeUsingPython(json_data)
>>> len(obj)
2
>>> obj.a_string = 'hello world' # known field
>>> len(obj)
3
'''
i = 0
for name in self:
i += 1
return i
[docs] def __str__(self):
r = []
for k in self:
r.append('%s=%s' % (k, self[k]))
return '%s(%s)' % (self.__class__.__name__, ', '.join(r))
[docs] def __repr__(self):
r = []
for k in self:
r.append('%s=%r' % (k, self[k]))
return '%s(%s)' % (self.__class__.__name__, ', '.join(r))
def __to_json_value__(self):
return ContainerTypeMeta.__to_json_value__(self.__class__, self)
def __bytes__(self):
return bytes(
json.dumps(self.__to_json_value__(), **self.__json_dump_args__),
'utf-8',
)
[docs]class BaseItem:
'''Base item for :class:`Arg` and :class:`Field`.
Each parameter has a GraphQL type, such as a derived class from
:class:`Scalar` or :class:`Type`, this is used for nesting,
conversion to native Python types, generating queries, etc.
'''
__slots__ = (
'_type',
'graphql_name',
'name',
'schema',
'container',
)
[docs] def __init__(self, typ, graphql_name=None):
'''
:param typ: the :class:`Scalar` or :class:`Type` derived
class. If this would cause a cross reference and the other
type is not declared yet, then use the string name to query
in the schema.
:type typ: :class:`Scalar`, :class:`Type` or str
:param graphql_name: the name to use in JSON object, usually ``aName``.
If ``None`` or empty, will be created from python, converting
``a_name`` to ``aName`` using
``Arg._to_graphql_name()``
:type graphql_name: str
'''
if isinstance(typ, str):
self._type = Lazy(typ, typ, lambda x: x)
elif isinstance(typ, Lazy):
self._type = typ
else:
self._type = BaseType.__ensure__(typ)
self.graphql_name = graphql_name
self.name = None
self.schema = None
self.container = None
def _set_container(self, schema, container, name):
self.schema = schema
self.container = container
self.name = name
if not self.graphql_name:
self.graphql_name = self._to_graphql_name(name)
@property # noqa: A003
def type(self): # noqa: A003
if not isinstance(self._type, Lazy):
return self._type
self._type = self._type.resolve(self.schema)
return self._type
_renamed_to_python_fields = {
'__typename': '__typename__',
}
_renamed_to_graphql_fields = {
v: k for k, v in _renamed_to_python_fields.items()
}
[docs] @classmethod
def _to_python_name(cls, graphql_name):
'''Converts a GraphQL name, ``aName`` to Python: ``a_name``.
Note that an underscore is appended if the name is a Python keyword.
>>> BaseItem._to_python_name('aName')
'a_name'
>>> BaseItem._to_python_name('for')
'for_'
>>> BaseItem._to_python_name('__typename')
'__typename__'
'''
s = []
rgx = re.compile('([^A-Z]+|[A-Z]+[^A-Z]*)')
for w in rgx.findall(graphql_name):
s.append(w.lower())
name = '_'.join(s)
if keyword.iskeyword(name):
return name + '_'
try:
return cls._renamed_to_python_fields[name]
except KeyError:
return name
[docs] @classmethod
def _to_graphql_name(cls, name):
'''Converts a Python name, ``a_name`` to GraphQL: ``aName``.
Note that leading underscores (``_``) are preserved.
>>> BaseItem._to_graphql_name('a_name')
'aName'
>>> BaseItem._to_graphql_name('__underscore_prefixed')
'__underscorePrefixed'
>>> BaseItem._to_graphql_name('__typename__')
'__typename'
'''
try:
return cls._renamed_to_graphql_fields[name]
except KeyError:
pass
prefix = ''
while name.startswith('_'):
prefix += '_'
name = name[1:]
parts = name.split('_')
return prefix + ''.join(parts[:1] + [p.title() for p in parts[1:]])
[docs] def __str__(self):
return self.name
def __to_graphql__(self, indent=0, indent_string=' '):
return '%s: %s' % (self.graphql_name, self.type)
[docs] def __repr__(self):
return self.__to_graphql__()
def __bytes__(self):
return bytes(self.__to_graphql__(indent_string=''), 'utf-8')
[docs]class Variable:
'''GraphQL variable: ``$varName``
Usually given as :class:`Arg` default value:
>>> class MyTypeWithVariable(Type):
... f = Field(str, args={'first': Arg(int, default=Variable('var'))})
...
>>> MyTypeWithVariable
type MyTypeWithVariable {
f(first: Int = $var): String
}
>>> print(repr(MyTypeWithVariable.f.args['first'].default))
$var
>>> print(str(MyTypeWithVariable.f.args['first'].default))
$var
>>> print(bytes(MyTypeWithVariable.f.args['first'].default).decode('utf8'))
$var
'''
__slots__ = ('name', 'graphql_name')
[docs] def __init__(self, name, graphql_name=None):
self.name = name
self.graphql_name = graphql_name or self._to_graphql_name(name)
[docs] def __str__(self):
return self.__to_graphql__()
[docs] def __repr__(self):
return self.__to_graphql__()
def __bytes__(self):
return bytes(self.__to_graphql__(indent_string=''), 'utf-8')
[docs] @staticmethod
def _to_graphql_name(name):
'''Converts a Python name, ``a_name`` to GraphQL: ``aName``.'''
parts = name.split('_')
return ''.join(parts[:1] + [p.title() for p in parts[1:]])
def __to_graphql__(self, indent=0, indent_string=' '):
return '$' + self.graphql_name
@classmethod
def __to_graphql_input__(cls, value, indent=0, indent_string=' '):
return '$' + value.graphql_name
[docs]class Arg(BaseItem):
'''GraphQL :class:`Field` argument.
>>> class MyTypeWithArgument(Type):
... a = Field(str, args={'arg_name': int}) # implicit
... b = Field(str, args={'arg': Arg(int)}) # explicit + Python
... c = Field(str, args={'arg': Arg(Int)}) # explicit + sgqlc.types
... d = Field(str, args={'arg': Arg(int, default=1)})
...
>>> MyTypeWithArgument
type MyTypeWithArgument {
a(argName: Int): String
b(arg: Int): String
c(arg: Int): String
d(arg: Int = 1): String
}
'''
__slots__ = ('default',)
[docs] def __init__(self, typ, graphql_name=None, default=None):
'''
:param typ: the :class:`Scalar` or :class:`Type` derived
class. If this would cause a cross reference and the other
type is not declared yet, then use the string name to query
in the schema.
:type typ: :class:`Scalar`, :class:`Type` or str
:param graphql_name: the name to use in JSON object, usually ``aName``.
If ``None`` or empty, will be created from python, converting
``a_name`` to ``aName`` using
:func:`BaseItem._to_graphql_name()`
:type graphql_name: str
:param default: The default value for field. May be a value or
:class:`Variable`.
'''
super(Arg, self).__init__(typ, graphql_name)
self.default = default
try:
get_variable_or_none(default)
return
except ValueError:
pass
# validate default value
typ(default)
def __to_graphql__(self, indent=0, indent_string=' '):
default = ''
if self.default is not None:
if isinstance(self.default, Variable):
default = self.default.__to_graphql__(indent, indent_string)
else:
default = self.type.__to_graphql_input__(
self.default, indent, indent_string
)
default = ' = ' + default
return super(Arg, self).__to_graphql__(indent, indent_string) + default
def __to_graphql_input__(self, value, indent=0, indent_string=' '):
if not isinstance(value, (self.type, Variable)):
value = self.type(value)
v = self.type.__to_graphql_input__(value, indent, indent_string)
return '%s: %s' % (self.graphql_name, v)
[docs]class ArgDict(OrderedDict):
'''The :class:`Field` Argument Dict.
Common usage is inside :class:`Field`:
>>> class MyType(Type):
... a = Field(Int, args={'argument1': String}) # implicit
... b = Field(Int, args=ArgDict(argument1=String)) # explicit
...
>>> print(repr(MyType))
type MyType {
a(argument1: String): Int
b(argument1: String): Int
}
>>> print(repr(MyType.a))
a(argument1: String): Int
>>> print(repr(MyType.a.args))
(argument1: String)
>>> print(repr(MyType.b))
b(argument1: String): Int
>>> print(repr(MyType.b.args))
(argument1: String)
>>> print(repr(MyType.b.args['argument1']))
argument1: String
>>> print(bytes(MyType.b.args['argument1']).decode('utf-8'))
argument1: String
This takes care to ensure values are :class:`Arg`. In the example
above, we're not passing :class:`Arg`, rather just a type
(:class:`String`) and it's working internally to create
:class:`Arg`. For ease of use, can be created in various
forms. Note they must be added to a container field to be useful,
which would call :func:`ArgDict._set_container()` for you, here
called manually for testing purposes:
>>> ad = ArgDict(name=str)
>>> ad._set_container(global_schema, None) # done automatically by Field
>>> print(ad)
(name: String)
>>> ad = ArgDict(name=String)
>>> ad._set_container(global_schema, None) # done automatically by Field
>>> print(ad)
(name: String)
>>> ad = ArgDict({'name': str})
>>> ad._set_container(global_schema, None) # done automatically by Field
>>> print(ad)
(name: String)
>>> ad = ArgDict(('name', str), ('other', int))
>>> ad._set_container(global_schema, None) # done automatically by Field
>>> print(ad)
(name: String, other: Int)
>>> ad = ArgDict((('name', str), ('other', int)))
>>> ad._set_container(global_schema, None) # done automatically by Field
>>> print(ad)
(name: String, other: Int)
Note that for better understanding, more than 3 arguments are
printed in multiple lines:
>>> ad = ArgDict(a=int, b=float, c=non_null(str), d=list_of(int))
>>> ad._set_container(global_schema, None) # done automatically by Field
>>> print(ad)
(
a: Int
b: Float
c: String!
d: [Int]
)
>>> print(bytes(ad).decode('utf-8'))
(
a: Int
b: Float
c: String!
d: [Int]
)
This is also the case for input values:
>>> print('fieldName' + ad.__to_graphql_input__({
... 'a': 1, 'b': 2.2, 'c': 'hi', 'd': [1, 2],
... }))
fieldName(
a: 1
b: 2.2
c: "hi"
d: [1, 2]
)
Variables can be handled using :class:`Variable` instances:
>>> print('fieldName' + ad.__to_graphql_input__({
... 'a': Variable('a'),
... 'b': Variable('b'),
... 'c': Variable('c'),
... 'd': Variable('d'),
... }))
fieldName(
a: $a
b: $b
c: $c
d: $d
)
'''
[docs] def __init__(self, *lst, **mapping):
super(ArgDict, self).__init__()
if not lst and not mapping:
return
if len(lst) == 1:
if lst[0] is None:
lst = []
elif isinstance(lst[0], (tuple, list)):
lst = lst[0]
elif isinstance(lst[0], dict):
mapping.update(lst[0])
lst = []
for k, v in lst:
if not isinstance(v, Arg):
v = Arg(v)
self[k] = v
for k, v in mapping.items():
if not isinstance(v, Arg):
v = Arg(v)
self[k] = v
def _set_container(self, schema, container):
for k, v in self.items():
v._set_container(schema, container, k)
def __to_graphql__(self, indent=0, indent_string=' '):
n = len(self)
if n == 0:
return ''
s = ['(']
if n <= 3:
args = (
p.__to_graphql__(indent, indent_string) for p in self.values()
)
s.extend((', '.join(args), ')'))
else:
s.append('\n')
prefix = indent_string * (indent + 1)
for p in self.values():
s.extend(
(prefix, p.__to_graphql__(indent, indent_string), '\n')
)
s.extend((indent_string * indent, ')'))
return ''.join(s)
def __to_graphql_input__(self, values, indent=0, indent_string=' '):
n = len(values)
if n == 0:
return ''
s = ['(']
if n <= 3:
args = []
for k, v in values.items():
p = self[k]
args.append(p.__to_graphql_input__(v))
s.extend((', '.join(args), ')'))
else:
s.append('\n')
prefix = indent_string * (indent + 2)
for k, v in values.items():
p = self[k]
s.extend(
(
prefix,
p.__to_graphql_input__(v, indent, indent_string),
'\n',
)
)
s.extend((indent_string * (indent + 1), ')'))
return ''.join(s)
[docs] def __str__(self):
return self.__to_graphql__()
[docs] def __repr__(self):
return self.__to_graphql__()
def __bytes__(self):
return bytes(self.__to_graphql__(indent_string=''), 'utf-8')
[docs]class Field(BaseItem):
'''Field in a :class:`Type` container.
Each field has a GraphQL type, such as a derived class from
:class:`Scalar` or :class:`Type`, this is used for nesting,
conversion to native Python types, generating queries, etc.
'''
__slots__ = ('args',)
[docs] def __init__(self, typ, graphql_name=None, args=None):
'''
:param typ: the :class:`Scalar` or :class:`Type` derived
class. If this would cause a cross reference and the other
type is not declared yet, then use the string name to query
in the schema.
:type typ: :class:`Scalar`, :class:`Type` or str
:param graphql_name: the name to use in JSON object, usually ``aName``.
If ``None`` or empty, will be created from python, converting
``a_name`` to ``aName`` using
:func:`BaseItem._to_graphql_name()`
:type graphql_name: str
:param args: The field parameters as a :class:`ArgDict` or
compatible type (dict, or iterable of key-value pairs). The
value may be a mapped Python type (ie: ``str``), explicit
type (ie: ``String``), type name (ie: ``"String"``, to allow
cross references) or :class:`Arg` instances.
:type args: :class:`ArgDict`
'''
super(Field, self).__init__(typ, graphql_name)
self.args = ArgDict(args)
def _set_container(self, schema, container, name):
super(Field, self)._set_container(schema, container, name)
for k, v in self.args.items():
v._set_container(schema, container, k)
def __to_graphql__(self, indent=0, indent_string=' '):
args = self.args.__to_graphql__(indent + 1, indent_string)
return '%s%s: %s' % (self.graphql_name, args, self.type)
[docs] def __bytes__(self):
'''Prints GraphQL without indentation.
>>> print(repr(global_schema.TypeUsingFields.many))
many(
a: Int
b: Int
c: Int
d: Int
): Int
>>> print(bytes(global_schema.TypeUsingFields.many).decode('utf-8'))
many(
a: Int
b: Int
c: Int
d: Int
): Int
'''
return bytes(self.__to_graphql__(indent_string=''), 'utf-8')
[docs]class Type(ContainerType):
'''GraphQL ``type Name``.
If the subclass also adds :class:`Interface` to the class
declarations, then it will emit ``type Name implements Iface1, Iface2``,
also making their fields automatically available in the final
class.
'''
__kind__ = 'type'
[docs]class Interface(ContainerType):
'''GraphQL ``interface Name``.
If the subclass also adds :class:`Interface` to the class
declarations, then it will emit
``interface Name implements Iface1, Iface2``,
also making their fields automatically available in the final
class.
Whenever interfaces are instantiated, if there is a ``__typename``
in ``json_data`` and the type is known, it will automatically
create the more specific type. Otherwise it instantiates the interface
itself:
>>> class SomeIface(Interface):
... i = int
...
>>> class TypeWithIface(Type, SomeIface):
... pass
...
>>> data = {'__typename': 'TypeWithIface', 'i': 123}
>>> SomeIface(data)
TypeWithIface(i=123)
>>> data = {'__typename': 'UnknownType', 'i': 123}
>>> SomeIface(data)
SomeIface(i=123)
'''
__kind__ = 'interface'
[docs] def __new__(cls, *args, **kwargs):
if len(args) > 0 and isinstance(args[0], dict):
type_name = args[0].get('__typename')
if type_name:
t = cls.__possible_types__.get(type_name)
if t is not None and t is not cls:
return t(*args, **kwargs)
return ContainerType.__new__(cls)
########################################################################
# Built-in types
########################################################################
[docs]class Int(Scalar):
'''Maps GraphQL ``Int`` to Python ``int``.
>>> Int # or repr()
scalar Int
>>> str(Int)
'Int'
>>> bytes(Int)
b'scalar Int'
'''
converter = int
[docs]class Float(Scalar):
'Maps GraphQL ``Float`` to Python ``float``.'
converter = float
[docs]class String(Scalar):
'Maps GraphQL ``String`` to Python ``str``.'
converter = str
[docs]class Boolean(Scalar):
'Maps GraphQL ``Boolean`` to Python ``bool``.'
converter = bool
[docs]class ID(Scalar):
'Maps GraphQL ``ID`` to Python ``str``.'
converter = str
class UnknownType(Type):
'Type found in the response that was not present in schema'
__auto_register = False # do not expose this in Schema, just subclasses
map_python_to_graphql = {
int: Int,
float: Float,
str: String,
bool: Boolean,
id: ID,
}