Source code for sgqlc.types

'''
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, two 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.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.


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
  scalar Time
  scalar Date
  scalar DateTime
  interface Node {
    id: ID!
  }
  type PageInfo {
    endCursor: String
    startCursor: String
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
  }
  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
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 PageInfo { ... 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 BaseMeta(type): 'Automatically adds class to its schema'
[docs] def __init__(cls, name, bases, namespace): super(BaseMeta, cls).__init__(name, bases, namespace) if not bases or BaseType in bases or \ BaseTypeWithTypename in bases or ContainerType in bases: return auto_register_name = '_%s__auto_register' % (name,) auto_register = getattr(cls, auto_register_name, True) if auto_register: cls.__schema__ += cls
[docs] def __str__(cls): return cls.__name__
def __to_graphql__(cls, indent=0, indent_string=' '): prefix = indent_string * indent return '%s%s %s' % (prefix, cls.__kind__, cls.__name__)
[docs] def __repr__(cls): return cls.__to_graphql__()
def __bytes__(cls): return bytes(cls.__to_graphql__(indent_string=''), 'utf-8')
[docs] def __ensure__(cls, t): '''Checks if ``t`` is subclass of ``BaseType`` or if a mapping is known. >>> BaseType.__ensure__(Int) scalar Int >>> BaseType.__ensure__(int) scalar Int >>> BaseType.__ensure__(bytes) Traceback (most recent call last): ... TypeError: Not BaseType or mapped: <class 'bytes'> ''' if isinstance(t, type) and issubclass(t, cls): return t try: return map_python_to_graphql[t] except KeyError as exc: raise TypeError('Not %s or mapped: %s' % (cls, t)) from exc
[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 _create_non_null_wrapper(name, t): 'creates type wrapper for non-null of given type' def realize_type(v, selection_list=None): if isinstance(v, (t, Variable)): return v return t(v, selection_list) 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 _create_list_of_wrapper(name, t): 'creates type wrapper for list of given type' def realize_type(v, selection_list=None): if isinstance(v, (t, Variable)): # pragma: no cover return v return t(v, selection_list) def __new__(cls, json_data, selection_list=None): if json_data is None: return None return [realize_type(v, selection_list) for v in json_data] def __to_graphql_input__(value, indent=0, indent_string=' '): r = [] for v in value: v = realize_type(v) r.append(t.__to_graphql_input__(v, indent, indent_string)) return '[' + ', '.join(r) + ']' def __to_json_value__(value): if value is None: return None return [t.__to_json_value__(v) for v in 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 ``Non``, 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 ''' 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' ''' __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): return None if json_data is None else 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 not cls.__choices__ and BaseType not in bases: raise ValueError(name + ': missing __choices__') 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=' '): 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 Failing to define choices will raise exception: >>> class FailureEnum(Enum): ... pass Traceback (most recent call last): ... ValueError: FailureEnum: missing __choices__ Enumerations have a special syntax in GraphQL, no quotes: >>> print(Fruits.__to_graphql_input__(Fruits.APPLE)) APPLE And for JSON it's a string as well (so JSON encoder adds quotes): >>> print(json.dumps(Fruits.__to_json_value__(Fruits.APPLE))) "APPLE" ''' __kind__ = 'enum' __choices__ = ()
[docs] def __new__(cls, json_data, selection_list=None): if json_data is None: return None 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) ''' __kind__ = 'union' __types__ = ()
[docs] def __new__(cls, json_data, selection_list=None): if json_data is None: return 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 ContainerTypeMeta(BaseMetaWithTypename): '''Creates container types, ensures fields are instance of Field. '''
[docs] def __init__(cls, name, bases, namespace): super(ContainerTypeMeta, cls).__init__(name, bases, namespace) cls.__fields = OrderedDict() cls.__interfaces__ = () if not bases or BaseTypeWithTypename in bases or \ ContainerType in bases: return if cls.__kind__ == 'interface': cls.__fix_type_kind(bases) if cls.__kind__ == 'interface': cls.__possible_types__ = {} cls.__populate_interfaces(bases) cls.__inherit_fields(bases) cls.__create_own_fields()
def __fix_type_kind(cls, bases): for b in bases: if b.__kind__ == 'type': cls.__kind__ = 'type' break def __populate_interfaces(cls, bases): ifaces = [] for b in bases: if b in (Type, Interface, Input): continue if getattr(b, '__kind__', '') == 'interface': ifaces.append(b) for i in getattr(b, '__interfaces__', []): if i not in ifaces: ifaces.append(i) cls.__interfaces__ = tuple(ifaces) for i in ifaces: i.__possible_types__[cls.__name__] = cls def __inherit_fields(cls, bases): for b in bases: cls.__fields.update(b.__fields) def __get_field_names(cls): try: return getattr(cls, "__field_names__") except AttributeError: all_fields = super(ContainerTypeMeta, cls).__dir__() return ( name for name in all_fields if not name.startswith("_") ) def __create_own_fields(cls): # call the parent __dir__(), we don't want our overridden version # that reports fields we're just deleting for name in cls.__get_field_names(): field = getattr(cls, name) if not isinstance(field, Field): if not isinstance(field, Lazy): try: field = BaseType.__ensure__(field) except TypeError: continue field = Field(field) field._set_container(cls.__schema__, cls, name) cls.__fields[name] = field try: # let fallback to cls.__fields using getitem delattr(cls, name) except AttributeError: # pragma: no cover pass # if may be defined in a parent class and already deleted def __getitem__(cls, key): if key.startswith('_'): try: return cls.__meta_fields__[key] except KeyError: pass try: return cls.__fields[key] except KeyError as exc: raise KeyError('%s has no field %s' % (cls, key)) from exc def __getattr__(cls, key): try: return cls.__fields[key] except KeyError as exc: raise AttributeError('%s has no field %s' % (cls, key)) from exc
[docs] def __dir__(cls): original_dir = super(ContainerTypeMeta, cls).__dir__() fields = list(cls.__fields.keys()) return sorted(original_dir + fields)
def __iter__(cls): return iter(cls.__fields.values()) def __contains__(cls, field_name): return field_name in cls.__fields def __to_graphql__(cls, indent=0, indent_string=' '): d = BaseMeta.__to_graphql__(cls, indent, indent_string) if hasattr(cls, '__interfaces__') and cls.__interfaces__: d += ' implements ' + ', '.join(str(i) for i in cls.__interfaces__) s = [d + ' {'] prefix = indent_string * (indent + 1) for f in cls: s.append(prefix + f.__to_graphql__(indent, indent_string)) s.append(indent_string * indent + '}') return '\n'.join(s) def __to_json_value__(cls, value): if value is None: return None d = {} for name, f in cls.__fields.items(): # elements may not exist since not queried and would # trigger exception for non-null fields if name in value: d[f.graphql_name] = f.type.__to_json_value__(value[name]) return d
[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``, ``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): for sel in sl.__get_selections_or_auto_select__(): 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) casts = sl.__casts__ if casts: tname = json_data.get('__typename') csl = casts.get(tname) if csl: self.__populate_fields_from_selection_list(csl, 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): if not isinstance(self._type, Lazy): return self._type self._type = self._type.resolve(self.schema) return self._type
[docs] @staticmethod def _to_graphql_name(name): '''Converts a Python name, ``a_name`` to GraphQL: ``aName``. Note that leading underscores (``_``) are preserved. ''' 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 if default is not None and not isinstance(default, Variable): 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=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] ) '''
[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)
[docs]class Input(ContainerType): '''GraphQL ``input Name``. Input types are similar to :class:`Type`, but they are used as argument values. They have more restrictions, such as no :class:`Interface`, :class:`Union` or :class:`Type` are allowed as field types. Only scalars or :class:`Input`. .. note:: SGQLC currently doesn't enforce the field type restrictions imposed by the server. >>> class MyInput(Input): ... a_int = int ... a_float = float ... >>> MyInput input MyInput { aInt: Int aFloat: Float } >>> print(MyInput.__to_graphql_input__({'a_int': 1, 'a_float': 2.2})) {aInt: 1, aFloat: 2.2} >>> a_var = Variable('input') >>> print(MyInput.__to_graphql_input__(a_var)) $input ''' __kind__ = 'input'
[docs] def __init__(self, _json_obj=None, _selection_list=None, **kwargs): '''Create the type given a json object or keyword arguments. >>> class AnotherInput(Input): ... a_str = str ... >>> class TheInput(Input): ... a_int = int ... a_float = float ... a_nested = AnotherInput ... a_nested_list = list_of(AnotherInput) ... >>> TheInput(a_int=1, a_float=1.2, a_nested=AnotherInput(a_str='hi')) TheInput(a_int=1, a_float=1.2, a_nested=AnotherInput(a_str='hi')) >>> TheInput({'aInt': 1, 'aFloat': 1.2, 'aNested': {'aStr': 'hi'}}) TheInput(a_int=1, a_float=1.2, a_nested=AnotherInput(a_str='hi')) >>> value = TheInput(a_int=1, a_float=1.2, ... a_nested=AnotherInput(a_str='hi'), ... a_nested_list=[AnotherInput(a_str='there')]) >>> print(TheInput.__to_graphql_input__(value)) {aInt: 1, aFloat: 1.2, aNested: {aStr: "hi"}, aNestedList: [{aStr: "there"}]} >>> value = TheInput({'aInt': 1, 'aFloat': 1.2, 'aNested': {'aStr': 'hi'}, ... 'aNestedList': [{'aStr': 'there'}]}) >>> print(TheInput.__to_graphql_input__(value)) {aInt: 1, aFloat: 1.2, aNested: {aStr: "hi"}, aNestedList: [{aStr: "there"}]} .. note:: ``selection_list`` parameter makes no sense and is ignored, it's only provided to cope with the ``ContainerType`` interface. ''' # noqa: E501 if _json_obj is None: _json_obj = {} super().__init__(_json_obj, _selection_list) cls = type(self) for k, v in kwargs.items(): f = cls[k] if not isinstance(v, (f.type, Variable, list)): v = f.type(v) setattr(self, k, v)
@classmethod def __to_graphql_input__(cls, value, indent=0, indent_string=' '): args = [] if isinstance(value, Variable): return Variable.__to_graphql_input__(value, indent, indent_string) elif isinstance(value, Input): value = value.__json_data__ for f in cls: try: v = value[f.graphql_name] except KeyError: try: # previous versions allowed Python name as dict keys v = value[f.name] except KeyError: # pragma: no cover continue vs = f.type.__to_graphql_input__(v, indent, indent_string) args.append('%s: %s' % (f.graphql_name, vs)) return '{' + ', '.join(args) + '}'
######################################################################## # 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, }