Skip to content

Commit

Permalink
Reimplement message id changes including on the C extension.
Browse files Browse the repository at this point in the history
  • Loading branch information
thefunny42 committed Oct 18, 2018
1 parent ae338be commit 22a4f85
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 85 deletions.
7 changes: 5 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ Changes
4.3 (unreleased)
----------------

- Nothing changed yet.
- Add attributes to support pluralization on a Message and update the
MessageFactory accordingly.


4.2 (2018-10-05)
----------------

- Fix the possibility of a rare crash in the C extension when deallocating items. See `#7 <https://github.com/zopefoundation/zope.i18nmessageid/issues/7>`_.
- Fix the possibility of a rare crash in the C extension when
deallocating items. See `issue 7
<https://github.com/zopefoundation/zope.i18nmessageid/issues/7>`_.

- Drop support for Python 3.3.

Expand Down
9 changes: 6 additions & 3 deletions docs/narr.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ exports an already-created factory for that domain:
>>> foo = _z_('foo')
>>> foo.domain
'zope'


Example Usage
-------------
Expand Down Expand Up @@ -135,14 +135,17 @@ Last but not least, messages are reduceable for pickling:
>>> args == (u'robot-message',
... 'futurama',
... u'${name} is a robot.',
... {u'name': u'Bender'})
... {u'name': u'Bender'},
... None,
... None,
... None)
True

>>> fembot = Message(u'fembot')
>>> callable, args = fembot.__reduce__()
>>> callable is Message
True
>>> args == (u'fembot', None, None, None)
>>> args == (u'fembot', None, None, None, None, None, None)
True

Pickling and unpickling works, which means we can store message IDs in
Expand Down
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@


def read(*rnames):
return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
with open(os.path.join(os.path.dirname(__file__), *rnames)) as stream:
return stream.read()


class optional_build_ext(build_ext):
Expand Down Expand Up @@ -135,7 +136,7 @@ def _unavailable(self, e):
packages=find_packages('src'),
package_dir={'': 'src'},
namespace_packages=['zope'],
install_requires=['setuptools'],
install_requires=['setuptools', 'six'],
include_package_data=True,
test_suite='zope.i18nmessageid.tests.test_suite',
zip_safe=False,
Expand Down
110 changes: 79 additions & 31 deletions src/zope/i18nmessageid/_zope_i18nmessageid_message.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,63 +63,99 @@ typedef struct {
PyObject *domain;
PyObject *default_;
PyObject *mapping;
PyObject *value_plural;
PyObject *default_plural;
PyObject *number;
} Message;

static PyTypeObject MessageType;

static PyObject *
Message_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
static char *kwlist[] = {"value", "domain", "default", "mapping", NULL};
static char *kwlist[] = {"value", "domain", "default", "mapping",
"msgid_plural", "default_plural", "number", NULL};
PyObject *value, *domain=NULL, *default_=NULL, *mapping=NULL, *s;
PyObject *value_plural=NULL, *default_plural=NULL, *number=NULL;
Message *self;

if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist,
&value, &domain, &default_, &mapping))
return NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOOOOO", kwlist,
&value, &domain, &default_, &mapping,
&value_plural, &default_plural, &number))
return NULL;

if (number != NULL && Py_None != number) {
#if PY_MAJOR_VERSION >= 3
if (!(PyLong_Check(number) || PyFloat_Check(number))) {
#else
if (!(PyLong_Check(number) || PyInt_Check(number) || PyFloat_Check(number))) {
#endif
PyErr_SetString(PyExc_TypeError,
"`number` should be an integer or a float");
return NULL;
}
}

args = Py_BuildValue("(O)", value);
if (args == NULL)
return NULL;

s = PyUnicode_Type.tp_new(type, args, 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;
}
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 (PyObject_TypeCheck(value, &MessageType)) {
/* value is a Message so we copy it and use it as base */
self->domain = ((Message *)value)->domain;
self->default_ = ((Message *)value)->default_;
self->mapping = ((Message *)value)->mapping;
self->value_plural = ((Message *)value)->value_plural;
self->default_plural = ((Message *)value)->default_plural;
self->number = ((Message *)value)->number;
}
else {
self->domain = NULL;
self->default_ = NULL;
self->mapping = NULL;
self->value_plural = NULL;
self->default_plural = NULL;
self->number = NULL;
}

if (domain != NULL)
self->domain = domain;

if (default_ != NULL)
self->default_ = default_;

if (mapping != NULL)
self->mapping = mapping;

if (value_plural != NULL)
self->value_plural = value_plural;

if (default_plural != NULL)
self->default_plural = default_plural;

if (number != NULL) {
self->number = number;
}

Py_XINCREF(self->mapping);
Py_XINCREF(self->default_);
Py_XINCREF(self->domain);
Py_XINCREF(self->value_plural);
Py_XINCREF(self->default_plural);
Py_XINCREF(self->number);

return (PyObject *)self;
}
Expand All @@ -132,6 +168,9 @@ static PyMemberDef Message_members[] = {
{ "domain", T_OBJECT, offsetof(Message, domain), READONLY },
{ "default", T_OBJECT, offsetof(Message, default_), READONLY },
{ "mapping", T_OBJECT, offsetof(Message, mapping), READONLY },
{ "msgid_plural", T_OBJECT, offsetof(Message, value_plural), READONLY },
{ "default_plural", T_OBJECT, offsetof(Message, default_plural), READONLY },
{ "number", T_OBJECT, offsetof(Message, number), READONLY },
{NULL} /* Sentinel */
};

Expand All @@ -141,6 +180,9 @@ Message_traverse(Message *self, visitproc visit, void *arg)
Py_VISIT(self->domain);
Py_VISIT(self->default_);
Py_VISIT(self->mapping);
Py_VISIT(self->value_plural);
Py_VISIT(self->default_plural);
Py_VISIT(self->number);
return 0;
}

Expand All @@ -150,6 +192,9 @@ Message_clear(Message *self)
Py_CLEAR(self->domain);
Py_CLEAR(self->default_);
Py_CLEAR(self->mapping);
Py_CLEAR(self->value_plural);
Py_CLEAR(self->default_plural);
Py_CLEAR(self->number);
return 0;
}

Expand All @@ -168,11 +213,14 @@ Message_reduce(Message *self)
value = PyObject_CallFunctionObjArgs((PyObject *)&PyUnicode_Type, self, NULL);
if (value == NULL)
return NULL;
result = Py_BuildValue("(O(OOOO))", Py_TYPE(&(self->base)),
result = Py_BuildValue("(O(OOOOOOO))", Py_TYPE(&(self->base)),
value,
self->domain ? self->domain : Py_None,
self->default_ ? self->default_ : Py_None,
self->mapping ? self->mapping : Py_None);
self->mapping ? self->mapping : Py_None,
self->value_plural ? self->value_plural : Py_None,
self->default_plural ? self->default_plural : Py_None,
self->number ? self->number : Py_None);
Py_DECREF(value);
return result;
}
Expand All @@ -184,7 +232,7 @@ static PyMethodDef Message_methods[] = {
};


static char MessageType__doc__[] =
static char MessageType__doc__[] =
"Message\n"
"\n"
"This is a string used as a message. It has a domain attribute that is\n"
Expand Down Expand Up @@ -216,7 +264,7 @@ MessageType = {
/* tp_setattro */ (setattrofunc)0,
/* tp_as_buffer */ 0,
/* tp_flags */ Py_TPFLAGS_DEFAULT
| Py_TPFLAGS_BASETYPE
| Py_TPFLAGS_BASETYPE
| Py_TPFLAGS_HAVE_GC,
/* tp_doc */ MessageType__doc__,
/* tp_traverse */ (traverseproc)Message_traverse,
Expand Down Expand Up @@ -252,7 +300,7 @@ static struct PyMethodDef _zope_i18nmessageid_message_methods[] = {
static char _zope_i18nmessageid_message_module_name[] =
"_zope_i18nmessageid_message";

static char _zope_i18nmessageid_message_module_documentation[] =
static char _zope_i18nmessageid_message_module_documentation[] =
"I18n Messages";

#if PY_MAJOR_VERSION >= 3
Expand Down Expand Up @@ -285,7 +333,7 @@ PyMODINIT_FUNC
MessageType.tp_base = &PyUnicode_Type;
if (PyType_Ready(&MessageType) < 0)
return MOD_ERROR_VAL;

/* Create the module and add the functions */
#if PY_MAJOR_VERSION >= 3
m = PyModule_Create(&moduledef);
Expand All @@ -294,10 +342,10 @@ PyMODINIT_FUNC
_zope_i18nmessageid_message_methods,
_zope_i18nmessageid_message_module_documentation);
#endif

if (m == NULL)
return MOD_ERROR_VAL;

/* Add types: */
if (PyModule_AddObject(m, "Message", (PyObject *)&MessageType) < 0)
return MOD_ERROR_VAL;
Expand Down
51 changes: 37 additions & 14 deletions src/zope/i18nmessageid/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@
"""
__docformat__ = "reStructuredText"

try:
unicode
except NameError: #pragma NO COVER Python3
unicode = str
import six


class Message(unicode):
class Message(six.text_type):
"""Message (Python implementation)
This is a string used as a message. It has a domain attribute that is
Expand All @@ -31,18 +28,37 @@ class Message(unicode):
message id itself implicitly serves as the default text.
"""

__slots__ = ('domain', 'default', 'mapping', '_readonly')
__slots__ = (
'domain', 'default', 'mapping', '_readonly',
'msgid_plural', 'default_plural', 'number')

def __new__(cls, ustr, domain=None, default=None, mapping=None):
self = unicode.__new__(cls, ustr)
def __new__(cls, ustr, domain=None, default=None, mapping=None,
msgid_plural=None, default_plural=None, number=None):
self = six.text_type.__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)
msgid_plural = (
ustr.msgid_plural and ustr.msgid_plural[:] or msgid_plural)
default_plural = (
ustr.default_plural and ustr.default_plural[:]
or default_plural)
number = ustr.number is not None and ustr.number or number
ustr = six.text_type(ustr)

self.domain = domain
self.default = default
self.mapping = mapping
self.msgid_plural = msgid_plural
self.default_plural = default_plural

if number is not None and not isinstance(
number, six.integer_types + (float,)):
# Number must be an integer
raise TypeError('`number` should be an integer or a float')

self.number = number
self._readonly = True
return self

Expand All @@ -54,27 +70,34 @@ def __setattr__(self, key, value):
if getattr(self, '_readonly', False):
raise TypeError('readonly attribute')
else:
return unicode.__setattr__(self, key, value)
return six.text_type.__setattr__(self, key, value)

def __getstate__(self):
return unicode(self), self.domain, self.default, self.mapping
return (
six.text_type(self), self.domain, self.default, self.mapping,
self.msgid_plural, self.default_plural, self.number)

def __reduce__(self):
return self.__class__, self.__getstate__()


# Name the fallback Python implementation to make it easier to test.
pyMessage = Message


try:
from ._zope_i18nmessageid_message import Message
except ImportError: # pragma: no cover
except ImportError: # pragma: no cover
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)
def __call__(self, ustr, default=None, mapping=None,
msgid_plural=None, default_plural=None, number=None):
return Message(ustr, self._domain, default, mapping,
msgid_plural, default_plural, number)
Loading

0 comments on commit 22a4f85

Please sign in to comment.