Symlinks for Zope 3.
Project description
Shortcuts
Shortcuts are objects that allow other objects (their target) to appear to be located in places other than the target’s actual location. They are somewhat like a symbolic link in Unix-like operating systems.
Creating a shortcut
Shortcuts are created by calling the Shortcut class’s constructor with a target, parent, and name:
>>> from zc.shortcut.shortcut import Shortcut >>> class MyTarget: ... attr = 'hi' ... __parent__ = 'Original Parent' ... __name__ = 'Original Name' >>> target = MyTarget() >>> sc = Shortcut(target) >>> sc.__parent__ = 'My Parent' >>> sc.__name__ = 'My Name'
A shortcut provides an attribute to access its target:
>>> sc.target <__builtin__.MyTarget instance at ...>
A shortcut’s __parent__ and __name__ are independent of their target:
>>> sc.__parent__ 'My Parent' >>> sc.target.__parent__ 'Original Parent' >>> sc.__name__ 'My Name' >>> sc.target.__name__ 'Original Name'
But the target knows the traversal parent, the traversal name, and the shortcut. This allows the shortcut to have annotations that may be accessed by views and other components that render or use the target.
>>> sc.target.__traversed_parent__ 'My Parent' >>> sc.target.__traversed_name__ 'My Name' >>> sc.target.__shortcut__ is sc True
See proxy.txt and adapters.txt for more details
Adapters
Adapters are provided to allow a shortcut to act as the target would when traversed.
ITraversable
First we have to import the interfaces we’ll be working with:
>>> from zope.publisher.interfaces import IRequest >>> from zope.publisher.interfaces.browser import IBrowserPublisher >>> from zope.traversing.interfaces import ITraversable >>> from zc.shortcut.interfaces import IShortcut >>> from zope.location.interfaces import ILocation >>> from zc.shortcut import interfaces
If we have a target object with a root:
>>> from zope import interface, component >>> class ISpam(interface.Interface): ... pass >>> class Spam: ... interface.implements(ISpam, ILocation) ... def __init__(self, parent, name): ... self.__parent__ = parent ... self.__name__ = name >>> from zope.traversing.interfaces import IContainmentRoot >>> class DummyContainmentRoot(object): ... __parent__ = __name__ = None ... interface.implements(IContainmentRoot) ... >>> root = DummyContainmentRoot() >>> real_parent = Spam(root, 'real_parent') >>> target = Spam(real_parent, 'target')
The target object provides a multiadapter for the target and request to an ITraversable so it can be traversed:
>>> class SpamTraversableAdapter: ... interface.implements(ITraversable) ... component.adapts(ISpam, IRequest) ... def __init__(self, spam, request): ... self.spam = spam >>> component.provideAdapter(SpamTraversableAdapter, name='view')
There is an adapter to return the target object adapted to ITraversable when a shortcut and request is adapted to ITraversable. For example if we create a shortcut to our target:
>>> from zc.shortcut.shortcut import Shortcut >>> shortcut = Shortcut(target) >>> shortcut_parent = Spam(root, 'shortcut_parent') >>> shortcut.__parent__ = shortcut_parent >>> shortcut.__name__ = 'shortcut'
And call the adapter with a request:
>>> from zope.publisher.browser import TestRequest >>> from zc.shortcut.adapters import ShortcutTraversalAdapterFactory >>> request = TestRequest() >>> adapter = ShortcutTraversalAdapterFactory(shortcut, request)
The result is the target’s ITraversal adapter:
>>> adapter <...SpamTraversableAdapter instance at...> >>> adapter.spam <...Spam instance at...>
Shortcut traversal
Shortcut traversal is unpleasantly tricky. First consider the case of traversing a shortcut and then traversing to get the default view (‘index.html’). In that case, the shortcut will be available to the view, and breadcrumbs and other view elements that care about how the object was traversed will merely need to look at the shortcut’s __parent__, or the target proxy’s __traversed_parent__. This is not too bad.
It becomes more interesting if one traverses through a shortcut to another content object. A naive implementation will traverse the shortcut by converting it to its target, and then traversing the target to get the contained content object. However, views for the content object will have no idea of the traversal path used to get to the content object: they will only have the __parent__ of the content object, which is the shortcut’s target without any target proxy. From there they will be able to find the target’s parent, but not the traversed shortcut’s parent. Breadcrumbs and other components that care about traversed path will be broken.
In order to solve this use case, traversing a shortcut needs to traverse the target and then wrap the resulting object in another target proxy that holds a reference to the shortcut’s target proxy as its traversed parent.
Traversing a shortcut and finding another shortcut is slightly trickier again. In this case, the shortcut’s target’s proxy should have a parent which is the shortcut’s proxy’s parent.
Two adapters are available for IPublishTraverse: one for shortcuts, and one for traversal proxies. If a traversal target doesn’t provide IPublishTraverse, then it should provide an adapter:
>>> from zc.shortcut import adapters >>> from zope.publisher.interfaces import IPublishTraverse >>> child_spam = Spam(real_parent, 'child_spam') >>> child_shortcut = Shortcut(child_spam) >>> child_shortcut.__parent__ = shortcut >>> child_shortcut.__name__ = 'child_shortcut' >>> class SpamPublishTraverseAdapter: ... interface.implements(IPublishTraverse) ... component.adapts(ISpam, IRequest) ... def __init__(self, spam, request): ... self.spam = spam ... def publishTraverse(self, request, name): ... print 'SpamPublishTraverseAdapter has been traversed.' ... return {'child_spam': child_spam, ... 'child_shortcut': child_shortcut}[name] >>> component.provideAdapter(SpamPublishTraverseAdapter)
If it does, the adapter will be used to do the traversal:
>>> adapter = adapters.ShortcutPublishTraverseAdapter(shortcut, request) >>> adapter <...ShortcutPublishTraverseAdapter object at...> >>> from zope.interface.verify import verifyObject >>> verifyObject(IPublishTraverse, adapter) True >>> res = adapter.publishTraverse(request, 'child_spam') SpamPublishTraverseAdapter has been traversed.
Notice that the traversed object has a traversal proxy (but not a target proxy).
>>> interfaces.ITraversalProxy.providedBy(res) True >>> interfaces.ITargetProxy.providedBy(res) False >>> res.__traversed_parent__ == shortcut.target True >>> res.__traversed_name__ 'child_spam' >>> res.__traversed_parent__.__shortcut__ is shortcut True >>> res.__traversed_parent__.__traversed_parent__ is shortcut_parent True
To traverse further down and still keep the traversal information, we need to register the ProxyPublishTraverseAdapter. Notice that we will also traverse to a shortcut this time, and look at the traversal trail up from the shortcut and from its target.
>>> component.provideAdapter(adapters.ProxyPublishTraverseAdapter) >>> from zope import component >>> adapter = component.getMultiAdapter((res, request), IPublishTraverse) >>> res = adapter.publishTraverse(request, 'child_shortcut') SpamPublishTraverseAdapter has been traversed. >>> res.__traversed_parent__ == child_spam True >>> res.__traversed_name__ 'child_shortcut' >>> res.__traversed_parent__.__traversed_parent__ == shortcut.target True >>> res.target.__traversed_parent__.__traversed_parent__ == shortcut.target True
If, instead, the target implements IPublishTraverse itself…:
>>> class SpamWithPublishTraverse(Spam): ... interface.implements(IPublishTraverse) ... def publishTraverse(self, request, name): ... print 'SpamWithPublishTraverse has been traversed.' ... return {'child_spam': child_spam, ... 'child_shortcut': child_shortcut}[name]
…then it’s publishTraverse() will be called directly:
>>> spam = SpamWithPublishTraverse(real_parent, 'special_spam') >>> shortcut = Shortcut(spam) >>> shortcut.__parent__ = shortcut_parent >>> shortcut.__name__ = 'special_spam_shortcut' >>> adapter = adapters.ShortcutPublishTraverseAdapter(shortcut, request) >>> adapter <...ShortcutPublishTraverseAdapter object at...> >>> another = adapter.publishTraverse(request, 'child_spam') SpamWithPublishTraverse has been traversed.
Ending traversal at a shortcut
When a shortcut is the target of a URL traversal, rather than a node along the way, the leaf-node handling of the target object must be invoked so that the shortcut behaves in the same way as the would would when accessed directly.
When a URL from a request represents an object (rather than a view), the publisher uses the browserDefault() method of the IBrowserPublisher interface to determine how the object should be handled. This method returns an object and a sequences of path elements that should be traversed.
For shortcuts, this is handled by delegating to the target of the shortcut, substituting a proxy for the target so the traversedURL view and breadcrumbs still work correctly.
Let’s start by defining an IBrowserPublisher for ISpam objects:
>>> class SpamBrowserPublisherAdapter(SpamPublishTraverseAdapter): ... interface.implements(IBrowserPublisher) ... def browserDefault(self, request): ... print "browserDefault for", repr(self.spam) ... return self.spam, ("@@foo.html",) >>> component.provideAdapter(SpamBrowserPublisherAdapter, ... provides=IBrowserPublisher) >>> adapter.browserDefault(request) # doctest: +ELLIPSIS browserDefault for <...SpamWithPublishTraverse instance at 0x...> (<...SpamWithPublishTraverse instance at 0x...>, ('@@foo.html',))
traversedURL
If shortcuts are traversed, an absolute url can lead a user to unexpected locations–to the real location of the object, rather than to the traversed location. In order to get the traversed url, the adapters module provides a traversedURL function, and the shortcut package also offers it from its __init__.py.
Given the result of the next-to-last shortcut traversal described above, for instance, traversedURL returns a URL that behaves similarly to absoluteURL except when it encounters target proxies, at which point the traversal parents are used rather than the actual parents.
>>> component.provideAdapter(adapters.TraversedURL) >>> component.provideAdapter(adapters.FallbackTraversedURL) >>> component.provideAdapter(adapters.RootTraversedURL) >>> adapters.traversedURL(res, request) 'http://127.0.0.1/shortcut_parent/shortcut/child_spam/child_shortcut'
Like absoluteURL, the returned value is html escaped.
>>> shortcut_parent.__name__ = 'shortcut parent' >>> adapters.traversedURL(res, request) 'http://127.0.0.1/shortcut%20parent/shortcut/child_spam/child_shortcut'
Also like absoluteURL, traversedURL is registered as a view so it can be used within page templates (as in context/@@traversedURL).
>>> component.provideAdapter(adapters.traversedURL, name="traversedURL") >>> component.getMultiAdapter((res, request), name='traversedURL') 'http://127.0.0.1/shortcut%20parent/shortcut/child_spam/child_shortcut'
Copy and Link
The zope.copypastemove package provides a number of interfaces to provide copy, move, rename, and other similar operations. The shortcut package provides a replacement implementation of copy for objects that looks up a repository and uses it if available; an implementation of copy that actually makes shortcuts (useful for immutable objects stored in a repository); and an interface and two implementations, one for shortcuts and one for other objects, for a new link operation, which makes a shortcut to the selected object.
Copying an Object
If you want copying an object to use repositories if they are available, this adapter provides the functionality. It is installed for all objects by default, but could also be configured only for certain interfaces.
In the example below, first we set up the dummy content objects, then we register the necessary adapters, and then we set up some event listener code that we use to show what events are being fired.
>>> class IDummy(interface.Interface): ... pass ... >>> import zope.app.container.interfaces >>> class Dummy(object): ... interface.implements( ... IDummy, zope.app.container.interfaces.IContained) >>> class DummyContainer(dict): ... interface.implements(zope.app.container.interfaces.IContainer) ... __parent__ = __name__ = None ... def __repr__(self): ... return "<%s at %d>" % (self.__class__.__name__, id(self)) ... >>> repo = DummyContainer() >>> folder = DummyContainer() >>> @component.adapter(IDummy) ... @interface.implementer(zope.app.container.interfaces.IContainer) ... def DummyRepoGetter(content): ... return repo ... >>> component.provideAdapter( ... DummyRepoGetter, name=interfaces.REPOSITORY_NAME) >>> from zope.app.container.contained import NameChooser >>> component.provideAdapter(NameChooser, adapts=(interface.Interface,)) >>> # now, before we actually actually run the adding machinery, we'll >>> # set up some machinery that will let us look at events firing ... >>> heard_events = [] # we'll collect the events here >>> from zope import event >>> event.subscribers.append(heard_events.append) >>> import pprint >>> from zope import interface >>> showEventsStart = 0 >>> def iname(ob): ... return iter(interface.providedBy(ob)).next().__name__ ... >>> def getId(ob): ... if ob is None or isinstance(ob, (int, float, basestring, tuple)): ... return "(%r)" % (ob,) ... id = getattr(ob, 'id', getattr(ob, '__name__', None)) ... if not id: ... id = "a %s (%s)" % (ob.__class__.__name__, iname(ob)) ... return id ... >>> def showEvents(start=None): # to generate a friendly view of events ... global showEventsStart ... if start is None: ... start = showEventsStart ... res = [ ... '%s fired for %s.' % (iname(ev), getId(ev.object)) ... for ev in heard_events[start:]] ... res.sort() ... pprint.pprint(res) ... showEventsStart = len(heard_events) ... >>> component.provideAdapter(adapters.ObjectCopier) >>> from zope.app.container.contained import NameChooser >>> component.provideAdapter(NameChooser, adapts=(interface.Interface,)) >>> dummy = Dummy() >>> repo['dummy'] = dummy >>> dummy.__parent__ = repo >>> dummy.__name__ = 'dummy' >>> dummy.id = 'foo' >>> from zope import copypastemove >>> copier = copypastemove.IObjectCopier(dummy) >>> verifyObject(copypastemove.IObjectCopier, copier) True >>> copier.copyTo(folder) 'dummy' >>> showEvents() ['IObjectCopiedEvent fired for foo.', 'IObjectCreatedEvent fired for a Shortcut (IShortcut).'] >>> folder['dummy'].raw_target is not dummy True >>> folder['dummy'].raw_target is repo['dummy-2'] True>>> folder['dummy'].raw_target.id 'foo' >>> folder.clear() # prepare for next test
Linking
In addition to the copy and move operations, the shortcut package offers up a new ‘link’ operation: this creates a shortcut to the selected object. In the case of linking a shortcut, the provided adapter links instead to the original shortcut’s target.
>>> from zope.app.container.constraints import contains >>> class INoDummyContainer(interface.Interface): ... contains(ISpam) # won't contain shortcuts ... >>> badcontainer = DummyContainer() >>> interface.alsoProvides(badcontainer, INoDummyContainer) >>> component.provideAdapter(adapters.ObjectLinkerAdapter) >>> component.provideAdapter(adapters.ShortcutLinkerAdapter) >>> dummy_linker = interfaces.IObjectLinker(dummy) >>> shortcut_linker = interfaces.IObjectLinker(shortcut) >>> verifyObject(interfaces.IObjectLinker, dummy_linker) True >>> verifyObject(interfaces.IObjectLinker, shortcut_linker) True >>> dummy_linker.linkable() True >>> shortcut_linker.linkable() True >>> dummy_linker.linkableTo(badcontainer) False >>> shortcut_linker.linkableTo(badcontainer) False >>> dummy_linker.linkableTo(folder) True >>> shortcut_linker.linkableTo(folder) True >>> dummy_linker.linkTo(badcontainer) Traceback (most recent call last): ... Invalid: ('Not linkableTo target with name', <DummyContainer...>, 'dummy') >>> shortcut_linker.linkTo(badcontainer) Traceback (most recent call last): ... Invalid: ('Not linkableTo target with name', <DummyContainer...>, 'special_spam_shortcut') >>> dummy_linker.linkTo(folder) 'dummy' >>> showEvents() ['IObjectCreatedEvent fired for a Shortcut (IShortcut).'] >>> folder['dummy'].raw_target is dummy True >>> shortcut_linker.linkTo(folder) 'special_spam_shortcut' >>> showEvents() ['IObjectCopiedEvent fired for a Shortcut (IShortcut).'] >>> folder['special_spam_shortcut'].raw_target is spam True >>> dummy_linker.linkTo(folder, 'dummy2') 'dummy2' >>> showEvents() ['IObjectCreatedEvent fired for a Shortcut (IShortcut).'] >>> folder['dummy2'].raw_target is dummy True >>> shortcut_linker.linkTo(folder, 'shortcut2') 'shortcut2' >>> showEvents() ['IObjectCopiedEvent fired for a Shortcut (IShortcut).'] >>> folder['shortcut2'].raw_target is spam True
Copying as Linking
For some objects–immutable objects that are primarily stored in a repository, for instance–having a copy gesture actually create a link may be desirable. The adapters module provides an ObjectCopierLinkingAdapter for these use cases. Whenever a copy is requested, a link is made instead. This adapter is not registered for any interfaces by default: it is expected to be installed selectively.
>>> class IImmutableDummy(IDummy): ... pass ... >>> immutable_dummy = Dummy() >>> interface.directlyProvides(immutable_dummy, IImmutableDummy) >>> originalcontainer = DummyContainer() >>> originalcontainer['immutable_dummy'] = immutable_dummy >>> immutable_dummy.__name__ = 'immutable_dummy' >>> immutable_dummy.__parent__ = originalcontainer >>> component.provideAdapter( ... adapters.ObjectCopierLinkingAdapter, adapts=(IImmutableDummy,)) >>> copier = copypastemove.IObjectCopier(immutable_dummy) >>> copier.copyable() True >>> copier.copyableTo(badcontainer) False >>> copier.copyableTo(folder) True >>> copier.copyTo(folder) 'immutable_dummy' >>> showEvents() ['IObjectCreatedEvent fired for a Shortcut (IShortcut).'] >>> folder['immutable_dummy'].raw_target is immutable_dummy True>>> event.subscribers.pop() is not None # cleanup True
Shortcut IAdding
The shortcut adding has a couple of different behaviors than the standard Zope 3 adding. The differences are to support traversal proxies; and to provide more flexibility for choosing the nextURL after an add.
Supporting Traversal Proxies
Both the action method and the nextURL method redirect to the absoluteURL of the container in the zope.app implementation. In the face of shortcuts and traversal proxies, this can generate surprising behavior for users, directing their URL to a location other than where they thought they were working. The shortcut adding changes both of these methods to use traversedURL instead. As a result, adding to a shortcut of a container returns the user to the shortcut, not the absolute path of the container’s real location; and submitting the form of the default view of the adding redirects to within the context of the traversed shortcut(s), not the absoluteURL.
The action method changes are pertinent to redirecting to an adding view.
>>> from zc.shortcut import adding, interfaces >>> from zope import interface, component >>> from zope.location.interfaces import ILocation >>> class ISpam(interface.Interface): ... pass ... >>> class Spam(dict): ... interface.implements(ISpam, ILocation) ... def __init__(self, parent, name): ... self.__parent__ = parent ... self.__name__ = name ... >>> from zope.traversing.interfaces import IContainmentRoot >>> class DummyContainmentRoot(object): ... interface.implements(IContainmentRoot) ... >>> root = DummyContainmentRoot() >>> real_parent = Spam(root, 'real_parent') >>> target = Spam(real_parent, 'target') >>> from zc.shortcut.shortcut import Shortcut >>> shortcut = Shortcut(target) >>> shortcut_parent = Spam(root, 'shortcut_parent') >>> shortcut.__parent__ = shortcut_parent >>> shortcut.__name__ = 'shortcut' >>> from zc.shortcut import adapters >>> component.provideAdapter(adapters.TraversedURL) >>> component.provideAdapter(adapters.FallbackTraversedURL) >>> component.provideAdapter(adapters.RootTraversedURL) >>> from zope.publisher.interfaces import IRequest >>> @component.adapter(interfaces.IAdding, IRequest) ... @interface.implementer(interface.Interface) ... def dummyAddingView(adding, request): ... return 'this is a view' ... >>> component.provideAdapter(dummyAddingView, name='foo_type') >>> from zope.publisher.browser import TestRequest >>> request = TestRequest() >>> adder = adding.Adding(shortcut.target, request) >>> adder.action('foo_type', 'foo_id') >>> request.response.getHeader('Location') 'http://127.0.0.1/shortcut_parent/shortcut/@@+/foo_type=foo_id'
The nextURL method changes are pertinent to the default behavior.
>>> adder.contentName = 'foo_id' >>> target['foo_id'] = Spam(target, 'foo_id') >>> adder.nextURL() 'http://127.0.0.1/shortcut_parent/shortcut/@@contents.html'
Adding Flexibility to ‘nextURL’
The nextURL method in the zope.app implementation of an adding defines precisely what the nextURL should be: the @@contents.html view of the context. The shortcut adding recreates this behavior, but only after seeing if different behavior has been registered.
nextURL tries to find an adapter named with the constant in zc.shortcut.interfaces.NEXT_URL_NAME, providing nothing, for the adding, the new content as found in the container (so it may be a shortcut), and the context. If an adapter is registered, it should be a string of the nextURL to be used; this value will be returned. If no adapter is registered or the registered adapter returns None, the @@contents.html view of the context is returned.
>>> @component.adapter(interfaces.IAdding, ISpam, ISpam) ... @interface.implementer(interface.Interface) ... def sillyNextURL(adding, content, container): ... return '%s class added "%s" to "%s"' % ( ... adding.__class__.__name__, ... content.__name__, ... container.__name__) ... >>> component.provideAdapter(sillyNextURL, name=interfaces.NEXT_URL_NAME) >>> adder.nextURL() 'Adding class added "foo_id" to "target"'
Shortcut factories
Shortcut factories are factories that place objects in a configured folder and then return a shortcut to the new object. Because they create objects and place them in containers, they fire an object creation event, and usually the configured folder fires an object added event.
>>> from zc.shortcut import factory, interfaces, Shortcut >>> from zope import interface, component, event >>> class IDummy(interface.Interface): ... pass ... >>> from zope.location.interfaces import ILocation >>> class Dummy(object): ... interface.implements(IDummy, ILocation) ... def __init__(self, *args, **kwargs): ... self.args = args ... self.kwargs = kwargs ... >>> f = factory.Factory(Dummy, 'title', 'description') >>> from zope.interface import verify >>> verify.verifyObject(interfaces.IShortcutFactory, f) True
The factory always returns an interface declaration for a shortcut from getInterfaces, while getTargetInterfaces returns the declaration for the created object.
>>> f.getInterfaces() == interface.implementedBy(Shortcut) True >>> f.getTargetInterfaces() == interface.implementedBy(Dummy) True
factories will fail to create an object if a container has not been registered as a repository.
>>> f() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE Traceback (most recent call last): ... ComponentLookupError: (<Dummy...>, <...IContainer>, 'shortcutTargetRepository')
If we register a repository then the factory will fire a creation event, add the object to the repository, and return a shortcut to the new object.
>>> import zope.app.container.interfaces >>> class DummyContainer(dict): ... interface.implements(zope.app.container.interfaces.IContainer) ... >>> repo = DummyContainer() >>> @component.adapter(IDummy) ... @interface.implementer(zope.app.container.interfaces.IContainer) ... def DummyRepoGetter(content): ... return repo ... >>> component.provideAdapter( ... DummyRepoGetter, name=interfaces.REPOSITORY_NAME) >>> from zope.app.container.contained import NameChooser >>> component.provideAdapter(NameChooser, adapts=(interface.Interface,)) >>> # now, before we actually actually run the adding machinery, we'll >>> # set up some machinery that will let us look at events firing ... >>> heard_events = [] # we'll collect the events here >>> event.subscribers.append(heard_events.append) >>> import pprint >>> from zope import interface >>> showEventsStart = 0 >>> def iname(ob): ... return iter(interface.providedBy(ob)).next().__name__ ... >>> def getId(ob): ... if ob is None or isinstance(ob, (int, float, basestring, tuple)): ... return "(%r)" % (ob,) ... id = getattr(ob, 'id', getattr(ob, '__name__', None)) ... if not id: ... id = "a %s (%s)" % (ob.__class__.__name__, iname(ob)) ... return id ... >>> def showEvents(start=None): # to generate a friendly view of events ... global showEventsStart ... if start is None: ... start = showEventsStart ... res = [ ... '%s fired for %s.' % (iname(ev), getId(ev.object)) ... for ev in heard_events[start:]] ... res.sort() ... pprint.pprint(res) ... showEventsStart = len(heard_events) ... >>> sc = f(12, 'foo', 'barbaz', sloop=19) >>> showEvents() ['IObjectCreatedEvent fired for a Dummy (IDummy).'] >>> repo['Dummy'].args (12, 'foo', 'barbaz') >>> repo['Dummy'].kwargs {'sloop': 19} >>> sc.raw_target is repo['Dummy'] True>>> event.subscribers.pop() is not None # cleanup True
Using alternate shortcut implementations
The shortcut factory takes an optional keyword parameter to specify the factory used to create the shortcut. By default, zc.shortcut.Shortcut is used, but more specialized shortcuts may be needed for some applications. This allows the factory to be used regardless of the specific shortcut implementation.
Let’s create an alternate class that can be used as a shortcut (it doesn’t really matter that the example class isn’t useful):
>>> class AlternateShortcut(object): ... interface.implements(interfaces.IShortcut) ... def __init__(self, object): ... self.raw_target = object ... self.target = object
Now we can create a factory that creates instances of this class instead of the default shortcut class:
>>> f = factory.Factory(Dummy, 'title', 'description', ... shortcut_factory=AlternateShortcut)
Using the factory returns an instance of our alternate shortcut implementation:
>>> sc = f(1, 2, 3) >>> isinstance(sc, AlternateShortcut) True >>> isinstance(sc.raw_target, Dummy) True >>> sc.target.args (1, 2, 3)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.