summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Theune <ct@gocept.com>2007-05-03 21:58:43 +0000
committerChristian Theune <ct@gocept.com>2007-05-03 21:58:43 +0000
commit3f6536d337be41c3acc241d46f8a023daa52b3ad (patch)
treef3adebb33cf00a800b79d0e6bc5c64613112b3c4
parent3a07c79a0358ace0a3576e8e0ff44303d06a60f5 (diff)
downloadzope-i18nmessageid-3f6536d337be41c3acc241d46f8a023daa52b3ad.tar.gz
Moving code to satellite.
-rw-r--r--src/zope/i18nmessageid/DEPENDENCIES.cfg1
-rw-r--r--src/zope/i18nmessageid/SETUP.cfg3
-rw-r--r--src/zope/i18nmessageid/__init__.py18
-rw-r--r--src/zope/i18nmessageid/_zope_i18nmessageid_message.c266
-rw-r--r--src/zope/i18nmessageid/message.py188
-rw-r--r--src/zope/i18nmessageid/messages.txt125
-rw-r--r--src/zope/i18nmessageid/tests.py28
7 files changed, 629 insertions, 0 deletions
diff --git a/src/zope/i18nmessageid/DEPENDENCIES.cfg b/src/zope/i18nmessageid/DEPENDENCIES.cfg
new file mode 100644
index 0000000..ea3a37f
--- /dev/null
+++ b/src/zope/i18nmessageid/DEPENDENCIES.cfg
@@ -0,0 +1 @@
+zope.testing
diff --git a/src/zope/i18nmessageid/SETUP.cfg b/src/zope/i18nmessageid/SETUP.cfg
new file mode 100644
index 0000000..c2a31ad
--- /dev/null
+++ b/src/zope/i18nmessageid/SETUP.cfg
@@ -0,0 +1,3 @@
+<extension _zope_i18nmessageid_message>
+ source _zope_i18nmessageid_message.c
+</extension>
diff --git a/src/zope/i18nmessageid/__init__.py b/src/zope/i18nmessageid/__init__.py
new file mode 100644
index 0000000..bff6cd0
--- /dev/null
+++ b/src/zope/i18nmessageid/__init__.py
@@ -0,0 +1,18 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""I18n Messages
+
+$Id$
+"""
+from zope.i18nmessageid.message import Message, MessageFactory
diff --git a/src/zope/i18nmessageid/_zope_i18nmessageid_message.c b/src/zope/i18nmessageid/_zope_i18nmessageid_message.c
new file mode 100644
index 0000000..02e0988
--- /dev/null
+++ b/src/zope/i18nmessageid/_zope_i18nmessageid_message.c
@@ -0,0 +1,266 @@
+/*############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+############################################################################*/
+
+/* $Id$ */
+
+#include "Python.h"
+
+/* these macros make gc support easier; they are only available in
+ Python 2.4 and borrowed from there */
+
+#ifndef Py_CLEAR
+#define Py_CLEAR(op) \
+ do { \
+ if (op) { \
+ PyObject *tmp = (op); \
+ (op) = NULL; \
+ Py_DECREF(tmp); \
+ } \
+ } while (0)
+#endif
+
+#ifndef Py_VISIT
+#define Py_VISIT(op) \
+ do { \
+ if (op) { \
+ int vret = visit((op), arg); \
+ if (vret) \
+ return vret; \
+ } \
+ } while (0)
+#endif
+
+/* ----------------------------------------------------- */
+
+typedef struct {
+ PyUnicodeObject base;
+ PyObject *domain;
+ PyObject *default_;
+ PyObject *mapping;
+} Message;
+
+static PyTypeObject MessageType;
+
+static PyObject *
+Message_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+ static char *kwlist[] = {"value", "domain", "default", "mapping", NULL};
+ PyObject *value, *domain=NULL, *default_=NULL, *mapping=NULL, *s;
+ Message *self;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist,
+ &value, &domain, &default_, &mapping))
+ return NULL;
+
+ args = Py_BuildValue("(O)", value);
+ if (args == NULL)
+ return NULL;
+
+ s = PyUnicode_Type.tp_new(type, args, NULL);
+ Py_DECREF(args);
+ if (s == NULL)
+ return NULL;
+
+ if (! PyObject_TypeCheck(s, &MessageType))
+ {
+ PyErr_SetString(PyExc_TypeError,
+ "unicode.__new__ didn't return a Message");
+ Py_DECREF(s);
+ return NULL;
+ }
+
+ self = (Message*)s;
+
+ if (PyObject_TypeCheck(value, &MessageType))
+ {
+ self->domain = ((Message *)value)->domain;
+ self->default_ = ((Message *)value)->default_;
+ self->mapping = ((Message *)value)->mapping;
+ }
+ else
+ {
+ self->domain = self->default_ = self->mapping = NULL;
+ }
+
+ if (domain != NULL)
+ self->domain = domain;
+
+ if (default_ != NULL)
+ self->default_ = default_;
+
+ if (mapping != NULL)
+ self->mapping = mapping;
+
+ Py_XINCREF(self->mapping);
+ Py_XINCREF(self->default_);
+ Py_XINCREF(self->domain);
+
+ return (PyObject *)self;
+}
+
+/* Code to access structure members by accessing attributes */
+
+#include "structmember.h"
+
+static PyMemberDef Message_members[] = {
+ { "domain", T_OBJECT, offsetof(Message, domain), RO },
+ { "default", T_OBJECT, offsetof(Message, default_), RO },
+ { "mapping", T_OBJECT, offsetof(Message, mapping), RO },
+ {NULL} /* Sentinel */
+};
+
+static int
+Message_traverse(Message *self, visitproc visit, void *arg)
+{
+ Py_VISIT(self->domain);
+ Py_VISIT(self->default_);
+ Py_VISIT(self->mapping);
+ return 0;
+}
+
+static int
+Message_clear(Message *self)
+{
+ Py_CLEAR(self->domain);
+ Py_CLEAR(self->default_);
+ Py_CLEAR(self->mapping);
+ return 0;
+}
+
+static void
+Message_dealloc(Message *self)
+{
+ Message_clear(self);
+ self->base.ob_type->tp_free((PyObject*)self);
+}
+
+static PyObject *
+Message_reduce(Message *self)
+{
+ PyObject *value, *result;
+ value = PyObject_CallFunctionObjArgs((PyObject *)&PyUnicode_Type, self, NULL);
+ if (value == NULL)
+ return NULL;
+ result = Py_BuildValue("(O(OOOO))", self->base.ob_type,
+ value,
+ self->domain ? self->domain : Py_None,
+ self->default_ ? self->default_ : Py_None,
+ self->mapping ? self->mapping : Py_None);
+ Py_DECREF(value);
+ return result;
+}
+
+static PyMethodDef Message_methods[] = {
+ {"__reduce__", (PyCFunction)Message_reduce, METH_NOARGS,
+ "Reduce messages to a serializable form."},
+ {NULL} /* Sentinel */
+};
+
+
+static char MessageType__doc__[] =
+"Message\n"
+"\n"
+"This is a string used as a message. It has a domain attribute that is\n"
+"its source domain, and a default attribute that is its default text to\n"
+"display when there is no translation. domain may be None meaning there is\n"
+"no translation domain. default may also be None, in which case the\n"
+"message id itself implicitly serves as the default text.\n";
+
+statichere PyTypeObject
+MessageType = {
+ PyObject_HEAD_INIT(NULL)
+ /* ob_size */ 0,
+ /* tp_name */ "zope.i18nmessageid.message."
+ "Message",
+ /* tp_basicsize */ sizeof(Message),
+ /* tp_itemsize */ 0,
+ /* tp_dealloc */ (destructor)&Message_dealloc,
+ /* tp_print */ (printfunc)0,
+ /* tp_getattr */ (getattrfunc)0,
+ /* tp_setattr */ (setattrfunc)0,
+ /* tp_compare */ (cmpfunc)0,
+ /* tp_repr */ (reprfunc)0,
+ /* tp_as_number */ 0,
+ /* tp_as_sequence */ 0,
+ /* tp_as_mapping */ 0,
+ /* tp_hash */ (hashfunc)0,
+ /* tp_call */ (ternaryfunc)0,
+ /* tp_str */ (reprfunc)0,
+ /* tp_getattro */ (getattrofunc)0,
+ /* tp_setattro */ (setattrofunc)0,
+ /* tp_as_buffer */ 0,
+ /* tp_flags */ Py_TPFLAGS_DEFAULT
+ | Py_TPFLAGS_BASETYPE
+ | Py_TPFLAGS_HAVE_GC,
+ /* tp_doc */ MessageType__doc__,
+ /* tp_traverse */ (traverseproc)Message_traverse,
+ /* tp_clear */ (inquiry)Message_clear,
+ /* tp_richcompare */ (richcmpfunc)0,
+ /* tp_weaklistoffset */ (long)0,
+ /* tp_iter */ (getiterfunc)0,
+ /* tp_iternext */ (iternextfunc)0,
+ /* tp_methods */ Message_methods,
+ /* tp_members */ Message_members,
+ /* tp_getset */ 0,
+ /* tp_base */ 0,
+ /* tp_dict */ 0, /* internal use */
+ /* tp_descr_get */ (descrgetfunc)0,
+ /* tp_descr_set */ (descrsetfunc)0,
+ /* tp_dictoffset */ 0,
+ /* tp_init */ (initproc)0,
+ /* tp_alloc */ (allocfunc)0,
+ /* tp_new */ (newfunc)Message_new,
+ /* tp_free */ 0, /* Low-level free-mem routine */
+ /* tp_is_gc */ (inquiry)0, /* For PyObject_IS_GC */
+};
+
+/* End of code for Message objects */
+/* -------------------------------------------------------- */
+
+
+/* List of methods defined in the module */
+
+static struct PyMethodDef _zope_i18nmessageid_message_methods[] = {
+ {NULL, (PyCFunction)NULL, 0, NULL} /* sentinel */
+};
+
+
+static char _zope_i18nmessageid_message_module_documentation[] =
+"I18n Messages"
+;
+
+#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */
+#define PyMODINIT_FUNC void
+#endif
+PyMODINIT_FUNC
+init_zope_i18nmessageid_message(void)
+{
+ PyObject *m;
+ /* Initialize types: */
+ MessageType.tp_base = &PyUnicode_Type;
+ if (PyType_Ready(&MessageType) < 0)
+ return;
+
+ /* Create the module and add the functions */
+ m = Py_InitModule3("_zope_i18nmessageid_message",
+ _zope_i18nmessageid_message_methods,
+ _zope_i18nmessageid_message_module_documentation);
+
+ if (m == NULL)
+ return;
+
+ /* Add types: */
+ if (PyModule_AddObject(m, "Message", (PyObject *)&MessageType) < 0)
+ return;
+}
diff --git a/src/zope/i18nmessageid/message.py b/src/zope/i18nmessageid/message.py
new file mode 100644
index 0000000..f414664
--- /dev/null
+++ b/src/zope/i18nmessageid/message.py
@@ -0,0 +1,188 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""I18n Messages
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+
+class Message(unicode):
+ """Message (Python implementation)
+
+ This is a string used as a message. It has a domain attribute that is
+ its source domain, and a default attribute that is its default text to
+ display when there is no translation. domain may be None meaning there is
+ no translation domain. default may also be None, in which case the
+ message id itself implicitly serves as the default text.
+
+ These are the doc tests from message.txt. Note that we have to create the
+ message manually since MessageFactory would return the C implementation.
+
+ >>> from zope.i18nmessageid.message import pyMessage as Message
+ >>> robot = Message(u"robot-message", 'futurama', u"${name} is a robot.")
+
+ >>> robot
+ u'robot-message'
+ >>> isinstance(robot, unicode)
+ True
+
+ >>> robot.default
+ u'${name} is a robot.'
+ >>> robot.mapping
+
+ Only the python implementation has a _readonly attribute
+ >>> robot._readonly
+ True
+
+ >>> 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
+
+ >>> 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'}
+
+ >>> 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)
+
+ Change classes for pickle tests
+ >>> import zope.i18nmessageid.message
+ >>> oldMessage = zope.i18nmessageid.message.Message
+ >>> zope.i18nmessageid.message.Message = Message
+
+ At first check if pickling and unpicklung from pyMessage to pyMessage works
+ >>> from pickle import dumps, loads
+ >>> pystate = dumps(new_robot)
+ >>> pickle_bot = loads(pystate)
+ >>> pickle_bot, pickle_bot.domain, pickle_bot.default, pickle_bot.mapping
+ (u'robot-message', 'futurama', u'${name} is a robot.', {u'name': u'Bender'})
+ >>> pickle_bot._readonly
+ True
+ >>> from zope.i18nmessageid.message import pyMessage
+ >>> pickle_bot.__reduce__()[0] is pyMessage
+ True
+ >>> del pickle_bot
+
+ At second check if cMessage is able to load the state of a pyMessage
+ >>> from _zope_i18nmessageid_message import Message
+ >>> zope.i18nmessageid.message.Message = Message
+ >>> c_bot = loads(pystate)
+ >>> c_bot, c_bot.domain, c_bot.default, c_bot.mapping
+ (u'robot-message', 'futurama', u'${name} is a robot.', {u'name': u'Bender'})
+ >>> c_bot._readonly
+ Traceback (most recent call last):
+ AttributeError: 'zope.i18nmessageid.message.Message' object has no attribute '_readonly'
+ >>> from _zope_i18nmessageid_message import Message as cMessage
+ >>> c_bot.__reduce__()[0] is cMessage
+ True
+
+ At last check if pyMessage can load a state of cMessage
+ >>> cstate = dumps(c_bot)
+ >>> del c_bot
+ >>> from zope.i18nmessageid.message import pyMessage as Message
+ >>> zope.i18nmessageid.message.Message = Message
+ >>> py_bot = loads(cstate)
+ >>> py_bot, py_bot.domain, py_bot.default, py_bot.mapping
+ (u'robot-message', 'futurama', u'${name} is a robot.', {u'name': u'Bender'})
+ >>> py_bot._readonly
+ True
+ >>> py_bot.__reduce__()[0] is pyMessage
+ True
+
+ Both pickle states should be equal
+ >>> pystate == cstate
+ True
+
+ Finally restore classes for other unit tests
+ >>> zope.i18nmessageid.message.Message = oldMessage
+ """
+
+ __slots__ = ('domain', 'default', 'mapping', '_readonly')
+
+ def __new__(cls, ustr, domain=None, default=None, mapping=None):
+ self = unicode.__new__(cls, ustr)
+ if isinstance(ustr, self.__class__):
+ domain = ustr.domain and ustr.domain[:] or domain
+ default = ustr.default and ustr.default[:] or default
+ mapping = ustr.mapping and ustr.mapping.copy() or mapping
+ ustr = unicode(ustr)
+ self.domain = domain
+ if default is None:
+ # MessageID does: self.default = ustr
+ self.default = default
+ else:
+ self.default = unicode(default)
+ self.mapping = mapping
+ self._readonly = True
+ return self
+
+ def __setattr__(self, key, value):
+ """Message is immutable
+
+ It cannot be changed once the message id is created.
+ """
+ if getattr(self, '_readonly', False):
+ raise TypeError('readonly attribute')
+ else:
+ return unicode.__setattr__(self, key, value)
+
+ def __reduce__(self):
+ return self.__class__, self.__getstate__()
+
+ def __getstate__(self):
+ return unicode(self), self.domain, self.default, self.mapping
+
+# save a copy for the unit tests
+pyMessage = Message
+
+try:
+ from _zope_i18nmessageid_message import Message
+except ImportError:
+ pass
+
+class MessageFactory(object):
+ """Factory for creating i18n messages."""
+
+ def __init__(self, domain):
+ self._domain = domain
+
+ def __call__(self, ustr, default=None, mapping=None):
+ return Message(ustr, self._domain, default, mapping)
diff --git a/src/zope/i18nmessageid/messages.txt b/src/zope/i18nmessageid/messages.txt
new file mode 100644
index 0000000..05ccdcc
--- /dev/null
+++ b/src/zope/i18nmessageid/messages.txt
@@ -0,0 +1,125 @@
+=============
+I18n Messages
+=============
+
+Rationale
+---------
+
+To translate any text, we must be able to discover the source domain
+of the text. A source domain is an identifier that identifies a
+project that produces program source strings. Source strings occur as
+literals in python programs, text in templates, and some text in XML
+data. The project implies a source language and an application
+context.
+
+We can think of a source domain as a collection of messages and
+associated translation strings.
+
+We often need to create unicode strings that will be displayed by
+separate views. The view cannot translate the string without knowing
+its source domain. A string or unicode literal carries no domain
+information, therefore we use messages. Messages are unicode strings
+which carry a translation source domain and possibly a default
+translation. They are created by a message factory. The message
+factory is created by calling ``MessageFactory`` with the source
+domain.
+
+
+Example
+-------
+
+In this example, we create a message factory and assign it to _. By
+convention, we use _ as the name of our factory to be compatible with
+translatable string extraction tools such as xgettext. We then call _
+with a string that needs to be translatable:
+
+ >>> 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.
diff --git a/src/zope/i18nmessageid/tests.py b/src/zope/i18nmessageid/tests.py
new file mode 100644
index 0000000..18a48d1
--- /dev/null
+++ b/src/zope/i18nmessageid/tests.py
@@ -0,0 +1,28 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Message ID tests.
+
+$Id$
+"""
+import unittest
+from zope.testing.doctestunit import DocTestSuite, DocFileSuite
+
+def test_suite():
+ return unittest.TestSuite((
+ DocTestSuite('zope.i18nmessageid.message'),
+ DocFileSuite('messages.txt', package='zope.i18nmessageid'),
+ ))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest="test_suite")