Skip to content

Commit 693d9e1

Browse files
committed
Enhance tests
test_capi.test_unicode: * Add tests on PyUnicode_New() and PyUnicode_Resize(). Check that the result is either a mutable string, or the empty string singleton. * Check that PyUnicode_Fill(), PyUnicode_CopyCharacters() and PyUnicode_WriteChar() fail to modify a string with 2 references. Adjust documentation.
1 parent 7d0e14f commit 693d9e1

4 files changed

Lines changed: 104 additions & 21 deletions

File tree

‎Doc/c-api/unicode.rst‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,8 @@ APIs:
414414
functions below.
415415
416416
While str objects are usually immutable in Python, this special C API
417-
returns a str object which can be mutated.
417+
returns a str object which can be mutated; except if *size* is zero in which
418+
case it returns the immutable empty string.
418419
419420
.. versionadded:: 3.3
420421
@@ -790,8 +791,10 @@ APIs:
790791
The function doesn't check string content, the result may not be a
791792
string in canonical representation.
792793
793-
While str objects are usually immutable in Python, this special C API allows
794-
mutating a fresh str object in-place if the string was not “used” yet.
794+
While str objects are usually immutable in Python, this special C API
795+
can resize a str object in-place if the string was not “used” yet.
796+
It returns a str object which can be mutated; except if *size* is zero in
797+
which case it returns the immutable empty string.
795798
796799
.. soft-deprecated:: next
797800
Use the :c:type:`PyUnicodeWriter` API instead.

‎Lib/test/test_capi/test_unicode.py‎

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
# Maximum invalid character which fits into 32-bit Py_UCS4
2626
MAX_INVALID_CHAR = 0xFFFF_FFFF
2727
NULL = None
28+
USED_STR_ERROR = 'Cannot modify a string currently used'
2829

2930
class Str(str):
3031
pass
@@ -76,9 +77,27 @@ def test_checkexact(self):
7677
# Test PyUnicode_CheckExact()
7778
self._test_check(_testlimitedcapi.unicode_checkexact, exact=True)
7879

80+
def assert_is_mutable(self, result):
81+
# Check that result is a "mutable" Unicode string
82+
self.assertEqual(sys.getrefcount(result), 1)
83+
self.assertFalse(sys._is_immortal(result))
84+
85+
def assert_is_empty_singleton(self, result):
86+
# Check that result is the empty string singleton
87+
self.assertEqual(result, '')
88+
self.assertTrue(sys._is_immortal(result))
89+
7990
def test_new(self):
8091
"""Test PyUnicode_New()"""
81-
new = _testcapi.unicode_new
92+
_unicode_new = _testcapi.unicode_new
93+
94+
def new(size, maxchar):
95+
result = _unicode_new(size, maxchar)
96+
if size != 0:
97+
self.assert_is_mutable(result)
98+
else:
99+
self.assert_is_empty_singleton(result)
100+
return result
82101

83102
for maxchar in 0, 0x61, 0xa1, 0x4f60, 0x1f600, 0x10ffff:
84103
self.assertEqual(new(0, maxchar), '')
@@ -123,6 +142,10 @@ def test_fill(self):
123142
self.assertEqual(fill(to, start, length, fill_char),
124143
(expected, filled))
125144

145+
# A string with 2 references cannot be modified
146+
with self.assertRaisesRegex(SystemError, USED_STR_ERROR):
147+
fill('abc', 0, 3, ord('x'), incref=True)
148+
126149
s = strings[0]
127150
self.assertRaises(IndexError, fill, s, -1, 0, 0x78)
128151
self.assertRaises(IndexError, fill, s, PY_SSIZE_T_MIN, 0, 0x78)
@@ -162,27 +185,41 @@ def _test_writechar(self, writechar, *, check):
162185

163186
def test_writechar(self):
164187
"""Test PyUnicode_WriteChar()"""
165-
self._test_writechar(_testlimitedcapi.unicode_writechar, check=True)
188+
writechar = _testlimitedcapi.unicode_writechar
189+
self._test_writechar(writechar, check=True)
190+
191+
# A string with 2 references cannot be modified
192+
with self.assertRaisesRegex(SystemError, USED_STR_ERROR):
193+
writechar('abc', 1, ord('x'), incref=True)
166194

167195
def test_write_macro(self):
168196
"""Test PyUnicode_WRITE()"""
169197
self._test_writechar(_testcapi.unicode_write, check=False)
170198

171199
def test_resize(self):
172200
"""Test PyUnicode_Resize()"""
173-
resize = _testlimitedcapi.unicode_resize
201+
_unicode_resize = _testlimitedcapi.unicode_resize
202+
203+
def resize(s, size):
204+
result, int_result = _unicode_resize (s, size)
205+
self.assertEqual(int_result, 0)
206+
if size != 0:
207+
self.assert_is_mutable(result)
208+
else:
209+
self.assert_is_empty_singleton(result)
210+
return result
174211

175212
strings = [
176213
# all strings have exactly 3 characters
177214
'abc', '\xa1\xa2\xa3', '\u4f60\u597d\u4e16',
178215
'\U0001f600\U0001f601\U0001f602'
179216
]
180217
for s in strings:
181-
self.assertEqual(resize(s, 3), (s, 0))
182-
self.assertEqual(resize(s, 2), (s[:2], 0))
183-
self.assertEqual(resize(s, 4), (s + '\0', 0))
184-
self.assertEqual(resize(s, 10), (s + '\0'*7, 0))
185-
self.assertEqual(resize(s, 0), ('', 0))
218+
self.assertEqual(resize(s, 3), s)
219+
self.assertEqual(resize(s, 2), s[:2])
220+
self.assertEqual(resize(s, 4), s + '\0')
221+
self.assertEqual(resize(s, 10), s + '\0'*7)
222+
self.assertEqual(resize(s, 0), '')
186223
self.assertRaises(MemoryError, resize, s, PY_SSIZE_T_MAX)
187224
self.assertRaises(SystemError, resize, s, -1)
188225
self.assertRaises(SystemError, resize, s, PY_SSIZE_T_MIN)
@@ -1746,6 +1783,11 @@ def test_copycharacters(self):
17461783
self.assertRaises(SystemError, unicode_copycharacters, s, 0, s, 0, PY_SSIZE_T_MIN)
17471784
self.assertRaises(SystemError, unicode_copycharacters, s, 0, b'', 0, 0)
17481785
self.assertRaises(SystemError, unicode_copycharacters, s, 0, [], 0, 0)
1786+
1787+
# A string with 2 references cannot be modified
1788+
with self.assertRaisesRegex(SystemError, USED_STR_ERROR):
1789+
unicode_copycharacters('abc', 0, 'abc', 0, 1, incref=True)
1790+
17491791
# CRASHES unicode_copycharacters(s, 0, NULL, 0, 0)
17501792
# TODO: Test PyUnicode_CopyCharacters() with non-unicode and
17511793
# non-modifiable unicode as "to".

‎Modules/_testcapi/unicode.c‎

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,20 @@ unicode_copy(PyObject *unicode)
5959

6060
/* Test PyUnicode_Fill() */
6161
static PyObject *
62-
unicode_fill(PyObject *self, PyObject *args)
62+
unicode_fill(PyObject *self, PyObject *args, PyObject *kwargs)
6363
{
64+
static char *kwlist[] = {"to", "start", "length", "fill_char",
65+
"incref", NULL};
6466
PyObject *to, *to_copy;
6567
Py_ssize_t start, length, filled;
6668
unsigned int fill_char;
69+
int incref = 0;
6770

68-
if (!PyArg_ParseTuple(args, "OnnI", &to, &start, &length, &fill_char)) {
71+
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
72+
"OnnI|p", kwlist,
73+
&to, &start, &length,
74+
&fill_char, &incref))
75+
{
6976
return NULL;
7077
}
7178

@@ -74,7 +81,14 @@ unicode_fill(PyObject *self, PyObject *args)
7481
return NULL;
7582
}
7683

84+
if (incref) {
85+
Py_INCREF(to_copy);
86+
}
7787
filled = PyUnicode_Fill(to_copy, start, length, (Py_UCS4)fill_char);
88+
if (incref) {
89+
Py_DECREF(to_copy);
90+
}
91+
7892
if (filled == -1 && PyErr_Occurred()) {
7993
Py_DECREF(to_copy);
8094
return NULL;
@@ -190,13 +204,18 @@ unicode_asutf8(PyObject *self, PyObject *args)
190204

191205
/* Test PyUnicode_CopyCharacters() */
192206
static PyObject *
193-
unicode_copycharacters(PyObject *self, PyObject *args)
207+
unicode_copycharacters(PyObject *self, PyObject *args, PyObject *kwargs)
194208
{
209+
static char *kwlist[] = {"to", "to_start", "from", "from_start",
210+
"howmany", "incref", NULL};
195211
PyObject *from, *to, *to_copy;
196212
Py_ssize_t from_start, to_start, how_many, copied;
213+
int incref = 0;
197214

198-
if (!PyArg_ParseTuple(args, "UnOnn", &to, &to_start,
199-
&from, &from_start, &how_many)) {
215+
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
216+
"UnOnn|p", kwlist,
217+
&to, &to_start, &from, &from_start,
218+
&how_many, &incref)) {
200219
return NULL;
201220
}
202221

@@ -210,8 +229,15 @@ unicode_copycharacters(PyObject *self, PyObject *args)
210229
return NULL;
211230
}
212231

232+
if (incref) {
233+
Py_INCREF(to_copy);
234+
}
213235
copied = PyUnicode_CopyCharacters(to_copy, to_start, from,
214236
from_start, how_many);
237+
if (incref) {
238+
Py_DECREF(to_copy);
239+
}
240+
215241
if (copied == -1 && PyErr_Occurred()) {
216242
Py_DECREF(to_copy);
217243
return NULL;
@@ -856,12 +882,12 @@ static PyType_Spec Writer_spec = {
856882

857883
static PyMethodDef TestMethods[] = {
858884
{"unicode_new", unicode_new, METH_VARARGS},
859-
{"unicode_fill", unicode_fill, METH_VARARGS},
885+
{"unicode_fill", _PyCFunction_CAST(unicode_fill), METH_VARARGS | METH_KEYWORDS},
860886
{"unicode_fromkindanddata", unicode_fromkindanddata, METH_VARARGS},
861887
{"unicode_asucs4", unicode_asucs4, METH_VARARGS},
862888
{"unicode_asucs4copy", unicode_asucs4copy, METH_VARARGS},
863889
{"unicode_asutf8", unicode_asutf8, METH_VARARGS},
864-
{"unicode_copycharacters", unicode_copycharacters, METH_VARARGS},
890+
{"unicode_copycharacters", _PyCFunction_CAST(unicode_copycharacters), METH_VARARGS | METH_KEYWORDS},
865891
{"unicode_GET_CACHED_HASH", unicode_GET_CACHED_HASH, METH_O},
866892
{"test_py_identifier", test_py_identifier, METH_NOARGS},
867893
{"corrupt_unicode", corrupt_unicode, METH_VARARGS},

‎Modules/_testlimitedcapi/unicode.c‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,19 @@ unicode_copy(PyObject *unicode)
140140

141141
/* Test PyUnicode_WriteChar() */
142142
static PyObject *
143-
unicode_writechar(PyObject *self, PyObject *args)
143+
unicode_writechar(PyObject *self, PyObject *args, PyObject *kwargs)
144144
{
145+
static char *kwlist[] = {"to", "index", "character", "incref", NULL};
145146
PyObject *to, *to_copy;
146147
Py_ssize_t index;
147148
unsigned int character;
148149
int result;
150+
int incref = 0;
149151

150-
if (!PyArg_ParseTuple(args, "OnI", &to, &index, &character)) {
152+
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
153+
"OnI|p", kwlist,
154+
&to, &index, &character, &incref))
155+
{
151156
return NULL;
152157
}
153158

@@ -156,7 +161,14 @@ unicode_writechar(PyObject *self, PyObject *args)
156161
return NULL;
157162
}
158163

164+
if (incref) {
165+
Py_INCREF(to_copy);
166+
}
159167
result = PyUnicode_WriteChar(to_copy, index, (Py_UCS4)character);
168+
if (incref) {
169+
Py_DECREF(to_copy);
170+
}
171+
160172
if (result == -1 && PyErr_Occurred()) {
161173
Py_DECREF(to_copy);
162174
return NULL;
@@ -1880,7 +1892,7 @@ static PyMethodDef TestMethods[] = {
18801892
test_unicode_compare_with_ascii, METH_NOARGS},
18811893
{"test_string_from_format", test_string_from_format, METH_NOARGS},
18821894
{"test_widechar", test_widechar, METH_NOARGS},
1883-
{"unicode_writechar", unicode_writechar, METH_VARARGS},
1895+
{"unicode_writechar", _PyCFunction_CAST(unicode_writechar), METH_VARARGS | METH_KEYWORDS},
18841896
{"unicode_resize", unicode_resize, METH_VARARGS},
18851897
{"unicode_append", unicode_append, METH_VARARGS},
18861898
{"unicode_appendanddel", unicode_appendanddel, METH_VARARGS},

0 commit comments

Comments
 (0)