'''
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


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',
    )

    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)

    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

    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]

    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

    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())

    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

    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

    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)

    def __repr__(self):
        return self.__to_graphql__()

    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()


class BaseMeta(type):
    'Automatically adds class to its schema'

    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

    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__)

    def __repr__(cls):
        return cls.__to_graphql__()

    def __bytes__(cls):
        return bytes(cls.__to_graphql__(indent_string=''), 'utf-8')

    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


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


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


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


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

    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


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__ = ()

    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]


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__ = ()

    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)


class ContainerTypeMeta(BaseMetaWithTypename):
    '''Creates container types, ensures fields are instance of Field.'''

    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

    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):
        try:
            return get_variable_or_none(value)
        except ValueError:
            pass

        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


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': (',', ':'),
    }

    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__

    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

    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

    def __setitem__(self, name, value):
        '''Set the item, maps to ``setattr(self, name, value)``'''
        setattr(self, name, value)

    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())

    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)

    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

    def __str__(self):
        r = []
        for k in self:
            r.append('%s=%s' % (k, self[k]))
        return '%s(%s)' % (self.__class__.__name__, ', '.join(r))

    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',
        )


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',
    )

    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()
    }

    @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

    @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:]])

    def __str__(self):
        return self.name

    def __to_graphql__(self, indent=0, indent_string='  '):
        return '%s: %s' % (self.graphql_name, self.type)

    def __repr__(self):
        return self.__to_graphql__()

    def __bytes__(self):
        return bytes(self.__to_graphql__(indent_string=''), 'utf-8')


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')

    def __init__(self, name, graphql_name=None):
        self.name = name
        self.graphql_name = graphql_name or self._to_graphql_name(name)

    def __str__(self):
        return self.__to_graphql__()

    def __repr__(self):
        return self.__to_graphql__()

    def __bytes__(self):
        return bytes(self.__to_graphql__(indent_string=''), 'utf-8')

    @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


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',)

    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)


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
      )

    '''

    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)

    def __str__(self):
        return self.__to_graphql__()

    def __repr__(self):
        return self.__to_graphql__()

    def __bytes__(self):
        return bytes(self.__to_graphql__(indent_string=''), 'utf-8')


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',)

    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)

    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')


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'


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'

    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)


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'

    def __new__(cls, *args, **kwargs):
        if len(args) == 1:
            try:
                return get_variable_or_none(args[0])
            except ValueError:
                pass

        return super().__new__(cls)

    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)
        ...

        It can be constructed using fields and values as a regular Python class:

        >>> 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'))

        Or can be given as a dict (ie: JSON data) as parameter:

        >>> TheInput({'aInt': 1, 'aFloat': 1.2, 'aNested': {'aStr': 'hi'}})
        TheInput(a_int=1, a_float=1.2, a_nested=AnotherInput(a_str='hi'))

        It can be printed to GraphQL DSL using ``__to_graphql_input__()``:

        >>> 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"}]}

        The nested types (lists, non-null) can also take an already realized
        value, see ``AnotherInput`` below:

        >>> value = TheInput({'aInt': 1, 'aFloat': 1.2, 'aNested': {'aStr': 'hi'},
        ...           'aNestedList': [AnotherInput(a_str='there')]})
        >>> print(TheInput.__to_graphql_input__(value))
        {aInt: 1, aFloat: 1.2, aNested: {aStr: "hi"}, aNestedList: [{aStr: "there"}]}

        Input types can be constructed from variables. Note that the input
        variable can't be an element of a list, the list itself must be a variable:

        >>> a_var = Variable('input')
        >>> value = TheInput(a_var)
        >>> print(TheInput.__to_graphql_input__(value))
        $input
        >>> value = TheInput(a_int=1, a_float=1.2,
        ...                  a_nested=a_var,
        ...                  a_nested_list=[AnotherInput(a_str='there')])
        >>> print(TheInput.__to_graphql_input__(value))
        {aInt: 1, aFloat: 1.2, aNested: $input, aNestedList: [{aStr: "there"}]}
        >>> value = TheInput(a_int=1, a_float=1.2,
        ...                  a_nested=AnotherInput(a_str='hi'),
        ...                  a_nested_list=a_var)
        >>> print(TheInput.__to_graphql_input__(value))
        {aInt: 1, aFloat: 1.2, aNested: {aStr: "hi"}, aNestedList: $input}

        ``None`` will print ``null``:

        >>> print(TheInput.__to_graphql_input__(None))
        null

        >>> value = TheInput(a_int=None, a_float=None, a_nested=None,
        ...                  a_nested_list=None)
        >>> print(TheInput.__to_graphql_input__(value))
        {aInt: null, aFloat: null, aNested: null, aNestedList: null}

        Unless fields are non-nullable, then ``ValueError`` is raised:

        >>> class TheNonNullInput(Input):
        ...     a_int = non_null(int)
        ...     a_float = non_null(float)
        ...     a_nested = non_null(AnotherInput)
        ...     a_nested_list = non_null(list_of(non_null(AnotherInput)))

        >>> TheNonNullInput(a_int=None, a_float=1.2,
        ...                 a_nested=AnotherInput(a_str='hi'),
        ...                 a_nested_list=[AnotherInput(a_str='there')]
        ...                 )  # doctest: +ELLIPSIS
        Traceback (most recent call last):
          ...
        ValueError: Int! received null value
        >>> TheNonNullInput(a_int=1, a_float=None,
        ...                 a_nested=AnotherInput(a_str='hi'),
        ...                 a_nested_list=[AnotherInput(a_str='there')]
        ...                 )  # doctest: +ELLIPSIS
        Traceback (most recent call last):
          ...
        ValueError: Float! received null value
        >>> TheNonNullInput(a_int=1, a_float=1.2,
        ...                 a_nested=None,
        ...                 a_nested_list=[AnotherInput(a_str='there')]
        ...                 )  # doctest: +ELLIPSIS
        Traceback (most recent call last):
          ...
        ValueError: AnotherInput! received null value
        >>> TheNonNullInput(a_int=1, a_float=1.2,
        ...                 a_nested=AnotherInput(a_str='hi'),
        ...                 a_nested_list=[None]
        ...                 )  # doctest: +ELLIPSIS
        Traceback (most recent call last):
          ...
        ValueError: AnotherInput! received null value
        >>> TheNonNullInput(a_int=1, a_float=1.2,
        ...                 a_nested=AnotherInput(a_str='hi'),
        ...                 a_nested_list=None
        ...                 )  # doctest: +ELLIPSIS
        Traceback (most recent call last):
          ...
        ValueError: [AnotherInput!]! received null value

        .. note::

            ``selection_list`` parameter makes no sense and is ignored,
            it's only provided to cope with the ``ContainerType`` interface.

        '''  # noqa: E501
        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)):
                v = f.type(v)
            setattr(self, k, v)

    @classmethod
    def __to_graphql_input__(cls, value, indent=0, indent_string='  '):
        try:
            return get_variable_or_none_input(value, indent, indent_string)
        except ValueError:
            pass

        if isinstance(value, Input):
            value = value.__json_data__
        assert isinstance(value, dict)

        args = []
        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
########################################################################


class Int(Scalar):
    '''Maps GraphQL ``Int`` to Python ``int``.

    >>> Int # or repr()
    scalar Int
    >>> str(Int)
    'Int'
    >>> bytes(Int)
    b'scalar Int'

    '''

    converter = int


class Float(Scalar):
    'Maps GraphQL ``Float`` to Python ``float``.'
    converter = float


class String(Scalar):
    'Maps GraphQL ``String`` to Python ``str``.'
    converter = str


class Boolean(Scalar):
    'Maps GraphQL ``Boolean`` to Python ``bool``.'
    converter = bool


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,
}
