<commit_before>
#
# spyne - Copyright (C) Spyne contributors.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
#

"""The ``spyne.model.complex`` module contains
:class:`spyne.model.complex.ComplexBase` class and its helper objects and
subclasses. These are mainly container classes for other simple or
complex objects -- they don't carry any data by themselves.
"""

from __future__ import print_function

import logging
logger = logging.getLogger(__name__)

import decimal

from weakref import WeakKeyDictionary
from collections import deque
from inspect import isclass
from itertools import chain

from spyne import BODY_STYLE_BARE, BODY_STYLE_WRAPPED, BODY_STYLE_EMPTY
from spyne import const
from spyne.const import xml_ns

from spyne.model import Point
from spyne.model import Unicode
from spyne.model import PushBase
from spyne.model import ModelBase
from spyne.model import json, xml, msgpack, table
from spyne.model._base import apply_pssm
from spyne.model.primitive import NATIVE_MAP

from spyne.util import six
from spyne.util import memoize
from spyne.util import memoize_id
from spyne.util import sanitize_args
from spyne.util.meta import Prepareable
from spyne.util.odict import odict
from spyne.util.six import add_metaclass, with_metaclass, string_types


PSSM_VALUES = {'json': json, 'xml': xml, 'msgpack': msgpack, 'table': table}

def _get_flat_type_info(cls, retval):
    assert isinstance(retval, TypeInfo)
    parent = getattr(cls, '__extends__', None)
    if parent != None:
        _get_flat_type_info(parent, retval)
    retval.update(cls._type_info)
    retval.alt.update(cls._type_info_alt) # FIXME: move to cls._type_info.alt
    return retval


class TypeInfo(odict):
    def __init__(self, *args, **kwargs):
        super(TypeInfo, self).__init__(*args, **kwargs)
        self.attributes = {}
        self.alt = {}

    def __setitem__(self, key, val):
        assert isinstance(key, string_types)
        super(TypeInfo, self).__setitem__(key, val)


class _SimpleTypeInfoElement(object):
    __slots__ = ['path', 'parent', 'type', 'is_array']

    def __init__(self, path, parent, type_, is_array):
        self.path = path
        self.parent = parent
        self.type = type_
        self.is_array = is_array

    def __repr__(self):
        return "SimpleTypeInfoElement(path=%r, parent=%r, type=%r, is_array=%r)" \
                            % (self.path, self.parent, self.type, self.is_array)


class XmlModifier(ModelBase):
    def __new__(cls, type, ns=None):
        retval = cls.customize()
        retval.type = type
        retval.Attributes = type.Attributes
        retval._ns = ns
        if type.__type_name__ is ModelBase.Empty:
            retval.__type_name__ = ModelBase.Empty
        return retval

    @staticmethod
    def resolve_namespace(cls, default_ns, tags=None):
        cls.type.resolve_namespace(cls.type, default_ns, tags)

        cls.__namespace__ = cls._ns

        if cls.__namespace__ is None:
            cls.__namespace__ = cls.type.get_namespace()

        if cls.__namespace__ in xml_ns.const_prefmap:
            cls.__namespace__ = default_ns


class XmlData(XmlModifier):
    """Items which are marshalled as data of the parent element."""

    @classmethod
    def marshall(cls, prot, name, value, parent_elt):
        if value is not None:
            if len(parent_elt) == 0:
                parent_elt.text = prot.to_string(cls.type, value)
            else:
                parent_elt[-1].tail = prot.to_string(cls.type, value)

    @classmethod
    def get_type_name(cls):
        return cls.type.get_type_name()

    @classmethod
    def get_type_name_ns(cls, interface):
        return cls.type.get_type_name_ns(interface)

    @classmethod
    def get_namespace(cls):
        return cls.type.get_namespace()

    @classmethod
    def get_element_name(cls):
        return cls.type.get_element_name()

    @classmethod
    def get_element_name_ns(cls, interface):
        return cls.type.get_element_name_ns(interface)


class XmlAttribute(XmlModifier):
    """Items which are marshalled as attributes of the parent element. If
    ``attribute_of`` is passed, it's marshalled as the attribute of the element
    with given name.
    """

    def __new__(cls, type_, use=None, ns=None, attribute_of=None):
        retval = super(XmlAttribute, cls).__new__(cls, type_, ns)
        retval._use = use
        if retval.type.Attributes.min_occurs > 0 and retval._use is None:
            retval._use = 'required'
        retval.attribute_of = attribute_of
        return retval


class XmlAttributeRef(XmlAttribute):
    """Reference to an Xml attribute."""

    def __init__(self, ref, use=None):
        self._ref = ref
        self._use = use

    def describe(self, name, element, app):
        element.set('ref', self._ref)
        if self._use:
            element.set('use', self._use)


class SelfReference(object):
    """Use this as a placeholder type in classes that contain themselves. See
    :func:`spyne.test.model.test_complex.TestComplexModel.test_self_reference`.
    """
    customize_args = []
    customize_kwargs = {}
    __orig__ = None

    def __init__(self):
        raise NotImplementedError()

    @classmethod
    def customize(cls, *args, **kwargs):
        args = chain(args, cls.customize_args)
        kwargs = dict(chain(kwargs.items(), cls.customize_kwargs.items()))
        if cls.__orig__ is None:
            cls.__orig__ = cls

        return type("SelfReference", (cls,), {
            'customize_args': args,
            'customize_kwargs': kwargs,
        })

def _get_spyne_type(cls_name, k, v):
    try:
        v = NATIVE_MAP.get(v, v)
    except TypeError:
        return

    try:
        subc = issubclass(v, ModelBase) or issubclass(v, SelfReference)
    except:
        subc = False

    if subc:
        if issubclass(v, Array) and len(v._type_info) != 1:
            raise Exception("Invalid Array definition in %s.%s."% (cls_name, k))
        elif issubclass(v, Point) and v.Attributes.dim is None:
            raise Exception("Please specify the number of dimensions")
        return v


def _join_args(x, y):
    if x is None:
        return y
    if y is None:
        return x

    xa, xk = sanitize_args(x)
    ya, yk = sanitize_args(y)

    xk = dict(xk)
    xk.update(yk)

    return xa + ya, xk


def _gen_attrs(cls_bases, cls_dict):
    attrs = cls_dict.get('Attributes', None)
    if attrs is None:
        for b in cls_bases:
            if hasattr(b, 'Attributes'):
                class Attributes(b.Attributes):
                    pass
                attrs = cls_dict['Attributes'] = Attributes
                break
        else:
            raise Exception("No ModelBase subclass in bases? Huh?")

    return attrs


def _get_type_info(cls, cls_name, cls_bases, cls_dict, attrs):
    base_type_info = TypeInfo()
    mixin = {}
    extends = cls_dict.get('__extends__', None)
    if extends is None:
        for b in cls_bases:
            base_types = getattr(b, "_type_info", None)

            if base_types is not None:
                if getattr(b, '__mixin__', False) == True:
                    mixin = b.get_flat_type_info(b)
                else:
                    if not (extends in (None, b)):
                        raise Exception("Spyne objects do not support multiple "
                            "inheritance. Use mixins if you need to reuse "
                            "fields from multiple classes.")

                    try:
                        if len(base_types) > 0 and issubclass(b, ModelBase):
                            extends = cls_dict["__extends__"] = b
                            b.get_subclasses.memo.clear()

                    except Exception as e:
                        logger.exception(e)
                        logger.error(repr(extends))
                        raise

    if not ('_type_info' in cls_dict):
        cls_dict['_type_info'] = _type_info = TypeInfo()
        _type_info.update(base_type_info)

        class_fields = []
        for k, v in cls_dict.items():
            if not k.startswith('_'):
                v = _get_spyne_type(cls_name, k, v)
                if v is not None:
                    class_fields.append((k, v))

        _type_info.update(class_fields)

    else:
        _type_info = cls_dict['_type_info']

        if not isinstance(_type_info, TypeInfo):
            _type_info = cls_dict['_type_info'] = TypeInfo(_type_info)

    _type_info.update(mixin)
    return _type_info


class _MethodsDict(dict):
    def __init__(self, *args, **kwargs):
        super(_MethodsDict, self).__init__(*args, **kwargs)

        self._processed = False

    def sanitize(self, cls):
        # sanitize is called on every customization, so we make sure it's run
        # only once in this class' lifetime.
        if self._processed:
            return

        self._processed = True

        for d in self.values():
            d.parent_class = cls

            if d.in_message_name_override:
                d.in_message.__type_name__ = '%s.%s' % \
                          (cls.get_type_name(), d.in_message.get_type_name())

            if d.body_style is BODY_STYLE_WRAPPED or d.out_message_name_override:
                d.out_message.__type_name__ = '%s.%s' % \
                          (cls.get_type_name(), d.out_message.get_type_name())

            if d.body_style in (BODY_STYLE_BARE, BODY_STYLE_EMPTY):
                # The method only needs the primary key(s) and shouldn't
                # complain when other mandatory fields are missing.
                d.in_message = cls.novalidate_freq()
                d.body_style = BODY_STYLE_BARE

            else:
                d.in_message.insert_field(0, 'self', cls.novalidate_freq())
                d.body_style = BODY_STYLE_WRAPPED

                if issubclass(d.in_message, ComplexModelBase):
                    for k, v in d.in_message._type_info.items():
                        # SelfReference is replaced by descriptor.in_message
                        # itself. However, in the context of mrpc, SelfReference
                        # means parent class. here, we do that substitution.
                        # It's a safe hack but a hack nevertheless.
                        if v is d.in_message:
                            d.in_message._type_info[k] = cls

            # Same as above, for the output type.
            if issubclass(d.out_message, ComplexModelBase):
                for k, v in d.out_message._type_info.items():
                    if v is d.out_message:
                        d.out_message._type_info[k] = cls

            if d.patterns is not None and not d.no_self:
                d.name = '.'.join((cls.get_type_name(), d.name))
                for p in d.patterns:
                    if p.address is None:
                        p.address = d.name


def _gen_methods(cls, cls_dict):
    methods = _MethodsDict()
    for k, v in cls_dict.items():
        if not k.startswith('_') and hasattr(v, '_is_rpc'):
            descriptor = v(_default_function_name=k, _self_ref_replacement=cls)
            setattr(cls, k, descriptor.function)
            methods[k] = descriptor

    return methods


def _get_ordered_attributes(cls_name, cls_dict, attrs):
    if not isinstance(cls_dict, odict):
        # FIXME: Maybe add a warning here?
        return cls_dict

    SUPPORTED_ORDERS = ('random', 'declared')
    if (attrs.declare_order is not None and
            not attrs.declare_order in SUPPORTED_ORDERS):

        msg = "The declare_order attribute value %r is invalid in %s"
        raise Exception(msg % (attrs.declare_order, cls_name))

    declare_order = attrs.declare_order or const.DEFAULT_DECLARE_ORDER
    if declare_order is None or declare_order == 'random':
        # support old behaviour
        cls_dict = dict(cls_dict)

    return cls_dict


def _sanitize_sqlalchemy_parameters(cls_dict, attrs):
    table_name = cls_dict.get('__tablename__', None)
    if attrs.table_name is None:
        attrs.table_name = table_name

    _cls_table = cls_dict.get('__table__', None)
    if attrs.sqla_table is None:
        attrs.sqla_table = _cls_table

    metadata = cls_dict.get('__metadata__', None)
    if attrs.sqla_metadata is None:
        attrs.sqla_metadata = metadata

    margs = cls_dict.get('__mapper_args__', None)
    attrs.sqla_mapper_args = _join_args(attrs.sqla_mapper_args, margs)

    targs = cls_dict.get('__table_args__', None)
    attrs.sqla_table_args = _join_args(attrs.sqla_table_args, targs)


def _sanitize_type_info(cls_name, _type_info, _type_info_alt):
    # make sure _type_info contents are sane
    for k, v in _type_info.items():
        if not isinstance(k, six.string_types):
            raise ValueError("Invalid class key", k)
        if not isclass(v):
            raise ValueError(v)

        if issubclass(v, SelfReference):
            continue

        elif not issubclass(v, ModelBase):
            v = _get_spyne_type(cls_name, k, v)
            if v is None:
                raise ValueError( (cls_name, k, v) )
            _type_info[k] = v

        elif issubclass(v, Array) and len(v._type_info) != 1:
            raise Exception("Invalid Array definition in %s.%s." %
                                                        (cls_name, k))
        sub_ns = v.Attributes.sub_ns
        sub_name = v.Attributes.sub_name

        if sub_ns is None and sub_name is None:
            pass

        elif sub_ns is not None and sub_name is not None:
            key = "{%s}%s" % (sub_ns, sub_name)
            if key in _type_info:
                raise Exception("%r is already defined: %r" %
                                                         (key, _type_info[key]))
            _type_info_alt[key] = v, k

        elif sub_ns is None:
            key = sub_name
            if sub_ns in _type_info:
                raise Exception("%r is already defined: %r" %
                                                        (key, _type_info[key]))
            _type_info_alt[key] = v, k

        elif sub_name is None:
            key = "{%s}%s" % (<KEY>
            if key in _type_info:
                raise Exception("%r is already defined: %r" %
                                                        (key, _type_info[key]))
            _type_info_alt[key] = v, k


class ComplexModelMeta(with_metaclass(Prepareable, type(ModelBase))):
    """This metaclass sets ``_type_info``, ``__type_name__`` and ``__extends__``
    which are going to be used for (de)serialization and schema generation.
    """

    def __new__(cls, cls_name, cls_bases, cls_dict):
        """This function initializes the class and registers attributes for
        serialization.
        """

        attrs = _gen_attrs(cls_bases, cls_dict)
        assert issubclass(attrs, ComplexModelBase.Attributes), \
                   ("%r must be a ComplexModelBase.Attributes subclass" % attrs)

        cls_dict = _get_ordered_attributes(cls_name, cls_dict, attrs)

        type_name = cls_dict.get("__type_name__", None)
        if type_name is None:
            cls_dict["__type_name__"] = cls_name

        _type_info = _get_type_info(cls, cls_name, cls_bases, cls_dict, attrs)

        # used for sub_name and sub_ns
        _type_info_alt = cls_dict['_type_info_alt'] = TypeInfo()
        for b in cls_bases:
            if hasattr(b, '_type_info_alt'):
                _type_info_alt.update(b._type_info_alt)

        _sanitize_type_info(cls_name, _type_info, _type_info_alt)
        _sanitize_sqlalchemy_parameters(cls_dict, attrs)

        return super(ComplexModelMeta, cls).__new__(cls, cls_name, cls_bases,
                                                    cls_dict)

    def __init__(self, cls_name, cls_bases, cls_dict):
        type_info = self._type_info

        extends = self.__extends__
        if extends is not None and self.__orig__ is None:
            eattr = extends.Attributes;
            if eattr._subclasses is None:
                eattr._subclasses = []
            eattr._subclasses.append(self)
            if self.Attributes._subclasses is eattr._subclasses:
                self.Attributes._subclasses = None

        for k, v in type_info.items():
            if issubclass(v, SelfReference):
                self.replace_field(k, self.customize(*v.customize_args,
                                                          **v.customize_kwargs))

            elif issubclass(v, XmlData):
                if self.Attributes._xml_tag_body_as is None:
                    self.Attributes._xml_tag_body_as = [(k, v)]
                else:
                    self.Attributes._xml_tag_body_as.append((k, v))

            elif issubclass(v, XmlAttribute):
                a_of = v.attribute_of
                if a_of is not None:
                    type_info.attributes[k] = type_info[a_of]

            elif issubclass(v, Array):
                v2, = v._type_info.values()
                while issubclass(v2, Array):
                    v = v2
                    v2, = v2._type_info.values()

                if issubclass(v2, SelfReference):
                    v._set_serializer(self)

        # FIXME: Implement this better
        new_type_info = []
        for k, v in self._type_info.items():
            if v.Attributes.order == None:
                new_type_info.append(k)

        for k, v in self._type_info.items():
            if v.Attributes.order is not None:
                new_type_info.insert(v.Attributes.order, k)

        assert len(self._type_info) == len(new_type_info)
        self._type_info.keys()[:] = new_type_info

        tn = self.Attributes.table_name
        meta = self.Attributes.sqla_metadata
        t = self.Attributes.sqla_table

        methods = _gen_methods(self, cls_dict)
        if len(methods) > 0:
            self.Attributes.methods = methods

        methods = self.Attributes.methods
        if methods is not None:
            assert isinstance(methods, _MethodsDict)
            methods.sanitize(self)

        # For spyne objects reflecting an existing db table
        if tn is None:
            if t is not None:
                self.Attributes.sqla_metadata = t.metadata
                from spyne.util.sqlalchemy import gen_spyne_info

                gen_spyne_info(self)

        # For spyne objects being converted to a sqlalchemy table
        elif meta is not None and (tn is not None or t is not None) and \
                                                       len(self._type_info) > 0:
            from spyne.util.sqlalchemy import gen_sqla_info

            gen_sqla_info(self, cls_bases)

        super(ComplexModelMeta, self).__init__(cls_name, cls_bases, cls_dict)

    #
    # We record the order fields are defined into ordered dict, so we can
    # declare them in the same order in the WSDL.
    #
    # For Python 3 __prepare__ works out of the box, see PEP 3115.
    # But we use `Preparable` metaclass for both Python 2 and Python 3 to
    # support six.add_metaclass decorator
    #
    @classmethod
    def __prepare__(mcs, name, bases, **kwds):
        return odict()


# FIXME: what an ugly hack.
def _fill_empty_type_name(v, parent_ns, parent_tn, k):
    v.__namespace__ = parent_ns
    tn = "%s_%s%s" % (parent_tn, k, const.TYPE_SUFFIX)

    if issubclass(v, Array):
        child_v, = v._type_info.values()
        child_v.__type_name__ = tn

        v._type_info = TypeInfo({tn: child_v})
        v.__type_name__ = '%s%s%s'% (const.ARRAY_PREFIX, tn, const.ARRAY_SUFFIX)

        extends = child_v.__extends__
        while extends is not None and extends.get_type_name() is v.Empty:
            _fill_empty_type_name(extends, parent_ns, parent_tn,
                                                        k + const.PARENT_SUFFIX)
            extends = extends.__extends__

    elif issubclass(v, XmlModifier):
        child_v = v.type
        child_v.__type_name__ = tn

        v._type_info = TypeInfo({tn: child_v})
        v.__type_name__ = '%s%s%s'% (const.ARRAY_PREFIX, tn, const.ARRAY_SUFFIX)

        extends = child_v.__extends__
        while extends is not None and extends.get_type_name() is v.Empty:
            _fill_empty_type_name(extends, parent_ns, parent_tn,
                                                        k + const.PARENT_SUFFIX)
            extends = extends.__extends__

    else:
        v.__type_name__ = "%s_%s%s" % (parent_tn, k, const.TYPE_SUFFIX)
        extends = v.__extends__
        while extends is not None and extends.__type_name__ is ModelBase.Empty:
            _fill_empty_type_name(v.__extends__, v.get_namespace(),
                                     v.get_type_name(), k + const.PARENT_SUFFIX)
            extends = extends.__extends__

_is_array = lambda v: issubclass(v, Array) or (v.Attributes.min_occurs > 1)


class ComplexModelBase(ModelBase):
    """If you want to make a better class type, this is what you should inherit
    from.
    """

    __mixin__ = False

    class Attributes(ModelBase.Attributes):
        """ComplexModel-specific attributes"""

        store_as = None
        """Method for serializing to persistent storage. One of %r. It makes
        sense to specify this only when this object is a child of another
        ComplexModel sublass.""" % (PSSM_VALUES,)

        sqla_metadata = None
        """None or :class:`sqlalchemy.MetaData` instance."""

        sqla_table_args = None
        """A dict that will be passed to :class:`sqlalchemy.schema.Table`
        constructor as ``**kwargs``.
        """

        sqla_mapper_args = None
        """A dict that will be passed to :func:`sqlalchemy.orm.mapper`
        constructor as. ``**kwargs``.
        """

        sqla_table = None
        """The sqlalchemy table object"""

        sqla_mapper = None
        """The sqlalchemy mapper object"""

        validate_freq = True
        """When ``False``, soft validation ignores missing mandatory attributes.
        """

        child_attrs = None
        """Customize child attributes in one go. It's a dict of dicts. This is
        ignored unless used via explicit customization."""

        declare_order = None
        """The order fields of the :class:``ComplexModel`` are to be declared
        in the SOAP WSDL. If this is left as None or explicitly set to
        ``'random'`` declares then the fields appear in whatever order the
        Python's hash map implementation seems fit in the WSDL. This randomised
        order can change every time the program is run. This is what Spyne <2.11
        did if you didn't set _type_info as an explicit sequence (e.g. using a
        list, odict, etc.). It means that clients who are manually complied or
        generated from the WSDL will likely need to be recompiled every time it
        changes. The string ``name`` means the field names are alphabetically
        sorted in the WSDL declaration.  The string ``declared`` means in the
        order the field type was declared in Python 2, and the order the
        field was declared in Python 3.

        In order to get declared field order in Python 2, the
        :class:`spyne.util.meta.Preparable` class inspects the frame stack in
        order to locate the class definition, re-parses it to get declaration
        order from the AST and uses that information to order elements.

        It's a horrible hack that we tested to work with CPython 2.6 through 3.3
        and PyPy. It breaks in Nuitka as Nuitka does away with code objects.
        Other platforms were not tested.

        It's not recommended to use set this to ``'declared'`` in Python 2
        unless you're sure you fully understand the consequences.
        """

        parent_variant = None
        """FIXME: document me yo."""

        methods = None
        """FIXME: document me yo."""

        _variants = None
        _xml_tag_body_as = None
        _delayed_child_attrs = None
        _subclasses = None

    def __init__(self, *args, **kwargs):
        cls = self.__class__
        fti = cls.get_flat_type_info(cls)

        if cls.Attributes._xml_tag_body_as is not None:
            for arg, (xtba_key, xtba_type) in \
                                     zip(args, cls.Attributes._xml_tag_body_as):
                if xtba_key is not None and len(args) == 1:
                    self._safe_set(xtba_key, arg, xtba_type)
                elif len(args) > 0:
                    raise TypeError(
                                "Positional argument is only for ComplexModels "
                                "with XmlData field. You must use keyword "
                                "arguments in any other case.")

        for k, v in fti.items():
            if k in kwargs:
                self._safe_set(k, kwargs[k], v)

            elif not k in self.__dict__:
                attr = v.Attributes
                def_val = attr.default
                def_fac = attr.default_factory

                if def_fac is not None:
                    if six.PY2 and hasattr(def_fac, 'im_func'):
                        # unbound-method error workaround. huh.
                        def_fac = def_fac.im_func
                    dval = def_fac()

                    # should not check for read-only for default values
                    setattr(self, k, dval)

                elif def_val is not None:
                    # should not check for read-only for default values
                    setattr(self, k, def_val)

                # sqlalchemy objects do their own init.
                elif '_sa_class_manager' in cls.__dict__:
                    # except the attributes that sqlalchemy doesn't know about
                    if v.Attributes.exc_table:
                        try:
                            setattr(self, k, None)
                        except AttributeError: # it could be a read-only property<commit_msg>more robust sa instance check<commit_after>
#
# spyne - Copyright (C) Spyne contributors.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
#

"""The ``spyne.model.complex`` module contains
:class:`spyne.model.complex.ComplexBase` class and its helper objects and
subclasses. These are mainly container classes for other simple or
complex objects -- they don't carry any data by themselves.
"""

from __future__ import print_function

import logging
logger = logging.getLogger(__name__)

import decimal

from weakref import WeakKeyDictionary
from collections import deque
from inspect import isclass
from itertools import chain

from spyne import BODY_STYLE_BARE, BODY_STYLE_WRAPPED, BODY_STYLE_EMPTY
from spyne import const
from spyne.const import xml_ns

from spyne.model import Point
from spyne.model import Unicode
from spyne.model import PushBase
from spyne.model import ModelBase
from spyne.model import json, xml, msgpack, table
from spyne.model._base import apply_pssm
from spyne.model.primitive import NATIVE_MAP

from spyne.util import six
from spyne.util import memoize
from spyne.util import memoize_id
from spyne.util import sanitize_args
from spyne.util.meta import Prepareable
from spyne.util.odict import odict
from spyne.util.six import add_metaclass, with_metaclass, string_types


PSSM_VALUES = {'json': json, 'xml': xml, 'msgpack': msgpack, 'table': table}

def _get_flat_type_info(cls, retval):
    assert isinstance(retval, TypeInfo)
    parent = getattr(cls, '__extends__', None)
    if parent != None:
        _get_flat_type_info(parent, retval)
    retval.update(cls._type_info)
    retval.alt.update(cls._type_info_alt) # FIXME: move to cls._type_info.alt
    return retval


class TypeInfo(odict):
    def __init__(self, *args, **kwargs):
        super(TypeInfo, self).__init__(*args, **kwargs)
        self.attributes = {}
        self.alt = {}

    def __setitem__(self, key, val):
        assert isinstance(key, string_types)
        super(TypeInfo, self).__setitem__(key, val)


class _SimpleTypeInfoElement(object):
    __slots__ = ['path', 'parent', 'type', 'is_array']

    def __init__(self, path, parent, type_, is_array):
        self.path = path
        self.parent = parent
        self.type = type_
        self.is_array = is_array

    def __repr__(self):
        return "SimpleTypeInfoElement(path=%r, parent=%r, type=%r, is_array=%r)" \
                            % (self.path, self.parent, self.type, self.is_array)


class XmlModifier(ModelBase):
    def __new__(cls, type, ns=None):
        retval = cls.customize()
        retval.type = type
        retval.Attributes = type.Attributes
        retval._ns = ns
        if type.__type_name__ is ModelBase.Empty:
            retval.__type_name__ = ModelBase.Empty
        return retval

    @staticmethod
    def resolve_namespace(cls, default_ns, tags=None):
        cls.type.resolve_namespace(cls.type, default_ns, tags)

        cls.__namespace__ = cls._ns

        if cls.__namespace__ is None:
            cls.__namespace__ = cls.type.get_namespace()

        if cls.__namespace__ in xml_ns.const_prefmap:
            cls.__namespace__ = default_ns


class XmlData(XmlModifier):
    """Items which are marshalled as data of the parent element."""

    @classmethod
    def marshall(cls, prot, name, value, parent_elt):
        if value is not None:
            if len(parent_elt) == 0:
                parent_elt.text = prot.to_string(cls.type, value)
            else:
                parent_elt[-1].tail = prot.to_string(cls.type, value)

    @classmethod
    def get_type_name(cls):
        return cls.type.get_type_name()

    @classmethod
    def get_type_name_ns(cls, interface):
        return cls.type.get_type_name_ns(interface)

    @classmethod
    def get_namespace(cls):
        return cls.type.get_namespace()

    @classmethod
    def get_element_name(cls):
        return cls.type.get_element_name()

    @classmethod
    def get_element_name_ns(cls, interface):
        return cls.type.get_element_name_ns(interface)


class XmlAttribute(XmlModifier):
    """Items which are marshalled as attributes of the parent element. If
    ``attribute_of`` is passed, it's marshalled as the attribute of the element
    with given name.
    """

    def __new__(cls, type_, use=None, ns=None, attribute_of=None):
        retval = super(XmlAttribute, cls).__new__(cls, type_, ns)
        retval._use = use
        if retval.type.Attributes.min_occurs > 0 and retval._use is None:
            retval._use = 'required'
        retval.attribute_of = attribute_of
        return retval


class XmlAttributeRef(XmlAttribute):
    """Reference to an Xml attribute."""

    def __init__(self, ref, use=None):
        self._ref = ref
        self._use = use

    def describe(self, name, element, app):
        element.set('ref', self._ref)
        if self._use:
            element.set('use', self._use)


class SelfReference(object):
    """Use this as a placeholder type in classes that contain themselves. See
    :func:`spyne.test.model.test_complex.TestComplexModel.test_self_reference`.
    """
    customize_args = []
    customize_kwargs = {}
    __orig__ = None

    def __init__(self):
        raise NotImplementedError()

    @classmethod
    def customize(cls, *args, **kwargs):
        args = chain(args, cls.customize_args)
        kwargs = dict(chain(kwargs.items(), cls.customize_kwargs.items()))
        if cls.__orig__ is None:
            cls.__orig__ = cls

        return type("SelfReference", (cls,), {
            'customize_args': args,
            'customize_kwargs': kwargs,
        })

def _get_spyne_type(cls_name, k, v):
    try:
        v = NATIVE_MAP.get(v, v)
    except TypeError:
        return

    try:
        subc = issubclass(v, ModelBase) or issubclass(v, SelfReference)
    except:
        subc = False

    if subc:
        if issubclass(v, Array) and len(v._type_info) != 1:
            raise Exception("Invalid Array definition in %s.%s."% (cls_name, k))
        elif issubclass(v, Point) and v.Attributes.dim is None:
            raise Exception("Please specify the number of dimensions")
        return v


def _join_args(x, y):
    if x is None:
        return y
    if y is None:
        return x

    xa, xk = sanitize_args(x)
    ya, yk = sanitize_args(y)

    xk = dict(xk)
    xk.update(yk)

    return xa + ya, xk


def _gen_attrs(cls_bases, cls_dict):
    attrs = cls_dict.get('Attributes', None)
    if attrs is None:
        for b in cls_bases:
            if hasattr(b, 'Attributes'):
                class Attributes(b.Attributes):
                    pass
                attrs = cls_dict['Attributes'] = Attributes
                break
        else:
            raise Exception("No ModelBase subclass in bases? Huh?")

    return attrs


def _get_type_info(cls, cls_name, cls_bases, cls_dict, attrs):
    base_type_info = TypeInfo()
    mixin = {}
    extends = cls_dict.get('__extends__', None)
    if extends is None:
        for b in cls_bases:
            base_types = getattr(b, "_type_info", None)

            if base_types is not None:
                if getattr(b, '__mixin__', False) == True:
                    mixin = b.get_flat_type_info(b)
                else:
                    if not (extends in (None, b)):
                        raise Exception("Spyne objects do not support multiple "
                            "inheritance. Use mixins if you need to reuse "
                            "fields from multiple classes.")

                    try:
                        if len(base_types) > 0 and issubclass(b, ModelBase):
                            extends = cls_dict["__extends__"] = b
                            b.get_subclasses.memo.clear()

                    except Exception as e:
                        logger.exception(e)
                        logger.error(repr(extends))
                        raise

    if not ('_type_info' in cls_dict):
        cls_dict['_type_info'] = _type_info = TypeInfo()
        _type_info.update(base_type_info)

        class_fields = []
        for k, v in cls_dict.items():
            if not k.startswith('_'):
                v = _get_spyne_type(cls_name, k, v)
                if v is not None:
                    class_fields.append((k, v))

        _type_info.update(class_fields)

    else:
        _type_info = cls_dict['_type_info']

        if not isinstance(_type_info, TypeInfo):
            _type_info = cls_dict['_type_info'] = TypeInfo(_type_info)

    _type_info.update(mixin)
    return _type_info


class _MethodsDict(dict):
    def __init__(self, *args, **kwargs):
        super(_MethodsDict, self).__init__(*args, **kwargs)

        self._processed = False

    def sanitize(self, cls):
        # sanitize is called on every customization, so we make sure it's run
        # only once in this class' lifetime.
        if self._processed:
            return

        self._processed = True

        for d in self.values():
            d.parent_class = cls

            if d.in_message_name_override:
                d.in_message.__type_name__ = '%s.%s' % \
                          (cls.get_type_name(), d.in_message.get_type_name())

            if d.body_style is BODY_STYLE_WRAPPED or d.out_message_name_override:
                d.out_message.__type_name__ = '%s.%s' % \
                          (cls.get_type_name(), d.out_message.get_type_name())

            if d.body_style in (BODY_STYLE_BARE, BODY_STYLE_EMPTY):
                # The method only needs the primary key(s) and shouldn't
                # complain when other mandatory fields are missing.
                d.in_message = cls.novalidate_freq()
                d.body_style = BODY_STYLE_BARE

            else:
                d.in_message.insert_field(0, 'self', cls.novalidate_freq())
                d.body_style = BODY_STYLE_WRAPPED

                if issubclass(d.in_message, ComplexModelBase):
                    for k, v in d.in_message._type_info.items():
                        # SelfReference is replaced by descriptor.in_message
                        # itself. However, in the context of mrpc, SelfReference
                        # means parent class. here, we do that substitution.
                        # It's a safe hack but a hack nevertheless.
                        if v is d.in_message:
                            d.in_message._type_info[k] = cls

            # Same as above, for the output type.
            if issubclass(d.out_message, ComplexModelBase):
                for k, v in d.out_message._type_info.items():
                    if v is d.out_message:
                        d.out_message._type_info[k] = cls

            if d.patterns is not None and not d.no_self:
                d.name = '.'.join((cls.get_type_name(), d.name))
                for p in d.patterns:
                    if p.address is None:
                        p.address = d.name


def _gen_methods(cls, cls_dict):
    methods = _MethodsDict()
    for k, v in cls_dict.items():
        if not k.startswith('_') and hasattr(v, '_is_rpc'):
            descriptor = v(_default_function_name=k, _self_ref_replacement=cls)
            setattr(cls, k, descriptor.function)
            methods[k] = descriptor

    return methods


def _get_ordered_attributes(cls_name, cls_dict, attrs):
    if not isinstance(cls_dict, odict):
        # FIXME: Maybe add a warning here?
        return cls_dict

    SUPPORTED_ORDERS = ('random', 'declared')
    if (attrs.declare_order is not None and
            not attrs.declare_order in SUPPORTED_ORDERS):

        msg = "The declare_order attribute value %r is invalid in %s"
        raise Exception(msg % (attrs.declare_order, cls_name))

    declare_order = attrs.declare_order or const.DEFAULT_DECLARE_ORDER
    if declare_order is None or declare_order == 'random':
        # support old behaviour
        cls_dict = dict(cls_dict)

    return cls_dict


def _sanitize_sqlalchemy_parameters(cls_dict, attrs):
    table_name = cls_dict.get('__tablename__', None)
    if attrs.table_name is None:
        attrs.table_name = table_name

    _cls_table = cls_dict.get('__table__', None)
    if attrs.sqla_table is None:
        attrs.sqla_table = _cls_table

    metadata = cls_dict.get('__metadata__', None)
    if attrs.sqla_metadata is None:
        attrs.sqla_metadata = metadata

    margs = cls_dict.get('__mapper_args__', None)
    attrs.sqla_mapper_args = _join_args(attrs.sqla_mapper_args, margs)

    targs = cls_dict.get('__table_args__', None)
    attrs.sqla_table_args = _join_args(attrs.sqla_table_args, targs)


def _sanitize_type_info(cls_name, _type_info, _type_info_alt):
    # make sure _type_info contents are sane
    for k, v in _type_info.items():
        if not isinstance(k, six.string_types):
            raise ValueError("Invalid class key", k)
        if not isclass(v):
            raise ValueError(v)

        if issubclass(v, SelfReference):
            continue

        elif not issubclass(v, ModelBase):
            v = _get_spyne_type(cls_name, k, v)
            if v is None:
                raise ValueError( (cls_name, k, v) )
            _type_info[k] = v

        elif issubclass(v, Array) and len(v._type_info) != 1:
            raise Exception("Invalid Array definition in %s.%s." %
                                                        (cls_name, k))
        sub_ns = v.Attributes.sub_ns
        sub_name = v.Attributes.sub_name

        if sub_ns is None and sub_name is None:
            pass

        elif sub_ns is not None and sub_name is not None:
            key = "{%s}%s" % (sub_ns, sub_name)
            if key in _type_info:
                raise Exception("%r is already defined: %r" %
                                                         (key, _type_info[key]))
            _type_info_alt[key] = v, k

        elif sub_ns is None:
            key = sub_name
            if sub_ns in _type_info:
                raise Exception("%r is already defined: %r" %
                                                        (key, _type_info[key]))
            _type_info_alt[key] = v, k

        elif sub_name is None:
            key = "{%s}%s" % (sub_ns, k)
            if key in _type_info:
                raise Exception("%r is already defined: %r" %
                                                        (key, _type_info[key]))
            _type_info_alt[key] = v, k


class ComplexModelMeta(with_metaclass(Prepareable, type(ModelBase))):
    """This metaclass sets ``_type_info``, ``__type_name__`` and ``__extends__``
    which are going to be used for (de)serialization and schema generation.
    """

    def __new__(cls, cls_name, cls_bases, cls_dict):
        """This function initializes the class and registers attributes for
        serialization.
        """

        attrs = _gen_attrs(cls_bases, cls_dict)
        assert issubclass(attrs, ComplexModelBase.Attributes), \
                   ("%r must be a ComplexModelBase.Attributes subclass" % attrs)

        cls_dict = _get_ordered_attributes(cls_name, cls_dict, attrs)

        type_name = cls_dict.get("__type_name__", None)
        if type_name is None:
            cls_dict["__type_name__"] = cls_name

        _type_info = _get_type_info(cls, cls_name, cls_bases, cls_dict, attrs)

        # used for sub_name and sub_ns
        _type_info_alt = cls_dict['_type_info_alt'] = TypeInfo()
        for b in cls_bases:
            if hasattr(b, '_type_info_alt'):
                _type_info_alt.update(b._type_info_alt)

        _sanitize_type_info(cls_name, _type_info, _type_info_alt)
        _sanitize_sqlalchemy_parameters(cls_dict, attrs)

        return super(ComplexModelMeta, cls).__new__(cls, cls_name, cls_bases,
                                                    cls_dict)

    def __init__(self, cls_name, cls_bases, cls_dict):
        type_info = self._type_info

        extends = self.__extends__
        if extends is not None and self.__orig__ is None:
            eattr = extends.Attributes;
            if eattr._subclasses is None:
                eattr._subclasses = []
            eattr._subclasses.append(self)
            if self.Attributes._subclasses is eattr._subclasses:
                self.Attributes._subclasses = None

        for k, v in type_info.items():
            if issubclass(v, SelfReference):
                self.replace_field(k, self.customize(*v.customize_args,
                                                          **v.customize_kwargs))

            elif issubclass(v, XmlData):
                if self.Attributes._xml_tag_body_as is None:
                    self.Attributes._xml_tag_body_as = [(k, v)]
                else:
                    self.Attributes._xml_tag_body_as.append((k, v))

            elif issubclass(v, XmlAttribute):
                a_of = v.attribute_of
                if a_of is not None:
                    type_info.attributes[k] = type_info[a_of]

            elif issubclass(v, Array):
                v2, = v._type_info.values()
                while issubclass(v2, Array):
                    v = v2
                    v2, = v2._type_info.values()

                if issubclass(v2, SelfReference):
                    v._set_serializer(self)

        # FIXME: Implement this better
        new_type_info = []
        for k, v in self._type_info.items():
            if v.Attributes.order == None:
                new_type_info.append(k)

        for k, v in self._type_info.items():
            if v.Attributes.order is not None:
                new_type_info.insert(v.Attributes.order, k)

        assert len(self._type_info) == len(new_type_info)
        self._type_info.keys()[:] = new_type_info

        tn = self.Attributes.table_name
        meta = self.Attributes.sqla_metadata
        t = self.Attributes.sqla_table

        methods = _gen_methods(self, cls_dict)
        if len(methods) > 0:
            self.Attributes.methods = methods

        methods = self.Attributes.methods
        if methods is not None:
            assert isinstance(methods, _MethodsDict)
            methods.sanitize(self)

        # For spyne objects reflecting an existing db table
        if tn is None:
            if t is not None:
                self.Attributes.sqla_metadata = t.metadata
                from spyne.util.sqlalchemy import gen_spyne_info

                gen_spyne_info(self)

        # For spyne objects being converted to a sqlalchemy table
        elif meta is not None and (tn is not None or t is not None) and \
                                                       len(self._type_info) > 0:
            from spyne.util.sqlalchemy import gen_sqla_info

            gen_sqla_info(self, cls_bases)

        super(ComplexModelMeta, self).__init__(cls_name, cls_bases, cls_dict)

    #
    # We record the order fields are defined into ordered dict, so we can
    # declare them in the same order in the WSDL.
    #
    # For Python 3 __prepare__ works out of the box, see PEP 3115.
    # But we use `Preparable` metaclass for both Python 2 and Python 3 to
    # support six.add_metaclass decorator
    #
    @classmethod
    def __prepare__(mcs, name, bases, **kwds):
        return odict()


# FIXME: what an ugly hack.
def _fill_empty_type_name(v, parent_ns, parent_tn, k):
    v.__namespace__ = parent_ns
    tn = "%s_%s%s" % (parent_tn, k, const.TYPE_SUFFIX)

    if issubclass(v, Array):
        child_v, = v._type_info.values()
        child_v.__type_name__ = tn

        v._type_info = TypeInfo({tn: child_v})
        v.__type_name__ = '%s%s%s'% (const.ARRAY_PREFIX, tn, const.ARRAY_SUFFIX)

        extends = child_v.__extends__
        while extends is not None and extends.get_type_name() is v.Empty:
            _fill_empty_type_name(extends, parent_ns, parent_tn,
                                                        k + const.PARENT_SUFFIX)
            extends = extends.__extends__

    elif issubclass(v, XmlModifier):
        child_v = v.type
        child_v.__type_name__ = tn

        v._type_info = TypeInfo({tn: child_v})
        v.__type_name__ = '%s%s%s'% (const.ARRAY_PREFIX, tn, const.ARRAY_SUFFIX)

        extends = child_v.__extends__
        while extends is not None and extends.get_type_name() is v.Empty:
            _fill_empty_type_name(extends, parent_ns, parent_tn,
                                                        k + const.PARENT_SUFFIX)
            extends = extends.__extends__

    else:
        v.__type_name__ = "%s_%s%s" % (parent_tn, k, const.TYPE_SUFFIX)
        extends = v.__extends__
        while extends is not None and extends.__type_name__ is ModelBase.Empty:
            _fill_empty_type_name(v.__extends__, v.get_namespace(),
                                     v.get_type_name(), k + const.PARENT_SUFFIX)
            extends = extends.__extends__

_is_array = lambda v: issubclass(v, Array) or (v.Attributes.min_occurs > 1)


class ComplexModelBase(ModelBase):
    """If you want to make a better class type, this is what you should inherit
    from.
    """

    __mixin__ = False

    class Attributes(ModelBase.Attributes):
        """ComplexModel-specific attributes"""

        store_as = None
        """Method for serializing to persistent storage. One of %r. It makes
        sense to specify this only when this object is a child of another
        ComplexModel sublass.""" % (PSSM_VALUES,)

        sqla_metadata = None
        """None or :class:`sqlalchemy.MetaData` instance."""

        sqla_table_args = None
        """A dict that will be passed to :class:`sqlalchemy.schema.Table`
        constructor as ``**kwargs``.
        """

        sqla_mapper_args = None
        """A dict that will be passed to :func:`sqlalchemy.orm.mapper`
        constructor as. ``**kwargs``.
        """

        sqla_table = None
        """The sqlalchemy table object"""

        sqla_mapper = None
        """The sqlalchemy mapper object"""

        validate_freq = True
        """When ``False``, soft validation ignores missing mandatory attributes.
        """

        child_attrs = None
        """Customize child attributes in one go. It's a dict of dicts. This is
        ignored unless used via explicit customization."""

        declare_order = None
        """The order fields of the :class:``ComplexModel`` are to be declared
        in the SOAP WSDL. If this is left as None or explicitly set to
        ``'random'`` declares then the fields appear in whatever order the
        Python's hash map implementation seems fit in the WSDL. This randomised
        order can change every time the program is run. This is what Spyne <2.11
        did if you didn't set _type_info as an explicit sequence (e.g. using a
        list, odict, etc.). It means that clients who are manually complied or
        generated from the WSDL will likely need to be recompiled every time it
        changes. The string ``name`` means the field names are alphabetically
        sorted in the WSDL declaration.  The string ``declared`` means in the
        order the field type was declared in Python 2, and the order the
        field was declared in Python 3.

        In order to get declared field order in Python 2, the
        :class:`spyne.util.meta.Preparable` class inspects the frame stack in
        order to locate the class definition, re-parses it to get declaration
        order from the AST and uses that information to order elements.

        It's a horrible hack that we tested to work with CPython 2.6 through 3.3
        and PyPy. It breaks in Nuitka as Nuitka does away with code objects.
        Other platforms were not tested.

        It's not recommended to use set this to ``'declared'`` in Python 2
        unless you're sure you fully understand the consequences.
        """

        parent_variant = None
        """FIXME: document me yo."""

        methods = None
        """FIXME: document me yo."""

        _variants = None
        _xml_tag_body_as = None
        _delayed_child_attrs = None
        _subclasses = None

    def __init__(self, *args, **kwargs):
        cls = self.__class__
        fti = cls.get_flat_type_info(cls)

        if cls.Attributes._xml_tag_body_as is not None:
            for arg, (xtba_key, xtba_type) in \
                                     zip(args, cls.Attributes._xml_tag_body_as):
                if xtba_key is not None and len(args) == 1:
                    self._safe_set(xtba_key, arg, xtba_type)
                elif len(args) > 0:
                    raise TypeError(
                                "Positional argument is only for ComplexModels "
                                "with XmlData field. You must use keyword "
                                "arguments in any other case.")

        for k, v in fti.items():
            if k in kwargs:
                self._safe_set(k, kwargs[k], v)

            elif not k in self.__dict__:
                attr = v.Attributes
                def_val = attr.default
                def_fac = attr.default_factory

                if def_fac is not None:
                    if six.PY2 and hasattr(def_fac, 'im_func'):
                        # unbound-method error workaround. huh.
                        def_fac = def_fac.im_func
                    dval = def_fac()

                    # should not check for read-only for default values
                    setattr(self, k, dval)

                elif def_val is not None:
                    # should not check for read-only for default values
                    setattr(self, k, def_val)

                # sqlalchemy objects do their own init.
                elif hasattr(cls, '_sa_class_manager'):
                    # except the attributes that sqlalchemy doesn't know about
                    if v.Attributes.exc_table:
                        try:
                            setattr(self, k, None)
                        except AttributeError: # it could be a read-only property
{'fraction_non_alphanumeric': 0.0837735707168506, 'fraction_numerical': 0.0018800172961591247, 'mean_word_length': 2.032611174458381, 'pattern_counts': {'":': 0, '<': 6, '<?xml version=': 0, '>': 16, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 22}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 40, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2189247', 'n_tokens_mistral': 15773, 'n_tokens_neox': 15260, 'n_words': 4529}