Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Doc/c-api/apiabiversion.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ See :ref:`stable` for a discussion of API and ABI stability across versions.
The Python version as a string, for example, ``"3.4.1a2"``.

These macros are defined in :source:`Include/patchlevel.h`.


Run-time version
----------------
Expand Down
2 changes: 1 addition & 1 deletion Lib/plistlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ def write_bytes(self, data):
self._indent_level -= 1
maxlinelength = max(
16,
76 - len(self.indent.replace(b"\t", b" " * 8) * self._indent_level))
76 - len((self.indent * self._indent_level).expandtabs()))

for line in _encode_base64(data, maxlinelength).split(b"\n"):
if line:
Expand Down
34 changes: 34 additions & 0 deletions Lib/test/_test_atexit.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,40 @@ def __eq__(self, other):
atexit.unregister(Evil())
atexit._clear()

def test_eq_unregister(self):
# Issue #112127: callback's __eq__ may call unregister
def f1():
log.append(1)
def f2():
log.append(2)
def f3():
log.append(3)

class Pred:
def __eq__(self, other):
nonlocal cnt
cnt += 1
if cnt == when:
atexit.unregister(what)
if other is f2:
return True
return False

for what, expected in (
(f1, [3]),
(f2, [3, 1]),
(f3, [1]),
):
for when in range(1, 4):
with self.subTest(what=what.__name__, when=when):
cnt = 0
log = []
for f in (f1, f2, f3):
atexit.register(f)
atexit.unregister(Pred())
atexit._run_exitfuncs()
self.assertEqual(log, expected)


if __name__ == "__main__":
unittest.main()
19 changes: 19 additions & 0 deletions Lib/test/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,7 @@ def __hash__(self):
d.get(key2)

def test_clear_at_lookup(self):
# gh-140551 dict crash if clear is called at lookup stage
class X:
def __hash__(self):
return 1
Expand All @@ -1622,13 +1623,31 @@ def __eq__(self, other):
self.assertEqual(len(d), 1)

def test_split_table_update_with_str_subclass(self):
# gh-142218: inserting into a split table dictionary with a non str
# key that matches an existing key.
class MyStr(str): pass
class MyClass: pass
obj = MyClass()
obj.attr = 1
obj.__dict__[MyStr('attr')] = 2
self.assertEqual(obj.attr, 2)

def test_split_table_insert_with_str_subclass(self):
# gh-143189: inserting into split table dictionary with a non str
# key that matches an existing key in the shared table but not in
# the dict yet.

class MyStr(str): pass
class MyClass: pass

obj = MyClass()
obj.attr1 = 1

obj2 = MyClass()
d = obj2.__dict__
d[MyStr("attr1")] = 2
self.assertIsInstance(list(d)[0], MyStr)


class CAPITest(unittest.TestCase):

Expand Down
64 changes: 63 additions & 1 deletion Lib/test/test_plistlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,69 @@ def test_bytes(self):
data2 = plistlib.dumps(pl2)
self.assertEqual(data, data2)

def test_bytes_indent(self):
header = (
b'<?xml version="1.0" encoding="UTF-8"?>\n'
b'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n'
b'<plist version="1.0">\n')
data = [{'bytes': bytes(range(50))}]
pl = plistlib.dumps(data)
self.assertEqual(pl, header +
b'<array>\n'
b'\t<dict>\n'
b'\t\t<key>bytes</key>\n'
b'\t\t<data>\n'
b'\t\tAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKiss\n'
b'\t\tLS4vMDE=\n'
b'\t\t</data>\n'
b'\t</dict>\n'
b'</array>\n'
b'</plist>\n')

def dumps_with_indent(data, indent):
fp = BytesIO()
writer = plistlib._PlistWriter(fp, indent=indent)
writer.write(data)
return fp.getvalue()

pl = dumps_with_indent(data, b' ')
self.assertEqual(pl, header +
b'<array>\n'
b' <dict>\n'
b' <key>bytes</key>\n'
b' <data>\n'
b' AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDE=\n'
b' </data>\n'
b' </dict>\n'
b'</array>\n'
b'</plist>\n')

pl = dumps_with_indent(data, b' \t')
self.assertEqual(pl, header +
b'<array>\n'
b' \t<dict>\n'
b' \t \t<key>bytes</key>\n'
b' \t \t<data>\n'
b' \t \tAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKiss\n'
b' \t \tLS4vMDE=\n'
b' \t \t</data>\n'
b' \t</dict>\n'
b'</array>\n'
b'</plist>\n')

pl = dumps_with_indent(data, b'\t ')
self.assertEqual(pl, header +
b'<array>\n'
b'\t <dict>\n'
b'\t \t <key>bytes</key>\n'
b'\t \t <data>\n'
b'\t \t AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygp\n'
b'\t \t KissLS4vMDE=\n'
b'\t \t </data>\n'
b'\t </dict>\n'
b'</array>\n'
b'</plist>\n')

def test_loads_str_with_xml_fmt(self):
pl = self._create()
b = plistlib.dumps(pl)
Expand Down Expand Up @@ -581,7 +644,6 @@ def test_appleformatting(self):
self.assertEqual(data, TESTDATA[fmt],
"generated data was not identical to Apple's output")


def test_appleformattingfromliteral(self):
self.maxDiff = None
for fmt in ALL_FORMATS:
Expand Down
14 changes: 12 additions & 2 deletions Lib/test/test_xml_etree.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,7 @@ def test_simpleops(self):
self.serialize_check(element, '<tag key="value"><subtag /></tag>') # 4
element.remove(subelement)
self.serialize_check(element, '<tag key="value" />') # 5
with self.assertRaisesRegex(ValueError,
r'Element\.remove\(.+\): element not found'):
with self.assertRaises(ValueError):
element.remove(subelement)
self.serialize_check(element, '<tag key="value" />') # 6
element[0:0] = [subelement, subelement, subelement]
Expand Down Expand Up @@ -2758,6 +2757,17 @@ def test_pickle_issue18997(self):
self.assertEqual(e2.tag, 'group')
self.assertEqual(e2[0].tag, 'dogs')

def test_remove_errors(self):
e = ET.Element('tag')
with self.assertRaisesRegex(ValueError,
r"<Element 'subtag'.*> not in <Element 'tag'.*>"):
e.remove(ET.Element('subtag'))
with self.assertRaisesRegex(TypeError,
r".*\bElement, not type"):
e.remove(ET.Element)
with self.assertRaisesRegex(TypeError,
r".*\bElement, not int"):
e.remove(1)

class BadElementTest(ElementTestCase, unittest.TestCase):

Expand Down
7 changes: 5 additions & 2 deletions Lib/xml/etree/ElementTree.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,12 +263,15 @@ def remove(self, subelement):
ValueError is raised if a matching element could not be found.

"""
# assert iselement(element)
try:
self._children.remove(subelement)
except ValueError:
# to align the error type with the C implementation
if isinstance(subelement, type) or not iselement(subelement):
raise TypeError('expected an Element, not %s' %
type(subelement).__name__) from None
# to align the error message with the C implementation
raise ValueError("Element.remove(x): element not found") from None
raise ValueError(f"{subelement!r} not in {self!r}") from None

def find(self, path, namespaces=None):
"""Find first matching element by tag name or path.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix crash when inserting a non-:class:`str` key into a split table
dictionary when the key matches an existing key in the split table
but has no corresponding value in the dict.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Improve errors for :meth:`Element.remove
<xml.etree.ElementTree.Element.remove>`.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix concurrent and reentrant call of :func:`atexit.unregister`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix incorrect wrapping of the Base64 data in :class:`!plistlib._PlistWriter`
when the indent contains a mix of tabs and spaces.
3 changes: 1 addition & 2 deletions Modules/_elementtree.c
Original file line number Diff line number Diff line change
Expand Up @@ -1679,8 +1679,7 @@ _elementtree_Element_remove_impl(ElementObject *self, PyObject *subelement)
}

if (rc == 0) {
PyErr_SetString(PyExc_ValueError,
"Element.remove(x): element not found");
PyErr_Format(PyExc_ValueError, "%R not in %R", subelement, self);
return NULL;
}

Expand Down
28 changes: 21 additions & 7 deletions Modules/atexitmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -256,22 +256,36 @@ atexit_ncallbacks(PyObject *module, PyObject *Py_UNUSED(dummy))
static int
atexit_unregister_locked(PyObject *callbacks, PyObject *func)
{
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(callbacks); ++i) {
for (Py_ssize_t i = PyList_GET_SIZE(callbacks) - 1; i >= 0; --i) {
PyObject *tuple = Py_NewRef(PyList_GET_ITEM(callbacks, i));
assert(PyTuple_CheckExact(tuple));
PyObject *to_compare = PyTuple_GET_ITEM(tuple, 0);
int cmp = PyObject_RichCompareBool(func, to_compare, Py_EQ);
Py_DECREF(tuple);
if (cmp < 0)
{
if (cmp < 0) {
Py_DECREF(tuple);
return -1;
}
if (cmp == 1) {
// We found a callback!
if (PyList_SetSlice(callbacks, i, i + 1, NULL) < 0) {
return -1;
// But its index could have changed if it or other callbacks were
// unregistered during the comparison.
Py_ssize_t j = PyList_GET_SIZE(callbacks) - 1;
j = Py_MIN(j, i);
for (; j >= 0; --j) {
if (PyList_GET_ITEM(callbacks, j) == tuple) {
// We found the callback index! For real!
if (PyList_SetSlice(callbacks, j, j + 1, NULL) < 0) {
Py_DECREF(tuple);
return -1;
}
i = j;
break;
}
}
--i;
}
Py_DECREF(tuple);
if (i >= PyList_GET_SIZE(callbacks)) {
i = PyList_GET_SIZE(callbacks);
}
}

Expand Down
7 changes: 5 additions & 2 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1877,7 +1877,7 @@ static int
insertdict(PyDictObject *mp,
PyObject *key, Py_hash_t hash, PyObject *value)
{
PyObject *old_value;
PyObject *old_value = NULL;
Py_ssize_t ix;

ASSERT_DICT_LOCKED(mp);
Expand All @@ -1898,11 +1898,14 @@ insertdict(PyDictObject *mp,
goto Fail;
}

if (ix == DKIX_EMPTY) {
if (old_value == NULL) {
// insert_combined_dict() will convert from non DICT_KEYS_GENERAL table
// into DICT_KEYS_GENERAL table if key is not Unicode.
// We don't convert it before _Py_dict_lookup because non-Unicode key
// may change generic table into Unicode table.
//
// NOTE: ix may not be DKIX_EMPTY because split table may have key
// without value.
if (insert_combined_dict(mp, hash, key, value) < 0) {
goto Fail;
}
Expand Down
Loading