Skip to content

Commit cbaff58

Browse files
Add function ZPublisher.utils.fix_properties.
You can call this to fix lines properties to only contain strings, not bytes. It also replaces the deprecated property types ulines, utext, utoken, and ustring with their non-unicode variants. See #987 for why this is needed. Note: the code is not called by Zope itself. The idea is that integrators who think they need this in their database, can call it. Sample use: app.ZopeFindAndApply(app, apply_func=fix_properties) I intend to use this (or a temporary copy) in the Plone 6 upgrade code. See plone/Products.CMFPlone#3305
1 parent adacc41 commit cbaff58

File tree

3 files changed

+184
-0
lines changed

3 files changed

+184
-0
lines changed

CHANGES.rst

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst
1111
5.4 (unreleased)
1212
----------------
1313

14+
- Add function ``ZPublisher.utils.fix_properties``.
15+
You can call this to fix lines properties to only contain strings, not bytes.
16+
It also replaces the deprecated property types ulines, utext, utoken, and ustring
17+
with their non-unicode variants.
18+
See `issue 987 <https://github.com/zopefoundation/Zope/issues/987>`_.
19+
1420
- Add support for Python 3.10.
1521

1622
- Update to newest compatible versions of dependencies.

src/ZPublisher/tests/test_utils.py

+68
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,71 @@ def test_unicode(self):
3434
def test_utf_8(self):
3535
self.assertEqual(self._makeOne('test\xc2\xae'), 'test\xc2\xae')
3636
self.assertEqual(self._makeOne(b'test\xc2\xae'), 'test\xae')
37+
38+
39+
class FixPropertiesTests(unittest.TestCase):
40+
41+
def _makeOne(self):
42+
from OFS.PropertyManager import PropertyManager
43+
44+
return PropertyManager()
45+
46+
def test_lines(self):
47+
from ZPublisher.utils import fix_properties
48+
49+
obj = self._makeOne()
50+
obj._setProperty("mixed", ["text and", b"bytes"], "lines")
51+
self.assertEqual(obj.getProperty("mixed"), ("text and", b"bytes"))
52+
self.assertEqual(obj.getPropertyType("mixed"), "lines")
53+
54+
fix_properties(obj)
55+
self.assertEqual(obj.getProperty("mixed"), ("text and", "bytes"))
56+
self.assertEqual(obj.getPropertyType("mixed"), "lines")
57+
58+
def test_ulines(self):
59+
from ZPublisher.utils import fix_properties
60+
61+
obj = self._makeOne()
62+
obj._setProperty("mixed", ["text and", b"bytes"], "ulines")
63+
self.assertEqual(obj.getProperty("mixed"), ("text and", b"bytes"))
64+
self.assertEqual(obj.getPropertyType("mixed"), "ulines")
65+
66+
fix_properties(obj)
67+
self.assertEqual(obj.getProperty("mixed"), ("text and", "bytes"))
68+
self.assertEqual(obj.getPropertyType("mixed"), "lines")
69+
70+
def test_utokens(self):
71+
from ZPublisher.utils import fix_properties
72+
73+
obj = self._makeOne()
74+
obj._setProperty("mixed", ["text", "and", b"bytes"], "utokens")
75+
self.assertEqual(obj.getProperty("mixed"), ("text", "and", b"bytes"))
76+
self.assertEqual(obj.getPropertyType("mixed"), "utokens")
77+
78+
fix_properties(obj)
79+
self.assertEqual(obj.getProperty("mixed"), ("text", "and", "bytes"))
80+
self.assertEqual(obj.getPropertyType("mixed"), "tokens")
81+
82+
def test_utext(self):
83+
from ZPublisher.utils import fix_properties
84+
85+
obj = self._makeOne()
86+
obj._setProperty("prop1", "multiple\nlines", "utext")
87+
self.assertEqual(obj.getProperty("prop1"), "multiple\nlines")
88+
self.assertEqual(obj.getPropertyType("prop1"), "utext")
89+
90+
fix_properties(obj)
91+
self.assertEqual(obj.getProperty("prop1"), "multiple\nlines")
92+
self.assertEqual(obj.getPropertyType("prop1"), "text")
93+
94+
def test_ustring(self):
95+
from ZPublisher.utils import fix_properties
96+
97+
obj = self._makeOne()
98+
obj._setProperty("prop1", "single line", "ustring")
99+
self.assertEqual(obj.getProperty("prop1"), "single line")
100+
self.assertEqual(obj.getPropertyType("prop1"), "ustring")
101+
102+
fix_properties(obj)
103+
self.assertEqual(obj.getProperty("prop1"), "single line")
104+
self.assertEqual(obj.getPropertyType("prop1"), "string")

src/ZPublisher/utils.py

+110
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from Acquisition import aq_parent
2020

2121

22+
logger = logging.getLogger('ZPublisher')
2223
AC_LOGGER = logging.getLogger('event.AccessControl')
2324

2425

@@ -100,3 +101,112 @@ def basic_auth_decode(token):
100101
plain = plain.decode('latin-1')
101102
user, password = plain.split(':', 1) # Split at most once
102103
return (user, password)
104+
105+
106+
def _string_tuple(value):
107+
if not value:
108+
return ()
109+
return tuple([safe_unicode(element) for element in value])
110+
111+
112+
def fix_properties(obj, path=None):
113+
"""Fix properties on object.
114+
115+
This does two things:
116+
117+
1. Make sure lines contain only strings, instead of bytes,
118+
or worse: a combination of strings and bytes.
119+
2. Replace deprecated ulines, utext, utoken, and ustring properties
120+
with their non-unicode variant, using native strings.
121+
122+
See https://github.com/zopefoundation/Zope/issues/987
123+
124+
Since Zope 5.3, a lines property stores strings instead of bytes.
125+
But there is no migration yet. (We do that here.)
126+
Result is that getProperty on an already created lines property
127+
will return the old value with bytes, but a newly created lines property
128+
will return strings. And you might get combinations.
129+
130+
Also since Zope 5.3, the ulines property type is deprecated.
131+
You should use a lines property instead.
132+
Same for a few others: utext, utoken, ustring.
133+
The unicode variants are planned to be removed in Zope 6.
134+
135+
Intended usage:
136+
app.ZopeFindAndApply(app, apply_func=fix_properties)
137+
"""
138+
if path is None:
139+
# When using ZopeFindAndApply, path is always given.
140+
# But we may be called by other code.
141+
if hasattr(object, 'getPhysicalPath'):
142+
path = '/'.join(object.getPhysicalPath())
143+
else:
144+
# Some simple object, for example in tests.
145+
# We don't care about the path then, it is only shown in logs.
146+
path = "/dummy"
147+
148+
for prop_info in obj.propertyMap():
149+
# Example: {'id': 'title', 'type': 'string', 'mode': 'w'}
150+
prop_id = prop_info.get("id")
151+
current = obj.getProperty(prop_id)
152+
if current is None:
153+
continue
154+
new_type = prop_type = prop_info.get("type")
155+
if prop_type == "lines":
156+
new = _string_tuple(current)
157+
elif prop_type == "ulines":
158+
new_type = "lines"
159+
new = _string_tuple(current)
160+
elif prop_type == "utokens":
161+
new_type = "tokens"
162+
new = _string_tuple(current)
163+
elif prop_type == "utext":
164+
new_type = "text"
165+
new = safe_unicode(current)
166+
elif prop_type == "ustring":
167+
new_type = "string"
168+
new = safe_unicode(current)
169+
else:
170+
continue
171+
if prop_type != new_type:
172+
# Replace with non-unicode variant.
173+
# This could easily lead to:
174+
# Exceptions.BadRequest: Invalid or duplicate property id.
175+
# obj._delProperty(prop_id)
176+
# obj._setProperty(prop_id, new, new_type)
177+
# So fix it by using internal details.
178+
for prop in obj._properties:
179+
if prop.get("id") == prop_id:
180+
prop["type"] = new_type
181+
# This is a tuple, so force a safe, just to be sure.
182+
obj._properties = obj._properties
183+
break
184+
else:
185+
# This probably cannot happen.
186+
# If it does, we want to know.
187+
logger.warning(
188+
"Could not change property %s from %s to %s for %s",
189+
prop_id,
190+
prop_type,
191+
new_type,
192+
path,
193+
)
194+
continue
195+
obj._updateProperty(prop_id, new)
196+
logger.info(
197+
"Changed property %s from %s to %s for %s",
198+
prop_id,
199+
prop_type,
200+
new_type,
201+
path,
202+
)
203+
continue
204+
if current != new:
205+
obj._updateProperty(prop_id, new)
206+
logger.info(
207+
"Changed property %s at %s so value fits the type %s (%r)",
208+
prop_id,
209+
path,
210+
prop_type,
211+
new,
212+
)

0 commit comments

Comments
 (0)