#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Directives to be used in ZCML.
These directives are all in the ``http://nextthought.com/ntp/ext`` namespace,
and are loaded using the ``meta.zcml`` file.
Example (non-sensical) of all the directives:
.. code-block:: xml
<configure xmlns:ext="http://nextthought.com/ntp/ext">
<include package="nti.externalization" file="meta.zcml" />
<ext:registerMimeFactories module="the.module" />
<ext:registerAutoPackageIO
root_interfaces="other_module.interfaces.IExtRoot"
modules="other_module.factories"
iobase="other_module.externalization.IOBase"
register_legacy_search_module="yes" />
<ext:classObjectFactory
factory="third_module.Factory"
name="AName" />
<ext:anonymousObjectFactory
factory="module3.Factory"
for="interfaces.ISchema"
field="field" />
<ext:anonymousObjectFactoryInPlace
for="interfaces.ISchema"
field="field2" />
</configure>
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from ZODB import loglevels
from zope import interface
from zope.component import zcml as component_zcml
from zope.configuration.fields import Bool
from zope.configuration.fields import GlobalInterface
from zope.configuration.fields import GlobalObject
from zope.configuration.fields import Tokens
from zope.configuration.fields import MessageID
from zope.configuration.fields import PythonIdentifier
from .interfaces import _ILegacySearchModuleFactory
from .autopackage import AutoPackageSearchingScopedInterfaceObjectIO
from .factory import MimeObjectFactory
from .factory import ClassObjectFactory
from .factory import AnonymousObjectFactory
from .interfaces import IMimeObjectFactory
from .interfaces import IClassObjectFactory
from .interfaces import IAnonymousObjectFactory
from .internalization.legacy_factories import find_factories_in_module
__docformat__ = "restructuredtext en"
logger = __import__('logging').getLogger(__name__)
# pylint: disable=protected-access,inherit-non-class
__all__ = [
'IRegisterInternalizationMimeFactoriesDirective',
'IAutoPackageExternalizationDirective',
'IClassObjectFactoryDirective',
'IBaseAnonymousObjectFactoryDirective',
'IAnonymousObjectFactoryDirective',
'IAnonymousObjectFactoryInPlaceDirective',
]
[docs]class IRegisterInternalizationMimeFactoriesDirective(interface.Interface):
"""
Defines the ``ext:registerMimeFactories`` directive.
Poke through the classes defined in *module*. If a class
defines the ``mimeType`` attribute and can be created externally,
(because it defines ``__external_can_create__`` to be true), registers
a factory utility under the ``mimeType`` name. (For backwards compatibility,
``mime_type`` is accepted if there is no ``mimeType``.)
Factories are discovered using `.find_factories_in_module`.
See :func:`nti.externalization.internalization.find_factory_for`
for how factories are used.
"""
module = GlobalObject(
title=u"Module to scan for Mime factories to add",
required=True,
)
def registerMimeFactories(_context, module):
for object_name, value in find_factories_in_module(module, case_sensitive=True):
__traceback_info__ = object_name, value
try:
mime_type = value.mimeType
except AttributeError:
try:
mime_type = value.mime_type
except AttributeError:
continue
if mime_type:
logger.log(loglevels.TRACE,
"Registered mime factory utility %s = %s (%s)",
object_name, value, mime_type)
factory = MimeObjectFactory(value,
title=object_name,
interfaces=list(interface.implementedBy(value)))
component_zcml.utility(_context,
provides=IMimeObjectFactory,
component=factory,
name=mime_type)
[docs]class IAutoPackageExternalizationDirective(interface.Interface):
"""
Defines the ``ext:registerAutoPackageIO`` directive.
This directive combines the effects of
`IRegisterInternalizationMimeFactoriesDirective` with that
of :mod:`.autopackage`, removing all need to repeat root
interfaces and module names.
After this directive is complete, a new class that descends from
:class:`~.AutoPackageSearchingScopedInterfaceObjectIO` will be
registered as the :class:`~nti.externalization.interfaces.IInternalObjectIO` adapter for all of
the *root_interface* objects, and the *modules* (or
*factory_modules*) will be searched for object factories via
`IRegisterInternalizationMimeFactoriesDirective`.
.. versionchanged:: 1.0
Add the *register_legacy_search_module* keyword argument, defaulting to
False. Previously legacy search modules would always be registered, but
now you must explicitly ask for it.
"""
root_interfaces = Tokens(
title=u"The root interfaces defined by the package.",
value_type=GlobalInterface(),
required=True)
modules = Tokens(
title=u"Module names that contain the implementations of the root_interfaces.",
value_type=GlobalObject(),
required=True)
factory_modules = Tokens(
title=u"If given, module names that should be searched for internalization factories.",
description=(u"If not given, all *modules* will be examined. If given, "
u"**only** these modules will be searched."),
value_type=GlobalObject(),
required=False)
iobase = GlobalObject(
title=(u"If given, a base class that will be used. "
u"You can customize aspects of externalization that way."),
description=(u"This class should descend from `object`, and it should implement "
u"the extension methods documented to customize "
u"`.AutoPackageSearchingScopedInterfaceObjectIO`."),
required=False)
register_legacy_search_module = Bool(
title=(u"Register found factories by their class name."),
description=(u"If true (*not* the default), then, in addition to registering "
u"factories by their mime type, also register them all by their class name. "
u"This is not recommended; currently no conflicts are caught and the order "
u"is ill-defined. "
u"See https://github.com/NextThought/nti.externalization/issues/33"),
default=False,
required=False,
)
def autoPackageExternalization(_context, root_interfaces, modules,
factory_modules=None, iobase=None,
register_legacy_search_module=False):
# TODO: Simplify this method; refactor inta parts
# pylint:disable=too-many-locals
ext_module_name = root_interfaces[0].__module__
package_name = ext_module_name.rsplit('.', 1)[0]
root_interfaces = frozenset(root_interfaces)
@classmethod
def _ap_enumerate_externalizable_root_interfaces(_cls, unused_ifaces):
return root_interfaces
module_names = frozenset([m.__name__.split('.')[-1] for m in modules])
@classmethod
def _ap_enumerate_module_names(_cls):
return module_names
@classmethod
def _ap_find_package_name(_cls):
return package_name
# Items in a class dict and its name need to be native strings
# under both py2 and py3
cls_dict = {
'_ap_find_package_name': _ap_find_package_name,
'_ap_enumerate_module_names': _ap_enumerate_module_names,
'_ap_enumerate_externalizable_root_interfaces': _ap_enumerate_externalizable_root_interfaces
}
bases = (AutoPackageSearchingScopedInterfaceObjectIO,)
if iobase:
bases = (iobase,) + bases
cls_iio = type('AutoPackageSearchingScopedInterfaceObjectIO',
bases,
cls_dict)
# If we don't set the __module__, it defaults to this module,
# which would be very confusing.
cls_iio.__module__ = _context.package.__name__ if _context.package else '__dynamic__'
for iface in root_interfaces:
logger.log(loglevels.TRACE,
"Registering ObjectIO for %s as %s", iface, cls_iio)
component_zcml.adapter(_context, factory=(cls_iio,), for_=(iface,))
# Now init the class so that it can add the things that internalization
# needs.
# Unfortunately, we are doing this eagerly instead of when the
# configuration executes its actions runs because it must be done
# before ``registerMimeFactories`` is invoked in order to add the
# mimeType fields if they are missing. If we deferred it, we would
# have to defer registerMimeFactories---and one action cannot
# invoke another action or add more actions to the list and still
# have any conflicts detected. Using the `order` parameter doesn't help us
# much with that, either.
# The plus side is that now that we are using component_zcml.utility()
# to register legacy class factories too, there's not much harm in
# initing the class early.
legacy_factories = cls_iio.__class_init__()
# Now that it's initted, register the factories
for module in (factory_modules or modules):
logger.log(loglevels.TRACE,
"Examining module %s for mime factories", module)
registerMimeFactories(_context, module)
if register_legacy_search_module:
for name, factory in find_factories_in_module(legacy_factories):
component_zcml.utility(_context,
provides=_ILegacySearchModuleFactory,
component=factory,
name=name)
[docs]class IClassObjectFactoryDirective(interface.Interface):
"""
Defines the ``ext:classObjectFactory`` directive.
This directive registers a single
:class:`nti.externalization.interfaces.IClassObjectFactory`.
The factory will be registered for a class object.
"""
factory = GlobalObject(
title=u'The class object that will be created.',
description=u"This must define the ``__external_can_create__`` attribute to be true.",
required=True
)
name = PythonIdentifier(
title=u"The name for the factory.",
description=(u"If not given, the ``__external_class_name__`` of the class will be used. "
u"If that's not available, the ``__name__`` will be used."),
required=False,
)
trusted = Bool(
title=u"Ignore any value for ``__external_can_create__`` on the factory.",
required=False,
default=False,
)
title = MessageID(
title=u"Title",
description=u"Provides a title for the object.",
required=False,
)
description = MessageID(
title=u"Description",
description=u"Provides a description for the object.",
required=False
)
def _validate_factory(factory, trusted):
if not callable(factory):
raise TypeError("Object %r must be callable" % factory)
if not getattr(factory, '__external_can_create__', False) and not trusted:
raise TypeError("Object %r must set __external_can_create__ to true" % factory)
def classObjectFactoryDirective(_context, factory, name='',
trusted=False,
title='', description=''):
_validate_factory(factory, trusted)
name = name or getattr(factory, '__external_class_name__', factory.__name__)
factory = ClassObjectFactory(factory, title, description)
component_zcml.utility(_context,
provides=IClassObjectFactory,
component=factory,
name=name)
[docs]class IBaseAnonymousObjectFactoryDirective(interface.Interface):
"""
The common parts of anonymous object factory directives.
.. versionadded:: 1.0a3
"""
for_ = GlobalInterface(
title=u"The interface that is the *parent* object this will be used for",
required=True,
)
field = PythonIdentifier(
title=u'The name of the schema field',
description=u'The factory results will be assigned to this field.',
required=True,
)
[docs]class IAnonymousObjectFactoryDirective(IBaseAnonymousObjectFactoryDirective):
"""
Defines the ``ext:anonymousObjectFactory`` directive.
This directive registers a single
:class:`nti.externaliaztion.interfaces.IAnonymousObjectFactory`
for a single field used within a single object.
.. versionadded:: 1.0a3
"""
factory = GlobalObject(
title=u'The class object that will be created.',
required=True,
)
pass_external_object_to_factory = Bool(
title=(u"Pass the external object to the factory."),
description=(u"If true (*not* the default), then, the factory will recieve "
u"one argument, the anonymous external data. "
u'Otherwise, it gets no arguments.'),
default=False,
required=False,
)
trusted = Bool(
title=u"Ignore any value for ``__external_can_create__`` on the factory.",
required=False,
default=False,
)
title = MessageID(
title=u"Title",
description=u"Provides a title for the object.",
required=False,
)
description = MessageID(
title=u"Description",
description=u"Provides a description for the object.",
required=False
)
def anonymousObjectFactoryDirective(_context, factory, for_, field,
pass_external_object_to_factory=False,
trusted=False,
title=u'', description=u''):
_validate_factory(factory, trusted)
field_name = str(field)
field = for_[field]
if field.interface is not for_:
raise TypeError("Field %r is not directly part of the interface %r (it is %r)"
% (field_name, for_, field.interface))
factory = AnonymousObjectFactory(factory, title, description)
factory.__external_factory_wants_arg__ = pass_external_object_to_factory
name = '%s.%s:%s' % (for_.__module__, for_.__name__, field_name)
assert isinstance(name, str)
component_zcml.utility(_context,
provides=IAnonymousObjectFactory,
component=factory,
name=name)
_context.action(
discriminator=('anonymousObjectFactory', for_, field_name),
callable=field.setTaggedValue,
args=('__external_factory__', name)
)
[docs]class IAnonymousObjectFactoryInPlaceDirective(IBaseAnonymousObjectFactoryDirective):
"""
Defines the ``anonymousObjectFactoryInPlace`` directive.
This directive causes the external object itself to be
used and updated in place. This is helpful when the
object itself is not modelled, but its values are.
Such data might look like this::
{'home': {'MimeType': 'Address', ...},
'work': {'MimeType': "Address', ...}}
And it might have a schema field that looks like this:
.. code-block:: python
class IAddress(Interface):
pass
class IHasAddresses(Interface):
addresses = Dict(
title=u"A mapping of address objects.",
key_type=TextLine(title=u"Adresss key"),
value_type=Object(IAddress))
The ZCML would then look like this:
.. code-block:: xml
<ext:anonymoustObjectFactoryInPlace
for="IHasAddresses"
field="addresses" />
.. versionadded:: 1.0a3
"""
def _in_place(data):
return data
def anonymousObjectFactoryInPlaceDirective(_context, for_, field):
anonymousObjectFactoryDirective(
_context, _in_place, for_, field,
pass_external_object_to_factory=True,
trusted=True,
)