Skip to content

Commit 05510fb

Browse files
authored
Merge branch 'main' into gh-157364
2 parents 979a884 + 575fe39 commit 05510fb

52 files changed

Lines changed: 1446 additions & 786 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ Lib/test/test_unittest/testmock/ @cjw296
630630
Doc/library/zlib.rst @StanFromIreland
631631
Lib/compression/zlib.py @StanFromIreland
632632
Lib/test/test_zlib.py @StanFromIreland
633-
Modules/_zlibmodule.c @StanFromIreland
633+
Modules/zlibmodule.c @StanFromIreland
634634

635635
# Zipfile.Path
636636
Lib/test/test_zipfile/_path/ @jaraco

.github/workflows/reusable-docs.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ jobs:
8686
--fail-if-regression \
8787
--fail-if-improved \
8888
--fail-if-new-news-nit
89+
- name: 'Build list of changes'
90+
run: |
91+
make -C Doc/ PYTHON=../python changes
8992
- name: 'Collect HTML IDs'
9093
if: github.event_name == 'pull_request'
9194
run: python Doc/tools/check-html-ids.py collect Doc/build/html -o Doc/build/html-ids-head.json.gz

Doc/tools/extensions/changes.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from docutils import nodes
88
from sphinx import addnodes
9+
from sphinx.builders.changes import ChangesBuilder
910
from sphinx.domains.changeset import (
1011
VersionChange,
1112
versionlabel_classes,
@@ -17,6 +18,7 @@
1718
if TYPE_CHECKING:
1819
from docutils.nodes import Node
1920
from sphinx.application import Sphinx
21+
from sphinx.environment import BuildEnvironment
2022
from sphinx.util.typing import ExtensionMetadata
2123

2224

@@ -146,6 +148,32 @@ def _add_glossary_link(cls, inline: nodes.inline) -> None:
146148
break
147149

148150

151+
def _fixup_changesets(app: Sphinx, env: BuildEnvironment) -> None:
152+
changesets = env.get_domain("changeset").changesets
153+
154+
# The changeset domain records each entry's plain text before SoftDeprecated
155+
# replaces the :term:, so strip the markup before the changes builder renders it.
156+
for entries in changesets.values():
157+
for i, entry in enumerate(entries):
158+
if entry.type == "soft-deprecated":
159+
entries[i] = entry._replace(
160+
content=SoftDeprecated._TERM_RE.sub(r"\1", entry.content)
161+
)
162+
163+
# DeprecatedRemoved entries are recorded under their (deprecated,
164+
# removed) version tuple, which the changes builder ignores.
165+
# Re-file them under both versions.
166+
for versions in [v for v in changesets if isinstance(v, tuple)]:
167+
deprecated, removed = versions
168+
for entry in changesets.pop(versions):
169+
changesets.setdefault(deprecated, []).append(
170+
entry._replace(type="deprecated")
171+
)
172+
changesets.setdefault(removed, []).append(
173+
entry._replace(type="versionremoved")
174+
)
175+
176+
149177
def setup(app: Sphinx) -> ExtensionMetadata:
150178
# Override Sphinx's directives with support for 'next'
151179
app.add_directive("versionadded", PyVersionChange, override=True)
@@ -155,9 +183,15 @@ def setup(app: Sphinx) -> ExtensionMetadata:
155183

156184
# Register the ``.. deprecated-removed::`` directive
157185
app.add_directive("deprecated-removed", DeprecatedRemoved)
186+
# _fixup_changesets() changes these entries to 'deprecated'/'versionremoved'
187+
ChangesBuilder.typemap["deprecated-removed"] = "deprecated-removed"
158188

159189
# Register the ``.. soft-deprecated::`` directive
160190
app.add_directive("soft-deprecated", SoftDeprecated)
191+
ChangesBuilder.typemap["soft-deprecated"] = "soft deprecated"
192+
193+
# Repair the recorded changesets for the couple of custom directives above
194+
app.connect("env-updated", _fixup_changesets)
161195

162196
return {
163197
"version": "1.0",

Include/internal/pycore_bytesobject.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ PyAPI_FUNC(PyObject *) _PyBytes_Repeat(PyObject *self, Py_ssize_t n);
7575
*/
7676
#define _PyBytesObject_SIZE (offsetof(PyBytesObject, ob_sval) + 1)
7777

78+
extern int _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize);
79+
7880
/* --- PyBytesWriter ------------------------------------------------------ */
7981

8082
struct PyBytesWriter {

Lib/test/support/__init__.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,21 +1364,16 @@ def wrapper(self):
13641364
return wrapper
13651365
return decorator
13661366

1367-
def nomemtest(f):
1367+
def nomemtest(test):
13681368
"""Check that we can use this test with `_testcapi.set_nomemory`."""
13691369
from .import_helper import import_module
13701370

1371-
@functools.wraps(f)
1371+
@functools.wraps(test)
13721372
def internal(*args, **kwargs):
13731373
import_module('_testcapi')
1374-
return f(*args, **kwargs)
1374+
return test(*args, **kwargs)
13751375

1376-
return unittest.skipIf(
1377-
# Python built with Py_TRACE_REFS fail with a fatal error in
1378-
# _PyRefchain_Trace() on memory allocation error.
1379-
Py_TRACE_REFS,
1380-
'cannot test Py_TRACE_REFS build',
1381-
)(cpython_only(internal))
1376+
return cpython_only(internal)
13821377

13831378
def bigaddrspacetest(f):
13841379
"""Decorator for tests that fill the address space."""

Lib/test/test_bytes.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import array
8+
import contextlib
89
import operator
910
import os
1011
import re
@@ -48,6 +49,19 @@ def __index__(self):
4849
return self.value
4950

5051

52+
@contextlib.contextmanager
53+
def inject_memory_error(testcase, start):
54+
# Raise SkipTest if _testcapi extension module is missing
55+
_testcapi = import_helper.import_module('_testcapi')
56+
57+
with testcase.assertRaises(MemoryError):
58+
try:
59+
_testcapi.set_nomemory(start)
60+
yield
61+
finally:
62+
_testcapi.remove_mem_hooks()
63+
64+
5165
class BaseBytesTest:
5266

5367
def assertTypedEqual(self, actual, expected):
@@ -1116,13 +1130,14 @@ def test_translate(self):
11161130
self.assertRaises(ValueError, b.translate, bytes(range(255)))
11171131

11181132
c = b.translate(rosetta, b'hello')
1119-
self.assertEqual(b, b'hello')
1120-
self.assertIsInstance(c, self.type2test)
1133+
self.assertEqual(c, b'')
1134+
self.assertEqual(type(c), self.type2test)
11211135

11221136
c = b.translate(rosetta)
11231137
d = b.translate(rosetta, b'')
1124-
self.assertEqual(c, d)
11251138
self.assertEqual(c, b'helle')
1139+
self.assertEqual(type(c), self.type2test)
1140+
self.assertEqual(d, b'helle')
11261141

11271142
c = b.translate(rosetta, b'l')
11281143
self.assertEqual(c, b'hee')
@@ -1555,6 +1570,36 @@ def test_resize(self):
15551570
self.assertRaises(MemoryError, bytearray().resize, sys.maxsize)
15561571
self.assertRaises(MemoryError, bytearray(1000).resize, sys.maxsize)
15571572

1573+
@support.nomemtest
1574+
def test_resize_error(self):
1575+
# gh-157242: If bytearray.resize() fails (MemoryError),
1576+
# the bytearray must be left unchanged.
1577+
1578+
offset = 3
1579+
for logical_offset in (False, True):
1580+
with self.subTest(logical_offset=logical_offset):
1581+
# grow bytearray
1582+
ba = bytearray(b'0123456789')
1583+
if logical_offset:
1584+
expected = ba[offset:]
1585+
del ba[:offset]
1586+
else:
1587+
expected = ba.copy()
1588+
with inject_memory_error(self, 0):
1589+
ba.resize(1024)
1590+
self.assertEqual(ba, expected)
1591+
1592+
# shrink bytearray
1593+
ba = bytearray(b'0123456789')
1594+
if logical_offset:
1595+
expected = ba[offset:]
1596+
del ba[:offset]
1597+
else:
1598+
expected = ba.copy()
1599+
with inject_memory_error(self, 0):
1600+
ba.resize(1)
1601+
self.assertEqual(ba, expected)
1602+
15581603
def test_take_bytes(self):
15591604
ba = bytearray(b'ab')
15601605
self.assertEqual(ba.take_bytes(), b'ab')
@@ -1619,6 +1664,29 @@ def test_take_bytes(self):
16191664
self.assertEqual(ba, bytearray(b'A'))
16201665
self.assertEqual(ord(b'c'), ord('c'))
16211666

1667+
@support.nomemtest
1668+
def test_take_bytes_error(self):
1669+
# gh-157242: If bytearray.take_bytes() fails (MemoryError),
1670+
# the bytearray must be left unchanged.
1671+
1672+
for logical_offset, to_take, mem_errors in (
1673+
(True, 5, (0, 1)),
1674+
(False, 5, (0, 1)),
1675+
(True, None, (0,)),
1676+
):
1677+
for mem_error in mem_errors:
1678+
with self.subTest(logical_offset=logical_offset,
1679+
to_take=to_take, mem_error=mem_error):
1680+
ba = bytearray(b'0123456789')
1681+
if logical_offset:
1682+
expected = ba[3:]
1683+
del ba[:3]
1684+
else:
1685+
expected = ba.copy()
1686+
with inject_memory_error(self, mem_error):
1687+
ba.take_bytes(to_take)
1688+
self.assertEqual(ba, expected)
1689+
16221690
@support.cpython_only # tests an implementation detail
16231691
def test_take_bytes_optimization(self):
16241692
# Validate optimization around taking lots of little chunks out of a

0 commit comments

Comments
 (0)