summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorSerhiy Storchaka <storchaka@gmail.com>2024-07-25 08:45:19 (GMT)
committerGitHub <noreply@github.com>2024-07-25 08:45:19 (GMT)
commitdc07f65a53baf60d9857186294d3d7ba92d5606d (patch)
treead5e883c9e083b1f6af015b1fb2829a04abfad96 /Lib
parentca0f7c447c83503bd760dc2eb6d1ea4b3558f8e9 (diff)
downloadcpython-dc07f65a53baf60d9857186294d3d7ba92d5606d.zip
cpython-dc07f65a53baf60d9857186294d3d7ba92d5606d.tar.gz
cpython-dc07f65a53baf60d9857186294d3d7ba92d5606d.tar.bz2
gh-82951: Fix serializing by name in pickle protocols < 4 (GH-122149)
Serializing objects with complex __qualname__ (such as unbound methods and nested classes) by name no longer involves serializing parent objects by value in pickle protocols < 4.
Diffstat (limited to 'Lib')
-rw-r--r--Lib/pickle.py40
-rw-r--r--Lib/test/pickletester.py12
2 files changed, 43 insertions, 9 deletions
diff --git a/Lib/pickle.py b/Lib/pickle.py
index 115bd89..2d76498 100644
--- a/Lib/pickle.py
+++ b/Lib/pickle.py
@@ -1110,11 +1110,35 @@ class _Pickler:
self.save(module_name)
self.save(name)
write(STACK_GLOBAL)
- elif parent is not module:
- self.save_reduce(getattr, (parent, lastname))
- elif self.proto >= 3:
- write(GLOBAL + bytes(module_name, "utf-8") + b'\n' +
- bytes(name, "utf-8") + b'\n')
+ elif '.' in name:
+ # In protocol < 4, objects with multi-part __qualname__
+ # are represented as
+ # getattr(getattr(..., attrname1), attrname2).
+ dotted_path = name.split('.')
+ name = dotted_path.pop(0)
+ save = self.save
+ for attrname in dotted_path:
+ save(getattr)
+ if self.proto < 2:
+ write(MARK)
+ self._save_toplevel_by_name(module_name, name)
+ for attrname in dotted_path:
+ save(attrname)
+ if self.proto < 2:
+ write(TUPLE)
+ else:
+ write(TUPLE2)
+ write(REDUCE)
+ else:
+ self._save_toplevel_by_name(module_name, name)
+
+ self.memoize(obj)
+
+ def _save_toplevel_by_name(self, module_name, name):
+ if self.proto >= 3:
+ # Non-ASCII identifiers are supported only with protocols >= 3.
+ self.write(GLOBAL + bytes(module_name, "utf-8") + b'\n' +
+ bytes(name, "utf-8") + b'\n')
else:
if self.fix_imports:
r_name_mapping = _compat_pickle.REVERSE_NAME_MAPPING
@@ -1124,15 +1148,13 @@ class _Pickler:
elif module_name in r_import_mapping:
module_name = r_import_mapping[module_name]
try:
- write(GLOBAL + bytes(module_name, "ascii") + b'\n' +
- bytes(name, "ascii") + b'\n')
+ self.write(GLOBAL + bytes(module_name, "ascii") + b'\n' +
+ bytes(name, "ascii") + b'\n')
except UnicodeEncodeError:
raise PicklingError(
"can't pickle global identifier '%s.%s' using "
"pickle protocol %i" % (module, name, self.proto)) from None
- self.memoize(obj)
-
def save_type(self, obj):
if obj is type(None):
return self.save_reduce(type, (None,), obj=obj)
diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py
index 9922591..1366322 100644
--- a/Lib/test/pickletester.py
+++ b/Lib/test/pickletester.py
@@ -2818,6 +2818,18 @@ class AbstractPickleTests:
self.assertIs(unpickled, Recursive)
del Recursive.mod # break reference loop
+ def test_recursive_nested_names2(self):
+ global Recursive
+ class Recursive:
+ pass
+ Recursive.ref = Recursive
+ Recursive.__qualname__ = 'Recursive.ref'
+ for proto in range(pickle.HIGHEST_PROTOCOL + 1):
+ with self.subTest(proto=proto):
+ unpickled = self.loads(self.dumps(Recursive, proto))
+ self.assertIs(unpickled, Recursive)
+ del Recursive.ref # break reference loop
+
def test_py_methods(self):
global PyMethodsTest
class PyMethodsTest: