Skip to content

Commit 4c18bca

Browse files
authored
Merge branch 'main' into stats_cospi_sinpi
2 parents c4a0eea + 1a703ab commit 4c18bca

17 files changed

Lines changed: 276 additions & 86 deletions

Doc/c-api/bytes.rst

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ called with a non-bytes parameter.
231231
Resize a bytes object. *newsize* will be the new length of the bytes object.
232232
You can think of it as creating a new bytes object and destroying the old
233233
one, only more efficiently.
234+
234235
Pass the address of an
235236
existing bytes object as an lvalue (it may be written into), and the new size
236237
desired. On success, *\*bytes* holds the resized bytes object and ``0`` is
@@ -239,6 +240,11 @@ called with a non-bytes parameter.
239240
*\*bytes* is set to ``NULL``, :exc:`MemoryError` is set, and ``-1`` is
240241
returned.
241242
243+
While bytes objects are usually immutable in Python, this special C API
244+
allows mutating a bytes object in-place. The returned bytes object can still
245+
be mutated using :c:func:`PyBytesWriter_GetData`; except if *newsize* is
246+
zero in which case it returns the immutable empty bytes string.
247+
242248
.. soft-deprecated:: 3.15
243249
Use the :c:type:`PyBytesWriter` API instead.
244250
@@ -290,10 +296,10 @@ object.
290296
291297
.. c:type:: PyBytesWriter
292298
293-
A bytes writer instance.
299+
A bytes writer object.
294300
295-
The API is **not thread safe**: a writer should only be used by a single
296-
thread at the same time.
301+
The API is **not thread safe**. A :c:type:`PyBytesWriter` object must only
302+
be used by a single thread, it must not be shared between threads.
297303
298304
The instance must be destroyed by :c:func:`PyBytesWriter_Finish` on
299305
success, or :c:func:`PyBytesWriter_Discard` on error.
@@ -429,7 +435,7 @@ Low-level API
429435
On success, return ``0``.
430436
On error, set an exception and return ``-1``.
431437
432-
*size* can be negative to shrink the writer.
438+
*grow* can be negative to shrink the writer.
433439
434440
.. c:function:: void* PyBytesWriter_GrowAndUpdatePointer(PyBytesWriter *writer, Py_ssize_t size, void *buf)
435441

Include/internal/pycore_bytesobject.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ PyAPI_FUNC(PyObject *) _PyBytes_Repeat(PyObject *self, Py_ssize_t n);
7777

7878
extern int _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize);
7979

80+
#ifndef NDEBUG
81+
extern int _PyBytes_IsMutable(PyObject *obj);
82+
#endif
83+
8084
/* --- PyBytesWriter ------------------------------------------------------ */
8185

8286
struct PyBytesWriter {

Lib/test/test_annotationlib.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1869,7 +1869,7 @@ def nested():
18691869
self.assertEqual(type_repr(t'''{ 0
18701870
& 1
18711871
| 2
1872-
}'''), 't"""{ 0\n & 1\n | 2}"""')
1872+
}'''), 't"""{ 0\n & 1\n | 2\n }"""')
18731873
self.assertEqual(
18741874
type_repr(Template("hi", Interpolation(42, "42"))), "t'hi{42}'"
18751875
)

Lib/test/test_bytes.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,13 +1130,14 @@ def test_translate(self):
11301130
self.assertRaises(ValueError, b.translate, bytes(range(255)))
11311131

11321132
c = b.translate(rosetta, b'hello')
1133-
self.assertEqual(b, b'hello')
1134-
self.assertIsInstance(c, self.type2test)
1133+
self.assertEqual(c, b'')
1134+
self.assertEqual(type(c), self.type2test)
11351135

11361136
c = b.translate(rosetta)
11371137
d = b.translate(rosetta, b'')
1138-
self.assertEqual(c, d)
11391138
self.assertEqual(c, b'helle')
1139+
self.assertEqual(type(c), self.type2test)
1140+
self.assertEqual(d, b'helle')
11401141

11411142
c = b.translate(rosetta, b'l')
11421143
self.assertEqual(c, b'hee')

Lib/test/test_capi/test_bytes.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,24 @@ def test_grow(self):
461461
writer.grow(0) # noop
462462
self.assertEqual(writer.finish(), b'number=123')
463463

464+
for size in (self.SMALL_BUFFER, self.LARGE_BUFFER):
465+
with self.subTest(size=size):
466+
# Truncate the last byte
467+
data = b'x' * size
468+
writer = self.create_writer(size)
469+
writer.write(0, data)
470+
self.assertEqual(writer.get_data(), data)
471+
writer.grow(-1)
472+
self.assertEqual(writer.get_data(), data[:-1])
473+
self.assertEqual(writer.finish(), data[:-1])
474+
475+
# Make the buffer empty
476+
writer = self.create_writer(size)
477+
writer.write(0, data)
478+
writer.grow(-size)
479+
self.assertEqual(writer.get_data(), b'')
480+
self.assertEqual(writer.finish(), b'')
481+
464482
# Switch from small buffer to large buffer
465483
writer = self.create_writer()
466484
small, large = self.SMALL_BUFFER, self.LARGE_BUFFER
@@ -476,8 +494,8 @@ def test_grow(self):
476494
with self.subTest(size=size):
477495
writer = self.create_writer()
478496
writer.write_bytes(b'x' * size, -1)
479-
with self.assertRaisesRegex(ValueError, 'size must be >= 0'):
480-
writer.grow(-1)
497+
with self.assertRaisesRegex(ValueError, 'invalid size'):
498+
writer.grow(-size - 1)
481499
with self.assertRaises(MemoryError):
482500
writer.grow(_testcapi.PY_SSIZE_T_MAX)
483501
self.assertEqual(writer.finish(), b'x' * size)

Lib/test/test_fstring.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1695,6 +1695,14 @@ def __repr__(self):
16951695
self.assertEqual(f'''{f"{d["a#b"]}"=}''',
16961696
'f"{d["a#b"]}"=\'42\'')
16971697

1698+
result = f'''{(
1699+
1, # Force lexer metadata reconstruction.
1700+
"\"#")=}'''
1701+
self.assertEqual(
1702+
result,
1703+
'(\n 1, \n "\\"#")=(1, \'"#\')',
1704+
)
1705+
16981706
self.assertEqual(f'{ # some comment goes here
16991707
"""hello"""=}', ' \n """hello"""=\'hello\'')
17001708
self.assertEqual(f'{"""# this is not a comment

Lib/test/test_io/test_memoryio.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import unittest
77
from test import support
8+
from test.support import import_helper
89

910
import gc
1011
import io
@@ -753,6 +754,35 @@ def __buffer__(self, flags):
753754
self.assertEqual(memio.getvalue(), b"01AAA56789")
754755
self.assertEqual(memio.tell(), 5)
755756

757+
def test_memory_error(self):
758+
# gh-157242: io.BytesIO() must not close the file on MemoryError
759+
_testcapi = import_helper.import_module('_testcapi')
760+
761+
# write()
762+
stream = self.ioclass()
763+
stream.write(self.buftype('abc'))
764+
with self.assertRaises(MemoryError):
765+
try:
766+
data = self.buftype('def')
767+
_testcapi.set_nomemory(0)
768+
stream.write(data)
769+
finally:
770+
_testcapi.remove_mem_hooks()
771+
stream.write(self.buftype('123'))
772+
self.assertEqual(stream.getvalue(), self.buftype('abc123'))
773+
774+
# truncate()
775+
data = self.buftype('x' * 100)
776+
stream = self.ioclass()
777+
stream.write(data)
778+
with self.assertRaises(MemoryError):
779+
try:
780+
_testcapi.set_nomemory(0)
781+
stream.truncate(5)
782+
finally:
783+
_testcapi.remove_mem_hooks()
784+
self.assertEqual(stream.getvalue(), data)
785+
756786

757787
class TextIOTestMixin:
758788

Lib/test/test_tstring.py

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,47 @@ def test_debug_specifier(self):
136136
# Test white space in debug specifier
137137
t = t"Value: {value = }"
138138
self.assertTStringEqual(
139-
t, ("Value: value = ", ""), [(value, "value", "r")]
139+
t, ("Value: value = ", ""), [(value, "value ", "r")]
140140
)
141141
self.assertEqual(fstring(t), "Value: value = 42")
142142

143+
# Explicit line continuations after the debug marker are part of
144+
# the debug text, not the interpolation expression.
145+
for template, strings, interpolation, rendered in (
146+
(
147+
t"""Value: {value =\
148+
}""",
149+
("Value: value =\\\n", ""),
150+
(value, "value ", "r"),
151+
"Value: value =\\\n42",
152+
),
153+
(
154+
t"""Value: {value =\
155+
!r}""",
156+
("Value: value =\\\n", ""),
157+
(value, "value ", "r"),
158+
"Value: value =\\\n42",
159+
),
160+
(
161+
t"""Value: {value =\
162+
:04}""",
163+
("Value: value =\\\n", ""),
164+
(value, "value ", None, "04"),
165+
"Value: value =\\\n0042",
166+
),
167+
(
168+
t"""Value: {value =\
169+
\
170+
}""",
171+
("Value: value =\\\n\\\n", ""),
172+
(value, "value ", "r"),
173+
"Value: value =\\\n\\\n42",
174+
),
175+
):
176+
with self.subTest(template=template):
177+
self.assertTStringEqual(template, strings, [interpolation])
178+
self.assertEqual(fstring(template), rendered)
179+
143180
class C:
144181
def __format__(self, spec):
145182
return f"FORMAT-{spec}"
@@ -149,6 +186,41 @@ def __format__(self, spec):
149186
self.assertEqual(t.interpolations[0].format_spec,
150187
"FORMAT-value=42")
151188

189+
def test_interpolation_expression_whitespace(self):
190+
x = 42
191+
for template, expected in (
192+
(t"{x}", "x"),
193+
(t"{x }", "x "),
194+
(t"{ x}", " x"),
195+
(t"{ x }", " x "),
196+
(t"{ x }", " x "),
197+
(t"""{
198+
x
199+
}""", "\n x\n"),
200+
(t"{ x !r}", " x "),
201+
(t"{ x :.2f}", " x "),
202+
(t"{ x = }", " x "),
203+
(t"{ x = !r}", " x "),
204+
(t"{ x = :.2f}", " x "),
205+
(t"{x == 42 = }", "x == 42 "),
206+
):
207+
with self.subTest(template=template):
208+
self.assertEqual(
209+
template.interpolations[0].expression,
210+
expected,
211+
)
212+
213+
def test_interpolation_expression_with_reconstructed_metadata(self):
214+
regular = t'''{(
215+
1, # Force lexer metadata reconstruction.
216+
"\"#")}'''
217+
debug = t'''{(
218+
1, # Force lexer metadata reconstruction.
219+
"\"#")=}'''
220+
expected = '(\n 1, \n "\\"#")'
221+
self.assertEqual(regular.interpolations[0].expression, expected)
222+
self.assertEqual(debug.interpolations[0].expression, expected)
223+
152224
def test_raw_tstrings(self):
153225
path = r"C:\Users"
154226
t = rt"{path}\Documents"
@@ -314,12 +386,12 @@ def test_triple_quoted(self):
314386

315387
t = t'{"""a""""#" # outside
316388
}'
317-
self.assertEqual(t.interpolations[0].expression, '"""a""""#"')
389+
self.assertEqual(t.interpolations[0].expression, '"""a""""#" \n')
318390

319391
x, y = 1, 2
320392
t = t'{x != y # outside
321393
}'
322-
self.assertEqual(t.interpolations[0].expression, 'x != y')
394+
self.assertEqual(t.interpolations[0].expression, 'x != y \n')
323395

324396
d = {'a#b': 42}
325397
t = t'''{f"{d["a#b"]}"}'''

Lib/test/test_unparse.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,15 @@ def test_tstrings(self):
216216
self.check_ast_roundtrip('t""')
217217
self.check_ast_roundtrip("t'{(lambda x: x)}'")
218218
self.check_ast_roundtrip("t'{t'{x}'}'")
219+
self.check_ast_roundtrip(
220+
r"""t'''{(
221+
1, # Force lexer metadata reconstruction.
222+
"\"#")}'''"""
223+
)
224+
self.check_ast_roundtrip(
225+
r'''t"""Value: {value =\
226+
}"""'''
227+
)
219228

220229
def test_tstring_with_nonsensical_str_field(self):
221230
# `value` suggests that the original code is `t'{test1}`, but `str` suggests otherwise
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Trailing whitespace in a t-string interpolation expression is now preserved
2+
in :attr:`string.templatelib.Interpolation.expression`, up to the closing ``}``
3+
or the conversion (``!``), format (``:``), or debug (``=``) delimiter.
4+
Explicit line continuations following a debug ``=`` remain part of the debug
5+
text and are excluded from :attr:`~string.templatelib.Interpolation.expression`.

0 commit comments

Comments
 (0)