#!/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