Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of proposal #18 #19

Merged
merged 3 commits into from
Feb 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ Breaking changes:

New features:

- *add item here*
- New option ``former_dotted_names`` that allows to register the former name under
which a behavior used to be registerd. This can be useful to ensure a smooth
transition in case a behavior's dotted name is changed.
Refs: #18
[pysailor]

Bug fixes:

Expand Down
5 changes: 5 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ The directive supports the attributes:

Must be one element (no multiadapters, applies also for auto-detection).

``former_dotted_names``
In case a behavior is modified so that its dotted name changes, this field can be used to register the old name(s). Therefore, it is possible to retrieve the name(s) under which a behavior was formerly registered under.

If a call to ``lookup_behavior_registration`` does not find a behavior under the given name, it will look at the former dotted names to try and find the behavior.


ZCML Examples
-------------
Expand Down
37 changes: 37 additions & 0 deletions plone/behavior/directives.rst
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ plone.behavior.tests:
... provides=".tests.INameOnlyBehavior"
... />
...
... <plone:behavior
... name="renamed_adapter_behavior"
... title="Renamed Adapter behavior"
... description="A basic adapter behavior that used to have a different name"
... provides=".tests.IRenamedAdapterBehavior"
... factory=".tests.RenamedAdapterBehavior"
... former_dotted_names="plone.behavior.tests.IOriginalAdapterBehavior"
... />
...
... </configure>
... """

Expand Down Expand Up @@ -340,6 +349,20 @@ declaration on the factory.
>>> dummy.name
u'name_only'

8) A behavior that used to be known under a different dotted name

A behavior that has been renamed, can of course be found under the new name.
The representation tells us the former dotted name.
>>> dummy = getUtility(IBehavior, name=u"plone.behavior.tests.IRenamedAdapterBehavior")
>>> dummy # doctest: +ELLIPSIS
<BehaviorRegistration renamed_adapter_behavior at ...
schema: plone.behavior.tests.IRenamedAdapterBehavior
marker: (no marker is set)
factory: <class 'plone.behavior.tests.RenamedAdapterBehavior'>
title: Renamed Adapter behavior
A basic adapter behavior that used to have a different name
former dotted names: plone.behavior.tests.IOriginalAdapterBehavior
>

Test registration lookup helper utility.

Expand Down Expand Up @@ -373,3 +396,17 @@ Test registration lookup helper utility.
title: Adapter behavior
A basic adapter behavior
>

A lookup via getUtility for a former behavior name fails.
>>> failed = False
>>> try:
... dummy = getUtility(IBehavior, name=u"plone.behavior.tests.IOriginalAdapterBehavior")
... except ComponentLookupError:
... failed = True
>>> failed
True

But the lookup helper still finds it under the former name.
>>> dummy = lookup_behavior_registration("plone.behavior.tests.IOriginalAdapterBehavior")
>>> dummy.name
u'renamed_adapter_behavior'
14 changes: 7 additions & 7 deletions plone/behavior/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,26 @@ class IBehavior(Interface):

title = schema.TextLine(
title=u'Short title of the behavior',
required=True
required=True,
)

description = schema.Text(
title=u'Longer description of the behavior',
required=False
required=False,
)

name = schema.TextLine(
title=u'Readable short name to be used for behavior lookup',
description=u'E.g. plone.somebehavior. If not given the full dotted '
u'name of the interfaces is used for lookup instead.'
u'Recommended, but due to BBB not required.',
required=False
required=False,
)

interface = schema.Object(
title=u'Interface describing this behavior',
required=True,
schema=IInterface
schema=IInterface,
)

marker = schema.Object(
Expand All @@ -59,13 +59,13 @@ class IBehavior(Interface):
u'is an adapter adapting the the marker and providing the '
u'"interface" of this behavior.',
required=False,
schema=IInterface
schema=IInterface,
)

factory = schema.Object(
title=u'An adapter factory for the behavior',
required=True,
schema=Interface
schema=Interface,
)


Expand All @@ -86,7 +86,7 @@ class IBehaviorAdapterFactory(Interface):

behavior = schema.Object(
title=u'The behavior this is a factory for',
schema=IBehavior
schema=IBehavior,
)

def __call__(context):
Expand Down
55 changes: 35 additions & 20 deletions plone/behavior/metaconfigure.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,48 +25,65 @@ class IBehaviorDirective(Interface):
name = TextLine(
title=u'Name',
description=u'Convenience lookup name for this behavior',
required=False)
required=False,
)

title = configuration_fields.MessageID(
title=u'Title',
description=u'A user friendly title for this behavior',
required=True)
required=True,
)

description = configuration_fields.MessageID(
title=u'Description',
description=u'A longer description for this behavior',
required=False)
required=False,
)

provides = configuration_fields.GlobalInterface(
title=u'An interface to which the behavior can be adapted',
description=u'This is what the conditional adapter factory will '
u'be registered as providing',
required=True)
required=True,
)

marker = configuration_fields.GlobalInterface(
title=u'A marker interface to be applied by the behavior',
description=u'If factory is not given, then this is optional',
required=False)
required=False,
)

factory = configuration_fields.GlobalObject(
title=u'The factory for this behavior',
description=u'If this is not given, the behavior is assumed to '
u'provide a marker interface',
required=False)
required=False,
)

for_ = configuration_fields.GlobalObject(
title=u'The type of object to register the conditional adapter '
u'factory for',
description=u'This is optional - the default is to register the '
u'factory for zope.interface.Interface',
required=False)
required=False,
)

name_only = configuration_fields.Bool(
title=u'Do not register the behavior under the dotted path, but '
u'only under the given name',
description=u'Use this option to register a behavior for the same '
u'provides under a different name.',
required=False)
required=False,
)

former_dotted_names = TextLine(
title=u'Space-separated list of dotted names that this behavior was '
u'formerly registered under',
description=u'Use this field in case you change the dotted name, '
u'so that the current behavior can be looked up under '
u'its former name.',
required=False,
)


def _detect_for(factory, marker):
Expand All @@ -80,8 +97,7 @@ def _detect_for(factory, marker):
return adapts[0]
if len(adapts) > 1:
raise ConfigurationError(
u'The factory can not be declared as multi-adapter.'
)
u'The factory can not be declared as multi-adapter.')
# down here it means len(adapts) < 1
if marker is not None:
# given we have a marker it is safe to register for the
Expand All @@ -92,7 +108,8 @@ def _detect_for(factory, marker):


def behaviorDirective(_context, title, provides, name=None, description=None,
marker=None, factory=None, for_=None, name_only=False):
marker=None, factory=None, for_=None, name_only=False,
former_dotted_names=''):

if marker is None and factory is None:
# a schema only behavior means usually direct attribute settings on the
Expand All @@ -103,13 +120,11 @@ def behaviorDirective(_context, title, provides, name=None, description=None,
if marker is not None and factory is None and marker is not provides:
raise ConfigurationError(
u'You cannot specify a different \'marker\' and \'provides\' if '
u'there is no adapter factory for the provided interface.'
)
u'there is no adapter factory for the provided interface.')
if name_only and name is None:
raise ConfigurationError(
u'If you decide to only register by \'name\', a name must '
u'be given.'
)
u'be given.')

# Instantiate the real factory if it's the schema-aware type. We do
# this here so that the for_ interface may take this into account.
Expand All @@ -124,6 +139,7 @@ def behaviorDirective(_context, title, provides, name=None, description=None,
marker=marker,
factory=factory,
name=name,
former_dotted_names=former_dotted_names,
)
# the behavior registration can be looked up as a named utility.
# the name of the utility is either the full dotted path of the interface
Expand All @@ -134,7 +150,7 @@ def behaviorDirective(_context, title, provides, name=None, description=None,
_context,
provides=IBehavior,
name=provides.__identifier__,
component=registration
component=registration,
)

if name is not None:
Expand All @@ -146,15 +162,14 @@ def behaviorDirective(_context, title, provides, name=None, description=None,
_context,
provides=IBehavior,
name=name,
component=registration
component=registration,
)

if factory is None:
if for_ is not None:
logger.warn(
u'Specifying \'for\' in behavior \'{0}\' if no \'factory\' is '
u'given has no effect and is superfluous.'.format(title)
)
u'given has no effect and is superfluous.'.format(title))
# w/o factory we're done here: schema only behavior
return

Expand All @@ -166,5 +181,5 @@ def behaviorDirective(_context, title, provides, name=None, description=None,
_context,
factory=(adapter_factory,),
provides=provides,
for_=(for_,)
for_=(for_,),
)
32 changes: 26 additions & 6 deletions plone/behavior/registration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
from plone.behavior import logger
from plone.behavior.interfaces import IBehavior
from zope.component import ComponentLookupError
from zope.component import getUtilitiesFor
from zope.component import getUtility
from zope.interface import implementer

Expand All @@ -20,21 +22,22 @@
marker: {marker}
factory: {factory}
title: {title}
{description}
{description}{extra_info}
>"""


@implementer(IBehavior)
class BehaviorRegistration(object):

def __init__(self, title, description, interface,
marker, factory, name=None):
marker, factory, name=None, former_dotted_names=''):
self.title = title
self.description = description
self.interface = interface
self.marker = marker
self.factory = factory
self.name = name
self.former_dotted_names = former_dotted_names

def __repr__(self):
if self.marker is not None:
Expand All @@ -53,8 +56,12 @@ def __repr__(self):
'title': self.title or '(no title)',
'description': textwrap.fill(
self.description or '(no description)',
subsequent_indent=' '
)
subsequent_indent=' ',
),
'extra_info': (
self.former_dotted_names and
'\n former dotted names: {0}'.format(self.former_dotted_names)
),
}
return REGISTRATION_REPR.format(**info)

Expand All @@ -64,8 +71,11 @@ class BehaviorRegistrationNotFound(Exception):
"""


def lookup_behavior_registration(name=None, identifier=None):
"""Lookup behavior registration either by name or interface identifier.
def lookup_behavior_registration(
name=None, identifier=None, warn_about_fallback=True):
"""Look up behavior registration either by name or interface identifier.
Fall back to checking the former_dotted_names if the lookup is not
successful.

``ValueError`` is thrown if function call is incomplete.
``BehaviorRegistrationNotFound`` is thrown if lookup fails.
Expand All @@ -80,4 +90,14 @@ def lookup_behavior_registration(name=None, identifier=None):
try:
return getUtility(IBehavior, name=name)
except ComponentLookupError:
for id_, behavior in getUtilitiesFor(IBehavior):
# Before we raise an error, iterate over all behaviors and check
# if the requested name is registered as a former dotted name.
if name in behavior.former_dotted_names:
if warn_about_fallback:
logger.warn(
'The dotted name "{0}" is deprecated. It has been '
'changed to "{1}"'.format(
name, behavior.interface.__identifier__, ))
return behavior
raise BehaviorRegistrationNotFound(name)
11 changes: 11 additions & 0 deletions plone/behavior/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ def __init__(self, context):
self.context = context


# Simple adapter behavior that used to have a different name
class IRenamedAdapterBehavior(Interface):
pass


@implementer(IRenamedAdapterBehavior)
class RenamedAdapterBehavior(object):
def __init__(self, context):
self.context = context


# Adapter behavior with explicit context restriction
class IRestrictedAdapterBehavior(Interface):
pass
Expand Down