Skip to content

Commit

Permalink
fix: ensure Message.mapping is immutable
Browse files Browse the repository at this point in the history
Closes #1
  • Loading branch information
tseaver committed May 25, 2024
1 parent 735d4f8 commit bb94c59
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 8 deletions.
25 changes: 21 additions & 4 deletions src/zope/i18nmessageid/_zope_i18nmessageid_message.c
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,13 @@ Message_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
if (default_ != NULL)
self->default_ = default_;

if (mapping != NULL)
self->mapping = mapping;
if (mapping == Py_None) {
self->mapping = Py_None;
Py_INCREF(Py_None);
} else if (mapping != NULL) {
self->mapping = PyDictProxy_New(mapping);
} else {
}

if (value_plural != NULL)
self->value_plural = value_plural;
Expand Down Expand Up @@ -172,15 +177,27 @@ Message_dealloc(Message *self)
static PyObject *
Message_reduce(Message *self)
{
PyObject *value, *result;
PyObject *value, *mapping, *result;
value = PyObject_CallFunctionObjArgs((PyObject *)&PyUnicode_Type, self, NULL);
if (value == NULL)
return NULL;
if (self->mapping == NULL) {
mapping = Py_None;
}
else if (self->mapping == Py_None) {
mapping = Py_None;
} else {
mapping = PyObject_CallFunctionObjArgs(
(PyObject *)&PyDict_Type, self->mapping, NULL);
if (mapping == NULL) {
return NULL;
}
}
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,
mapping,
self->value_plural ? self->value_plural : Py_None,
self->default_plural ? self->default_plural : Py_None,
self->number ? self->number : Py_None);
Expand Down
20 changes: 16 additions & 4 deletions src/zope/i18nmessageid/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
##############################################################################
"""I18n Messages and factories.
"""
import types


__docformat__ = "reStructuredText"
_marker = object()
Expand Down Expand Up @@ -54,8 +56,10 @@ def __new__(cls, ustr, domain=_marker, default=_marker, mapping=_marker,
self.domain = domain
if default is not _marker:
self.default = default
if mapping is not _marker:
self.mapping = mapping
if mapping is None:
self.mapping = None
elif mapping is not _marker:
self.mapping = types.MappingProxyType(mapping)
if msgid_plural is not _marker:
self.msgid_plural = msgid_plural
if default_plural is not _marker:
Expand All @@ -81,9 +85,17 @@ def __setattr__(self, key, value):
return str.__setattr__(self, key, value)

def __getstate__(self):
# types.MappingProxyType is not picklable
mapping = None if self.mapping is None else dict(self.mapping)
return (
str(self), self.domain, self.default, self.mapping,
self.msgid_plural, self.default_plural, self.number)
str(self),
self.domain,
self.default,
mapping,
self.msgid_plural,
self.default_plural,
self.number,
)

def __reduce__(self):
return self.__class__, self.__getstate__()
Expand Down
21 changes: 21 additions & 0 deletions src/zope/i18nmessageid/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,20 @@ def test_values(self):
self.assertEqual(message.msgid_plural, 'testings')
self.assertEqual(message.default_plural, 'defaults')
self.assertEqual(message.number, 2)

with self.assertRaises(TypeError):
message.mapping['key'] = 'new value'

if self._TEST_READONLY:
self.assertTrue(message._readonly)

def test_mapping_is_readonly(self):
mapping = {'key': 'value'}
message = self._makeOne('testing', 'domain', mapping=mapping)

with self.assertRaises(TypeError):
message.mapping['key'] = 'new value'

def test_values_without_defaults(self):
mapping = {'key': 'value'}
message = self._makeOne(
Expand Down Expand Up @@ -206,6 +217,16 @@ def test_unknown_immutable(self):
with self.assertRaises((TypeError, AttributeError)):
message.unknown = 'unknown'

def test___reduce___wo_values(self):
message = self._makeOne('testing')
klass, state = message.__reduce__()
self.assertTrue(klass is self._getTargetClass())
self.assertTrue(message.mapping is None)
self.assertEqual(
state,
('testing', None, None, None, None, None, None)
)

def test___reduce__(self):
mapping = {'key': 'value'}
source = self._makeOne('testing')
Expand Down

0 comments on commit bb94c59

Please sign in to comment.