Source code for nti.externalization.autopackage

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Support for handling the IO for all the objects in a *package*,
typically via a ZCML directive.
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from ZODB.loglevels import TRACE
from zope import interface

from zope.dottedname import resolve as dottedname
from zope.mimetype.interfaces import IContentTypeAware

from nti.schema.interfaces import find_most_derived_interface
from nti.externalization.datastructures import ModuleScopedInterfaceObjectIO


logger = __import__('logging').getLogger(__name__)

# If we extend ExtensionClass.Base, __class_init__ is called automatically
# for each subclass. But we also start participating in acquisition, which
# is probably not what we want
# import ExtensionClass


class _ClassNameRegistry(object):
    __name__ = ''

[docs]class AutoPackageSearchingScopedInterfaceObjectIO(ModuleScopedInterfaceObjectIO): """ A special, magic, type of interface-driven input and output, one designed for the common use case of a *package* that provides the common pattern: * interfaces.py * externalization.py (where a subclass of this object lives) * configure.zcml (where the subclass is registered as an adapter for each object; you may also then provide mime-factories as well) * other modules, where types are defined for the external interfaces. Once you derive from this class and implement the abstract methods, you need to call :meth:`__class_init__` (exactly once) on your subclass. You do not have to derive from this class; the common case is handled via the ZCML directive ``<ext:registerAutoPackageIO>`` (:class:`nti.externalization.zcml.IAutoPackageExternalizationDirective`). You can still customize the behaviour by providing the ``iobase`` argument. """ @staticmethod def _ap_iface_queryTaggedValue(iface, name): # zope.interface 4.7.0 caused tagged values to become # inherited. If we happened to have two items that implement a # derived interface in a module, then we could get duplicate # registartions (issue #97). So we make sure to only look # at exactly the class we're interested in to make sure we # return the same set of registrations as we did under 4.6.0 # and before. # # _ap_find_potential_factories_in_module() solves a related problem # when class aliases were being used. return iface.queryDirectTaggedValue(name)
[docs] @classmethod def _ap_compute_external_class_name_from_interface_and_instance(cls, unused_iface, impl): """ Assigned as the tagged value ``__external_class_name__`` to each interface. This will be called on an instance implementing iface. .. seealso:: :class:`~.InterfaceObjectIO` """ # Use the __class__, not type(), to work with proxies return cls._ap_compute_external_class_name_from_concrete_class(impl.__class__)
[docs] @classmethod def _ap_compute_external_class_name_from_concrete_class(cls, a_type): """ Return the string value of the external class name. By default this will be either the value of ``__external_class_name__`` or, if not found, the value of ``__name__``. Subclasses may override. """ return getattr(a_type, '__external_class_name__', a_type.__name__)
[docs] @classmethod def _ap_compute_external_mimetype(cls, package_name, unused_a_type, ext_class_name): """ Return the string value of the external mime type for the given type in the given package having the given external name (probably derived from :meth:`_ap_compute_external_class_name_from_concrete_class`). For example, given the arguments ('nti.assessment', FooBar, 'FooBar'), the result will be 'application/vnd.nextthought.assessment.foobar'. Subclasses may override. """ # 'nti.assessment', FooBar, 'FooBar' => vnd.nextthought.assessment.foobar # Recall that mimetypes should be byte strings local = package_name.rsplit('.', 1)[-1] return str('application/vnd.nextthought.' + local + '.' + ext_class_name.lower())
[docs] @classmethod # TODO: We can probably do something with this def _ap_enumerate_externalizable_root_interfaces(cls, interfaces): """ Return an iterable of the root interfaces in this package that should be externalized. Subclasses must implement. """ raise NotImplementedError()
[docs] @classmethod def _ap_enumerate_module_names(cls): """ Return an iterable of module names in this package that should be searched to find factories. Subclasses must implement. """ raise NotImplementedError()
[docs] @classmethod def _ap_find_potential_factories_in_module(cls, module): """ Given a module that we're supposed to examine, iterate over the types that could be factories. This includes only types defined in that module. Any given type will only be returned once. """ seen = set() for v in vars(module).values(): # ignore imports and non-concrete classes # NOTE: using issubclass to properly support metaclasses if getattr(v, '__module__', None) != module.__name__ \ or not issubclass(type(v), type) \ or v in seen: continue seen.add(v) yield v
[docs] @classmethod def _ap_find_factories(cls, package_name): """ Return a namespace object whose attribute names are external class names and whose attribute values are classes that can be created externally. For each module returned by :meth:`_ap_enumerate_module_names`, we will resolve it against the value of *package_name* (normally that given by :meth:`_ap_find_package_name`). The module is then searched for classes that live in that module. If a class implements an interface that has a tagged value of ``__external_class_name__``, it is added to the return value. The external class name (the name of the attribute) is computed by :meth:`_ap_compute_external_class_name_from_concrete_class`. Each class that is found has an appropriate ``mimeType`` added to it (derived by :meth:`_ap_compute_external_mimetype`), if it does not already have one; these classes also have the attribute ``__external_can_create__`` set to true on them if they do not have a value for it at all. This makes the classes ready to be used with :func:`~nti.externalization.zcml.registerMimeFactories`, which is done automatically by the ZCML directive :class:`~nti.externalization.zcml.IAutoPackageExternalizationDirective`. Each class that is found is also marked as implementing :class:`zope.mimetype.interfaces.IContentTypeAware`. """ registry = _ClassNameRegistry() registry.__name__ = package_name for mod_name in cls._ap_enumerate_module_names(): mod = dottedname.resolve(package_name + '.' + mod_name) for potential_factory in cls._ap_find_potential_factories_in_module(mod): cls._ap_handle_one_potential_factory_class(registry, package_name, potential_factory) return registry
@classmethod def _ap_handle_one_potential_factory_class(cls, namespace, package_name, implementation_class): # Private helper function # Does this implement something that should be externalizable? # Recall that __external_class_name__ was set on the root interfaces # identified by ``_ap_enumerate_externalizable_root_interfaces()`` in ``__class_init__`` interfaces_implemented = list(interface.implementedBy(implementation_class)) check_ext = any(cls._ap_iface_queryTaggedValue(iface, '__external_class_name__') for iface in interfaces_implemented) if not check_ext: return most_derived = find_most_derived_interface(None, interface.Interface, interfaces_implemented) if (most_derived is not interface.Interface and not cls._ap_iface_queryTaggedValue(most_derived, '__external_default_implementation__')): logger.log(TRACE, "Autopackage setting %s as __external_default_implementation__ for %s " "which is the most derived interface out of %r.", implementation_class, most_derived, interfaces_implemented) most_derived.setTaggedValue('__external_default_implementation__', implementation_class) ext_class_name = cls._ap_compute_external_class_name_from_concrete_class( implementation_class) # XXX: Checking for duplicates setattr(namespace, ext_class_name, implementation_class) if not implementation_class.__dict__.get('mimeType', None): # NOT hasattr. We don't use hasattr because inheritance would # throw us off. It could be something we added, and iteration order # is not defined (if we got the subclass first we're good, we fail if we # got superclass first). # Also, not a simple 'in' check. We want to allow for setting mimeType = None # in the dict for static analysis purposes # legacy check if 'mime_type' in implementation_class.__dict__: implementation_class.mimeType = implementation_class.mime_type else: implementation_class.mimeType = cls._ap_compute_external_mimetype( package_name, implementation_class, ext_class_name) implementation_class.mime_type = implementation_class.mimeType if not IContentTypeAware.implementedBy(implementation_class): # pylint:disable=no-value-for-parameter # well it does now interface.classImplements(implementation_class, IContentTypeAware) # Opt in for creating, unless explicitly disallowed if not hasattr(implementation_class, '__external_can_create__'): implementation_class.__external_can_create__ = True # Let them have containers if not hasattr(implementation_class, 'containerId'): implementation_class.containerId = None
[docs] @classmethod def _ap_find_package_name(cls): """ Return the package name to search for modules. By default we look at the module name of the *cls* object given. Subclasses may override. """ ext_module_name = cls.__module__ package_name = ext_module_name.rsplit('.', 1)[0] return package_name
[docs] @classmethod def _ap_find_package_interface_module(cls): """ Return the module that should be searched for interfaces. By default, this will be the ``interfaces`` sub-module of the package returned from :meth:`_ap_find_package_name`. Subclasses may override. """ # First, get the correct working module package_name = cls._ap_find_package_name() # Now the interfaces return dottedname.resolve(package_name + '.interfaces')
[docs] @classmethod def __class_init__(cls): # ExtensionClass.Base class initializer """ Class initializer. Should be called exactly *once* on each distinct subclass. First, makes all interfaces returned by :func:`_ap_enumerate_externalizable_root_interfaces` externalizable by setting the ``__external_class_name__`` tagged value (to :func:`_ap_compute_external_class_name_from_interface_and_instance`). (See :class:`~.InterfaceObjectIO`.) Then, find all of the object factories and initialize them using :func:`_ap_find_factories`. A namespace object representing these factories is returned. .. versionchanged:: 1.0 Registering the factories using :func:`~.register_legacy_search_module` is no longer done by default. If you are using this class outside of ZCML, you will need to subclass and override this method to make that call yourself. If you are using ZCML, you will need to set the appropriate attribute to True. """ # Do nothing when this class itself is initted if cls.__name__ == 'AutoPackageSearchingScopedInterfaceObjectIO' \ and cls.__module__ == __name__: return False # First, get the correct working module package_name = cls._ap_find_package_name() # Now the interfaces package_ifaces = cls._ap_find_package_interface_module() cls._ext_search_module = package_ifaces logger.log(TRACE, "Autopackage tagging interfaces in %s", package_ifaces) # Now tag them for iface in cls._ap_enumerate_externalizable_root_interfaces(package_ifaces): iface.setTaggedValue('__external_class_name__', cls._ap_compute_external_class_name_from_interface_and_instance) # Now find the factories factories = cls._ap_find_factories(package_name) return factories