Turning MessageIDs into rocks
Status:
Author
Status quo
MessageIDs are implemented as a subclass of unicode. They are
typically instanciated using MessageIDFactory.
For example:
>>> from zope.i18nmessageid import MessageIDFactory
>>> _ = MessageIDFactory("futurama")
>>> robot = _(u"robot-message", u"${name} is a robot.")
The default value as well as the mapping for variable interpolation are introspectable:
>>> robot
u'robot-message'
>>> robot.domain
'futurama'
>>> robot.default
u'${name} is a robot.'
>>> robot.mapping
These values are also changeable, which is especially useful when providing data with which the variables are to be interpolated:
>>> robot.domain = "planetexpress"
>>> robot.default = u"${name} is not a robot."
>>> robot.mapping = {u'name': u'Bender'}
>>> robot.domain
'planetexpress'
>>> robot.default
u'${name} is not a robot.'
>>> robot.mapping
{u'name': u'Bender'}
>>> from zope.i18n import translate
>>> translate(robot)
u'Bender is not a robot."
Problem
The mutability of message ids contradicts the fact that string and unicode objects are immutable. Except that it additionally carries translation information, a message id should behave like a unicode object anywhere else.
The mutability issue becomes very evident and problematic when
dealing with security. Since they are not basic objects
(immutable objects like string and unicode instances, often
called rocks), they are security proxied. Components using
message ids returned by other components have to strip security
proxies from the message ids before working with them. This is
unnecessary.
Having security declarations for message ids is only a somewhat acceptable solution. It would still not solve the mutability problem. We have been treating message ids like unicode strings all along. If it wasn't for translation, we would use unicode. Therefore, message ids should behave like unicode strings in every possible way. That includes immutability. As a bonus, the security system will treat them like basic objects (rocks), making security declarations unnecessary.
Goals
- Avoid security proxying of message ids.
- Make semantics of message ids similar to those of string and unicode objects.
- Retain full functionality.
Proposed solution
The solution I'm proposing makes message ids immutable, in other
words they'll be treated as rocks by the security framework.
This means that all of the additional attributes on messageid
instances are absolutely static. If one is to be changed, a new
instance is created.
Consider the example from above:
>>> from zope.i18nmessageid import MessageFactory, Message
>>> _ = MessageFactory("futurama")
>>> robot = _(u"robot-message", u"${name} is a robot.")
Messages at first seem like they are unicode strings:
>>> robot
u'robot-message'
>>> isinstance(robot, unicode)
True
The additional domain, default and mapping information is available through attributes:
>>> robot.default
u'${name} is a robot.'
>>> robot.mapping
>>> robot.domain
'futurama'
The message's attributes are considered part of the immutable message object. They cannot be changed once the message id is created:
>>> robot.domain = "planetexpress"
Traceback (most recent call last):
...
TypeError: readonly attribute
>>> robot.default = u"${name} is not a robot."
Traceback (most recent call last):
...
TypeError: readonly attribute
>>> robot.mapping = {u'name': u'Bender'}
Traceback (most recent call last):
...
TypeError: readonly attribute
If you need to change their information, you'll have to make a new message id object:
>>> new_robot = Message(robot, mapping={u'name': u'Bender'})
>>> new_robot
u'robot-message'
>>> new_robot.domain
'futurama'
>>> new_robot.default
u'${name} is a robot.'
>>> new_robot.mapping
{u'name': u'Bender'}
Last but not least, messages are reduceable for pickling:
>>> callable, args = new_robot.__reduce__()
>>> callable is Message
True
>>> args
(u'robot-message', 'futurama', u'${name} is a robot.', {u'name': u'Bender'})
>>> fembot = Message(u'fembot')
>>> callable, args = fembot.__reduce__()
>>> callable is Message
True
>>> args
(u'fembot', None, None, None)
Message IDs? and backward compatability
The change to immutability is not a simple refactoring that can be coped with backward compatible APIs?--it is a change in semantics. Because immutability is one of those "you either have it or you don't" things (like pregnancy or death), we will not be able to support both in one implementation.
The proposed solution for backward compatability is to support
both implementations in parallel, deprecating the mutable one. A
separate factory, MessageFactory, instantiates immutable
messages, while the deprecated old one continues to work like
before.
The roadmap to immutable-only message ids is proposed as follows:
Zope 3.1: Immutable message ids are introduced. Security declarations for mutable message ids are provided to make the stripping of security proxies unnecessary.
Zope 3.2: Mutable message ids are deprecated.
Zope 3.3: Mutable message ids are removed.
Implementation and Status
Because truly immutable objects cannot be implemented in Python, the new messages have to be implemented in C. Jim and I paired on this at the Castle Sprint 2004. This proposal serves as a doctest at zope.i18nmessageid/messages.txt.
Step 1 of the above roadmap has been fulfilled: Zope 3.1 has rolled out with the new message implementation. Step 2 has been fulfilled on the Zope 3 trunk which will become Zope 3.2.
Risks
- There exists a notable amount of documentation on message ids, most prominently the two printed books (this is why long-term backward compatability is important).
Implementation status --mgedmin, 2005/10/12 06:46 EST reply
Zope 3 trunk, revision 39091, has an implementation of this proposal, but without the string interpolation syntax.
You can do things like:
>>> from zope.i18nmessageid import MessageFactory
>>> from zope.i18n import translate
>>> _ = MessageFactory('my_domain')
>>> msg = _('A $message', mapping={'message': 'greeting!'})
>>> translate(msg)
u'A greeting!'
>>> msg = _('A $message') % {'message': 'greeting!'}
>>> translate(msg)
u'A $message'
