You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
551 lines
20 KiB
551 lines
20 KiB
|
|
#
|
|
# 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
|
|
#
|
|
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
from collections import deque, defaultdict
|
|
|
|
import spyne.interface
|
|
|
|
from spyne import EventManager, MethodDescriptor
|
|
from spyne.util import six
|
|
from spyne.model import ModelBase, Array, Iterable, ComplexModelBase
|
|
from spyne.model.complex import XmlModifier
|
|
from spyne.const import xml as namespace
|
|
|
|
|
|
class Interface(object):
|
|
"""The ``Interface`` class holds all information needed to build an
|
|
interface document.
|
|
|
|
:param app: A :class:`spyne.application.Application` instance.
|
|
"""
|
|
|
|
def __init__(self, app=None, import_base_namespaces=False):
|
|
self.__ns_counter = 0
|
|
self.__app = None
|
|
self.url = None
|
|
self.classes = {}
|
|
self.imports = {}
|
|
self.service_method_map = {}
|
|
self.method_id_map = {}
|
|
self.nsmap = {}
|
|
self.prefmap = {}
|
|
self.member_methods = deque()
|
|
self.method_descriptor_id_to_key = {}
|
|
self.service_attrs = defaultdict(dict)
|
|
|
|
self.import_base_namespaces = import_base_namespaces
|
|
self.app = app
|
|
|
|
def set_app(self, value):
|
|
assert self.__app is None, "One interface instance can belong to only " \
|
|
"one application instance."
|
|
|
|
self.__app = value
|
|
self.reset_interface()
|
|
self.populate_interface()
|
|
|
|
def get_app(self):
|
|
return self.__app
|
|
|
|
app = property(get_app, set_app)
|
|
|
|
@property
|
|
def services(self):
|
|
if self.__app:
|
|
return self.__app.services
|
|
return []
|
|
|
|
def reset_interface(self):
|
|
self.classes = {}
|
|
self.imports = {self.get_tns(): set()}
|
|
self.service_method_map = {}
|
|
self.method_id_map = {}
|
|
self.nsmap = dict(namespace.NSMAP)
|
|
self.prefmap = dict(namespace.PREFMAP)
|
|
self.member_methods = deque()
|
|
|
|
self.nsmap['tns'] = self.get_tns()
|
|
self.prefmap[self.get_tns()] = 'tns'
|
|
self.deps = defaultdict(set)
|
|
|
|
def has_class(self, cls):
|
|
"""Returns true if the given class is already included in the interface
|
|
object somewhere."""
|
|
|
|
ns = cls.get_namespace()
|
|
tn = cls.get_type_name()
|
|
|
|
key = '{%s}%s' % (ns, tn)
|
|
c = self.classes.get(key)
|
|
if c is None:
|
|
return False
|
|
|
|
if issubclass(c, ComplexModelBase) and \
|
|
issubclass(cls, ComplexModelBase):
|
|
o1 = getattr(cls, '__orig__', None) or cls
|
|
o2 = getattr(c, '__orig__', None) or c
|
|
|
|
if o1 is o2:
|
|
return True
|
|
|
|
# So that "Array"s and "Iterable"s don't conflict.
|
|
if set((o1, o2)) == set((Array, Iterable)):
|
|
return True
|
|
|
|
raise ValueError("classes %r and %r have conflicting names: '%s'" %
|
|
(cls, c, key))
|
|
return True
|
|
|
|
def get_class(self, key):
|
|
"""Returns the class definition that corresponds to the given key.
|
|
Keys are in '{namespace}class_name' form.
|
|
|
|
Not meant to be overridden.
|
|
"""
|
|
return self.classes[key]
|
|
|
|
def get_class_instance(self, key):
|
|
"""Returns the default class instance that corresponds to the given key.
|
|
Keys are in '{namespace}class_name' form, a.k.a. XML QName format.
|
|
Classes should not enforce arguments to the constructor.
|
|
|
|
Not meant to be overridden.
|
|
"""
|
|
return self.classes[key]()
|
|
|
|
def get_name(self):
|
|
"""Returns service name that is seen in the name attribute of the
|
|
definitions tag.
|
|
|
|
Not meant to be overridden.
|
|
"""
|
|
|
|
if self.app:
|
|
return self.app.name
|
|
|
|
def get_tns(self):
|
|
"""Returns default namespace that is seen in the targetNamespace
|
|
attribute of the definitions tag.
|
|
|
|
Not meant to be overridden.
|
|
"""
|
|
if self.app:
|
|
return self.app.tns
|
|
|
|
def add_method(self, method):
|
|
"""Generator method that adds the given method descriptor to the
|
|
interface. Also extracts and yields all the types found in there.
|
|
|
|
:param method: A :class:`MethodDescriptor` instance
|
|
:returns: Sequence of :class:`spyne.model.ModelBase` subclasses.
|
|
"""
|
|
|
|
if not (method.in_header is None):
|
|
if not isinstance(method.in_header, (list, tuple)):
|
|
method.in_header = (method.in_header,)
|
|
|
|
for in_header in method.in_header:
|
|
in_header.resolve_namespace(in_header, self.get_tns())
|
|
if method.aux is None:
|
|
yield in_header
|
|
in_header_ns = in_header.get_namespace()
|
|
if in_header_ns != self.get_tns() and \
|
|
self.is_valid_import(in_header_ns):
|
|
self.imports[self.get_tns()].add(in_header_ns)
|
|
|
|
if not (method.out_header is None):
|
|
if not isinstance(method.out_header, (list, tuple)):
|
|
method.out_header = (method.out_header,)
|
|
|
|
for out_header in method.out_header:
|
|
out_header.resolve_namespace(out_header, self.get_tns())
|
|
if method.aux is None:
|
|
yield out_header
|
|
out_header_ns = out_header.get_namespace()
|
|
if out_header_ns != self.get_tns() and \
|
|
self.is_valid_import(out_header_ns):
|
|
self.imports[self.get_tns()].add(out_header_ns)
|
|
|
|
if method.faults is None:
|
|
method.faults = []
|
|
elif not (isinstance(method.faults, (list, tuple))):
|
|
method.faults = (method.faults,)
|
|
|
|
for fault in method.faults:
|
|
fault.__namespace__ = self.get_tns()
|
|
fault.resolve_namespace(fault, self.get_tns())
|
|
if method.aux is None:
|
|
yield fault
|
|
|
|
method.in_message.resolve_namespace(method.in_message, self.get_tns())
|
|
in_message_ns = method.in_message.get_namespace()
|
|
if in_message_ns != self.get_tns() and \
|
|
self.is_valid_import(in_message_ns):
|
|
self.imports[self.get_tns()].add(method.in_message.get_namespace())
|
|
|
|
if method.aux is None:
|
|
yield method.in_message
|
|
|
|
method.out_message.resolve_namespace(method.out_message, self.get_tns())
|
|
assert not method.out_message.get_type_name() is method.out_message.Empty
|
|
|
|
out_message_ns = method.out_message.get_namespace()
|
|
if out_message_ns != self.get_tns() and \
|
|
self.is_valid_import(out_message_ns):
|
|
self.imports[self.get_tns()].add(out_message_ns)
|
|
|
|
if method.aux is None:
|
|
yield method.out_message
|
|
|
|
for p in method.patterns:
|
|
p.endpoint = method
|
|
|
|
def process_method(self, s, method):
|
|
assert isinstance(method, MethodDescriptor)
|
|
|
|
method_key = u'{%s}%s' % (self.app.tns, method.name)
|
|
|
|
if issubclass(s, ComplexModelBase):
|
|
method_object_name = method.name.split('.', 1)[0]
|
|
if s.get_type_name() != method_object_name:
|
|
method_key = u'{%s}%s.%s' % (self.app.tns, s.get_type_name(),
|
|
method.name)
|
|
|
|
key = method.gen_interface_key(s)
|
|
if key in self.method_id_map:
|
|
c = self.method_id_map[key].parent_class
|
|
if c is None:
|
|
pass
|
|
|
|
elif c is s:
|
|
pass
|
|
|
|
elif c.__orig__ is None:
|
|
assert c is s.__orig__, "%r.%s conflicts with %r.%s" % \
|
|
(c, key, s.__orig__, key)
|
|
elif s.__orig__ is None:
|
|
assert c.__orig__ is s, "%r.%s conflicts with %r.%s" % \
|
|
(c.__orig__, key, s, key)
|
|
else:
|
|
assert c.__orig__ is s.__orig__, "%r.%s conflicts with %r.%s" % \
|
|
(c.__orig__, key, s.__orig__, key)
|
|
return
|
|
|
|
logger.debug(' adding method %s.%s to match %r tag.',
|
|
method.get_owner_name(s), six.get_function_name(method.function),
|
|
method_key)
|
|
|
|
self.method_id_map[key] = method
|
|
|
|
val = self.service_method_map.get(method_key, None)
|
|
if val is None:
|
|
val = self.service_method_map[method_key] = []
|
|
|
|
if len(val) == 0:
|
|
val.append(method)
|
|
|
|
elif method.aux is not None:
|
|
val.append(method)
|
|
|
|
elif val[0].aux is not None:
|
|
val.insert(method, 0)
|
|
|
|
else:
|
|
om = val[0]
|
|
os = om.service_class
|
|
if os is None:
|
|
os = om.parent_class
|
|
raise ValueError("\nThe message %r defined in both '%s.%s'"
|
|
" and '%s.%s'"
|
|
% (method.name, s.__module__, s.__name__,
|
|
os.__module__, os.__name__))
|
|
|
|
def check_method(self, method):
|
|
"""Override this if you need to cherry-pick methods added to the
|
|
interface document."""
|
|
|
|
return True
|
|
|
|
def populate_interface(self, types=None):
|
|
"""Harvests the information stored in individual classes' _type_info
|
|
dictionaries. It starts from function definitions and includes only
|
|
the used objects.
|
|
"""
|
|
|
|
# populate types
|
|
for s in self.services:
|
|
logger.debug("populating %s types...", s.get_internal_key())
|
|
|
|
for method in s.public_methods.values():
|
|
if method.in_header is None:
|
|
method.in_header = s.__in_header__
|
|
|
|
if method.out_header is None:
|
|
method.out_header = s.__out_header__
|
|
|
|
if method.aux is None:
|
|
method.aux = s.__aux__
|
|
|
|
if method.aux is not None:
|
|
method.aux.methods.append(method.gen_interface_key(s))
|
|
|
|
if not self.check_method(method):
|
|
logger.debug("method %s' discarded by check_method",
|
|
method.class_key)
|
|
continue
|
|
|
|
logger.debug(" enumerating classes for method '%s'",
|
|
method.class_key)
|
|
for cls in self.add_method(method):
|
|
self.add_class(cls)
|
|
|
|
# populate additional types
|
|
for c in self.app.classes:
|
|
self.add_class(c)
|
|
|
|
# populate call routes for service methods
|
|
for s in self.services:
|
|
self.service_attrs[s]['tns'] = self.get_tns()
|
|
logger.debug("populating '%s.%s' routes...", s.__module__,
|
|
s.__name__)
|
|
for method in s.public_methods.values():
|
|
self.process_method(s, method)
|
|
|
|
# populate call routes for member methods
|
|
for cls, method in self.member_methods:
|
|
should_we = True
|
|
if method.static_when is not None:
|
|
should_we = method.static_when(self.app)
|
|
logger.debug("static_when returned %r for %s "
|
|
"while populating methods", should_we, method.internal_key)
|
|
|
|
if should_we:
|
|
s = method.service_class
|
|
if s is not None:
|
|
if method.in_header is None:
|
|
method.in_header = s.__in_header__
|
|
|
|
if method.out_header is None:
|
|
method.out_header = s.__out_header__
|
|
|
|
# FIXME: There's no need to process aux info here as it's
|
|
# not currently known how to write aux member methods in the
|
|
# first place.
|
|
|
|
self.process_method(cls.__orig__ or cls, method)
|
|
|
|
# populate method descriptor id to method key map
|
|
self.method_descriptor_id_to_key = dict(((id(v[0]), k)
|
|
for k,v in self.service_method_map.items()))
|
|
|
|
logger.debug("From this point on, you're not supposed to make any "
|
|
"changes to the class and method structure of the exposed "
|
|
"services.")
|
|
|
|
tns = property(get_tns)
|
|
|
|
def get_namespace_prefix(self, ns):
|
|
"""Returns the namespace prefix for the given namespace. Creates a new
|
|
one automatically if it doesn't exist.
|
|
|
|
Not meant to be overridden.
|
|
"""
|
|
|
|
if not (isinstance(ns, str) or isinstance(ns, six.text_type)):
|
|
raise TypeError(ns)
|
|
|
|
if not (ns in self.prefmap):
|
|
pref = "s%d" % self.__ns_counter
|
|
while pref in self.nsmap:
|
|
self.__ns_counter += 1
|
|
pref = "s%d" % self.__ns_counter
|
|
|
|
self.prefmap[ns] = pref
|
|
self.nsmap[pref] = ns
|
|
|
|
self.__ns_counter += 1
|
|
|
|
else:
|
|
pref = self.prefmap[ns]
|
|
|
|
return pref
|
|
|
|
def add_class(self, cls, add_parent=True):
|
|
if self.has_class(cls):
|
|
return
|
|
|
|
ns = cls.get_namespace()
|
|
tn = cls.get_type_name()
|
|
|
|
assert ns is not None, ('either assign a namespace to the class or call'
|
|
' cls.resolve_namespace(cls, "some_default_ns") on it.')
|
|
|
|
if not (ns in self.imports) and self.is_valid_import(ns):
|
|
self.imports[ns] = set()
|
|
|
|
class_key = '{%s}%s' % (ns, tn)
|
|
logger.debug(' adding class %r for %r', repr(cls), class_key)
|
|
|
|
assert class_key not in self.classes, ("Somehow, you're trying to "
|
|
"overwrite %r by %r for class key %r." %
|
|
(self.classes[class_key], cls, class_key))
|
|
|
|
assert not (cls.get_type_name() is cls.Empty), cls
|
|
|
|
self.deps[cls] # despite the appearances, this is not totally useless.
|
|
self.classes[class_key] = cls
|
|
if ns == self.get_tns():
|
|
self.classes[tn] = cls
|
|
|
|
# add parent class
|
|
extends = getattr(cls, '__extends__', None)
|
|
while extends is not None and \
|
|
(extends.get_type_name() is ModelBase.Empty):
|
|
extends = getattr(extends, '__extends__', None)
|
|
|
|
if add_parent and extends is not None:
|
|
assert issubclass(extends, ModelBase)
|
|
self.deps[cls].add(extends)
|
|
self.add_class(extends)
|
|
parent_ns = extends.get_namespace()
|
|
if parent_ns != ns and not parent_ns in self.imports[ns] and \
|
|
self.is_valid_import(parent_ns):
|
|
self.imports[ns].add(parent_ns)
|
|
logger.debug(" importing %r to %r because %r extends %r",
|
|
parent_ns, ns, cls.get_type_name(),
|
|
extends.get_type_name())
|
|
|
|
# add fields
|
|
if issubclass(cls, ComplexModelBase):
|
|
for k, v in cls._type_info.items():
|
|
if v is None:
|
|
continue
|
|
|
|
self.deps[cls].add(v)
|
|
|
|
logger.debug(" adding %s.%s = %r", cls.get_type_name(), k, v)
|
|
if v.get_namespace() is None:
|
|
v.resolve_namespace(v, ns)
|
|
|
|
self.add_class(v)
|
|
|
|
if v.get_namespace() is None and cls.get_namespace() is not None:
|
|
v.resolve_namespace(v, cls.get_namespace())
|
|
|
|
child_ns = v.get_namespace()
|
|
if child_ns != ns and not child_ns in self.imports[ns] and \
|
|
self.is_valid_import(child_ns):
|
|
self.imports[ns].add(child_ns)
|
|
logger.debug(" importing %r to %r for %s.%s(%r)",
|
|
child_ns, ns, cls.get_type_name(), k, v)
|
|
|
|
if issubclass(v, XmlModifier):
|
|
self.add_class(v.type)
|
|
|
|
child_ns = v.type.get_namespace()
|
|
if child_ns != ns and not child_ns in self.imports[ns] and \
|
|
self.is_valid_import(child_ns):
|
|
self.imports[ns].add(child_ns)
|
|
logger.debug(" importing %r to %r for %s.%s(%r)",
|
|
child_ns, ns, v.get_type_name(), k, v.type)
|
|
|
|
if cls.Attributes.methods is not None:
|
|
logger.debug(" populating member methods for '%s.%s'...",
|
|
cls.get_namespace(), cls.get_type_name())
|
|
|
|
for method_key, descriptor in cls.Attributes.methods.items():
|
|
assert hasattr(cls, method_key)
|
|
|
|
should_we = True
|
|
if descriptor.static_when is not None:
|
|
should_we = descriptor.static_when(self.app)
|
|
logger.debug("static_when returned %r for %s "
|
|
"while populating classes",
|
|
should_we, descriptor.internal_key)
|
|
|
|
if should_we:
|
|
self.member_methods.append((cls, descriptor))
|
|
for c in self.add_method(descriptor):
|
|
self.add_class(c)
|
|
|
|
if cls.Attributes._subclasses is not None:
|
|
logger.debug(" adding subclasses of '%s.%s'...",
|
|
cls.get_namespace(), cls.get_type_name())
|
|
|
|
for c in cls.Attributes._subclasses:
|
|
c.resolve_namespace(c, ns)
|
|
|
|
child_ns = c.get_namespace()
|
|
if child_ns == ns:
|
|
if not self.has_class(c):
|
|
self.add_class(c, add_parent=False)
|
|
self.deps[c].add(cls)
|
|
else:
|
|
logger.debug(" not adding %r to %r because it would "
|
|
"cause circular imports because %r extends %r and "
|
|
"they don't have the same namespace", child_ns,
|
|
ns, c.get_type_name(), cls.get_type_name())
|
|
|
|
def is_valid_import(self, ns):
|
|
"""This will return False for base namespaces unless told otherwise."""
|
|
|
|
if ns is None:
|
|
raise ValueError(ns)
|
|
|
|
return self.import_base_namespaces or not (ns in namespace.PREFMAP)
|
|
|
|
|
|
class AllYourInterfaceDocuments(object): # AreBelongToUs
|
|
def __init__(self, interface, wsdl11=None):
|
|
self.wsdl11 = wsdl11
|
|
if self.wsdl11 is None and spyne.interface.HAS_WSDL:
|
|
from spyne.interface.wsdl import Wsdl11
|
|
self.wsdl11 = Wsdl11(interface)
|
|
|
|
|
|
class InterfaceDocumentBase(object):
|
|
"""Base class for all interface document implementations.
|
|
|
|
:param interface: A :class:`spyne.interface.InterfaceBase` instance.
|
|
"""
|
|
|
|
def __init__(self, interface):
|
|
self.interface = interface
|
|
self.event_manager = EventManager(self)
|
|
|
|
def build_interface_document(self):
|
|
"""This function is supposed to be called just once, as late as possible
|
|
into the process start. It builds the interface document and caches it
|
|
somewhere. The overriding function should never call the overridden
|
|
function as this may result in the same event firing more than once.
|
|
"""
|
|
|
|
raise NotImplementedError('Extend and override.')
|
|
|
|
def get_interface_document(self):
|
|
"""This function is called by server transports that try to satisfy the
|
|
request for the interface document. This should just return a previously
|
|
cached interface document.
|
|
"""
|
|
|
|
raise NotImplementedError('Extend and override.')
|