From 3f6536d337be41c3acc241d46f8a023daa52b3ad Mon Sep 17 00:00:00 2001 From: Christian Theune Date: Thu, 3 May 2007 21:58:43 +0000 Subject: Moving code to satellite. --- src/zope/i18nmessageid/DEPENDENCIES.cfg | 1 + src/zope/i18nmessageid/SETUP.cfg | 3 + src/zope/i18nmessageid/__init__.py | 18 ++ .../i18nmessageid/_zope_i18nmessageid_message.c | 266 +++++++++++++++++++++ src/zope/i18nmessageid/message.py | 188 +++++++++++++++ src/zope/i18nmessageid/messages.txt | 125 ++++++++++ src/zope/i18nmessageid/tests.py | 28 +++ 7 files changed, 629 insertions(+) create mode 100644 src/zope/i18nmessageid/DEPENDENCIES.cfg create mode 100644 src/zope/i18nmessageid/SETUP.cfg create mode 100644 src/zope/i18nmessageid/__init__.py create mode 100644 src/zope/i18nmessageid/_zope_i18nmessageid_message.c create mode 100644 src/zope/i18nmessageid/message.py create mode 100644 src/zope/i18nmessageid/messages.txt create mode 100644 src/zope/i18nmessageid/tests.py 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 @@ + + source _zope_i18nmessageid_message.c + 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") -- cgit v1.2.1