Skip to content
Open
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
89 changes: 66 additions & 23 deletions Lib/test/test_marshal.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,16 @@ def test_no_allow_code(self):
data = {'a': [({co, 0},)]}
dump = marshal.dumps(data, allow_code=True)
self.assertEqual(marshal.loads(dump, allow_code=True), data)
with self.assertRaises(ValueError):
with self.assertRaisesRegex(ValueError,
'marshalling code objects is disallowed'):
marshal.dumps(data, allow_code=False)
with self.assertRaises(ValueError):
marshal.loads(dump, allow_code=False)

marshal.dump(data, io.BytesIO(), allow_code=True)
self.assertEqual(marshal.load(io.BytesIO(dump), allow_code=True), data)
with self.assertRaises(ValueError):
with self.assertRaisesRegex(ValueError,
'marshalling code objects is disallowed'):
marshal.dump(data, io.BytesIO(), allow_code=False)
with self.assertRaises(ValueError):
marshal.load(io.BytesIO(dump), allow_code=False)
Expand Down Expand Up @@ -339,16 +341,29 @@ def test_reference_loop_dict(self):
self.assertIsInstance(b, dict)
self.assertIs(b[None], b)

def check_reference_loop(self, a, typename, minversion,
oldmsg='object too deeply nested to marshal'):
# Only versions supporting references to the type detect the loop;
# older versions fail for a different reason.
for v in range(minversion):
with self.subTest(version=v):
with self.assertRaisesRegex(ValueError, oldmsg):
marshal.dumps(a, v)
for v in range(minversion, marshal.version + 1):
with self.subTest(version=v):
with self.assertRaisesRegex(
ValueError,
f'cannot marshal recursion {typename} objects'):
marshal.dumps(a, v)

def test_reference_loop_tuple(self):
a = ([],)
a[0].append(a)
for v in range(marshal.version + 1):
self.assertRaises(ValueError, marshal.dumps, a, v)
self.check_reference_loop(a, 'tuple', 3)

a = ({},)
a[0][None] = a
for v in range(marshal.version + 1):
self.assertRaises(ValueError, marshal.dumps, a, v)
self.check_reference_loop(a, 'tuple', 3)

def test_shared_reference_tuple(self):
# A tuple referenced more than once still round-trips with the
Expand All @@ -373,30 +388,28 @@ def f():
# so we need to break the loop manually. See gh-148722.
self.addCleanup(a.clear)
a.append(code)
for v in range(marshal.version + 1):
self.assertRaises(ValueError, marshal.dumps, code, v)
self.check_reference_loop(code, 'code', 3)

def test_reference_loop_slice(self):
oldmsg = 'marshalling slice objects requires version 5 or higher'
a = slice([], None)
a.start.append(a)
for v in range(marshal.version + 1):
self.assertRaises(ValueError, marshal.dumps, a, v)
self.check_reference_loop(a, 'slice', 5, oldmsg)

a = slice(None, [])
a.stop.append(a)
for v in range(marshal.version + 1):
self.assertRaises(ValueError, marshal.dumps, a, v)
self.check_reference_loop(a, 'slice', 5, oldmsg)

a = slice(None, None, [])
a.step.append(a)
for v in range(marshal.version + 1):
self.assertRaises(ValueError, marshal.dumps, a, v)
self.check_reference_loop(a, 'slice', 5, oldmsg)

def test_reference_loop_frozendict(self):
a = frozendict({None: []})
a[None].append(a)
for v in range(marshal.version + 1):
self.assertRaises(ValueError, marshal.dumps, a, v)
self.check_reference_loop(
a, 'frozendict', 6,
'marshalling frozendict objects requires version 6 or higher')

def test_shared_reference_frozendict(self):
# A frozendict referenced more than once must round-trip with the
Expand Down Expand Up @@ -467,7 +480,9 @@ def test_exact_type_match(self):
# Note: str subclasses are not tested because they get handled
# by marshal's routines for objects supporting the buffer API.
subtyp = type('subtyp', (typ,), {})
self.assertRaises(ValueError, marshal.dumps, subtyp())
with self.assertRaisesRegex(ValueError,
r'cannot marshal \S*subtyp objects'):
marshal.dumps(subtyp())

# Issue #1792 introduced a change in how marshal increases the size of its
# internal buffer; this test ensures that the new code is exercised.
Expand Down Expand Up @@ -570,9 +585,25 @@ def test_unmarshallable(self):
('code', code))
for name, arg in cases:
with self.subTest(name, arg=arg):
with self.assertRaisesRegex(ValueError, "unmarshallable object"):
with self.assertRaisesRegex(ValueError,
"cannot marshal type objects"):
marshal.dumps((arg, memoryview(b'')))

def test_error_in_set_item(self):
# Set items are sorted by their marshalled representation, and NaNs
# are only distinguished by identity, so they are compared as
# complex numbers.
nan = float('nan')
with self.assertRaisesRegex(TypeError, "'<' not supported"):
marshal.dumps({complex(nan, 0), complex(nan, 0)})

def test_error_in_buffer(self):
# The BufferError raised for a non-contiguous buffer is not replaced
# with a generic error.
step2 = slice(None, None, 2)
with self.assertRaises(BufferError):
marshal.dumps(memoryview(bytearray(b'abcdef'))[step2])


LARGE_SIZE = 2**31
pointer_size = 8 if sys.maxsize > 0xFFFFFFFF else 4
Expand All @@ -583,8 +614,14 @@ def write(self, s):

@unittest.skipIf(LARGE_SIZE > sys.maxsize, "test cannot run on 32-bit systems")
class LargeValuesTestCase(unittest.TestCase):
def check_unmarshallable(self, data):
self.assertRaises(ValueError, marshal.dump, data, NullWriter())
def check_unmarshallable(self, data, msg='object too large to marshal'):
with self.assertRaisesRegex(ValueError, msg):
marshal.dump(data, NullWriter())

@support.bigmemtest(size=LARGE_SIZE, memuse=4, dry_run=False)
def test_int(self, size):
# An int with more than SIZE32_MAX 15-bit digits.
self.check_unmarshallable(1 << (15 * size), 'int too large to marshal')

@support.bigmemtest(size=LARGE_SIZE, memuse=2, dry_run=False)
def test_bytes(self, size):
Expand Down Expand Up @@ -717,7 +754,10 @@ def testFrozenDict(self):
self.helper(dictobj)

for version in range(6):
with self.assertRaises(ValueError):
with self.assertRaisesRegex(
ValueError,
'marshalling frozendict objects requires '
'version 6 or higher'):
marshal.dumps(dictobj, version)

def testModule(self):
Expand Down Expand Up @@ -786,7 +826,10 @@ def test_slice(self):
self.helper(obj)

for version in range(5):
with self.assertRaises(ValueError):
with self.assertRaisesRegex(
ValueError,
'marshalling slice objects requires '
'version 5 or higher'):
marshal.dumps(obj, version)

@support.cpython_only
Expand Down Expand Up @@ -818,7 +861,7 @@ def test_write_to_file_error(self):

def test_write_unmarshallable_to_file(self):
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
with self.assertRaisesRegex(ValueError, 'unmarshallable object'):
with self.assertRaisesRegex(ValueError, 'cannot marshal object objects'):
_testcapi.pymarshal_write_object_to_file(object(), os_helper.TESTFN,
marshal.version)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:mod:`marshal` no longer replaces the exception raised while marshalling the
value with a generic ``ValueError("unmarshallable object")``. Error messages
now name the type of the unsupported object and, for the types supported only
by newer data formats, the required version.
Loading
Loading