Skip to content

Commit f39262d

Browse files
gh-156942: Raise the exception where the marshalling error is detected
Previously the marshal writer recorded an error code and converted it into an exception at the end, replacing the exception which was already raised with ValueError("unmarshallable object"). Error messages now name the type of the unsupported object and the required version. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent aaae15c commit f39262d

3 files changed

Lines changed: 117 additions & 90 deletions

File tree

Lib/test/test_marshal.py

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,16 @@ def test_no_allow_code(self):
154154
data = {'a': [({co, 0},)]}
155155
dump = marshal.dumps(data, allow_code=True)
156156
self.assertEqual(marshal.loads(dump, allow_code=True), data)
157-
with self.assertRaises(ValueError):
157+
with self.assertRaisesRegex(ValueError,
158+
'marshalling code objects is disallowed'):
158159
marshal.dumps(data, allow_code=False)
159160
with self.assertRaises(ValueError):
160161
marshal.loads(dump, allow_code=False)
161162

162163
marshal.dump(data, io.BytesIO(), allow_code=True)
163164
self.assertEqual(marshal.load(io.BytesIO(dump), allow_code=True), data)
164-
with self.assertRaises(ValueError):
165+
with self.assertRaisesRegex(ValueError,
166+
'marshalling code objects is disallowed'):
165167
marshal.dump(data, io.BytesIO(), allow_code=False)
166168
with self.assertRaises(ValueError):
167169
marshal.load(io.BytesIO(dump), allow_code=False)
@@ -339,16 +341,29 @@ def test_reference_loop_dict(self):
339341
self.assertIsInstance(b, dict)
340342
self.assertIs(b[None], b)
341343

344+
def check_reference_loop(self, a, typename, minversion,
345+
oldmsg='object too deeply nested to marshal'):
346+
# Only versions supporting references to the type detect the loop;
347+
# older versions fail for a different reason.
348+
for v in range(minversion):
349+
with self.subTest(version=v):
350+
with self.assertRaisesRegex(ValueError, oldmsg):
351+
marshal.dumps(a, v)
352+
for v in range(minversion, marshal.version + 1):
353+
with self.subTest(version=v):
354+
with self.assertRaisesRegex(
355+
ValueError,
356+
f'cannot marshal recursion {typename} objects'):
357+
marshal.dumps(a, v)
358+
342359
def test_reference_loop_tuple(self):
343360
a = ([],)
344361
a[0].append(a)
345-
for v in range(marshal.version + 1):
346-
self.assertRaises(ValueError, marshal.dumps, a, v)
362+
self.check_reference_loop(a, 'tuple', 3)
347363

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

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

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

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

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

395407
def test_reference_loop_frozendict(self):
396408
a = frozendict({None: []})
397409
a[None].append(a)
398-
for v in range(marshal.version + 1):
399-
self.assertRaises(ValueError, marshal.dumps, a, v)
410+
self.check_reference_loop(
411+
a, 'frozendict', 6,
412+
'marshalling frozendict objects requires version 6 or higher')
400413

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

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

592+
def test_error_in_set_item(self):
593+
# Set items are sorted by their marshalled representation, and NaNs
594+
# are only distinguished by identity, so they are compared as
595+
# complex numbers.
596+
nan = float('nan')
597+
with self.assertRaisesRegex(TypeError, "'<' not supported"):
598+
marshal.dumps({complex(nan, 0), complex(nan, 0)})
599+
600+
def test_error_in_buffer(self):
601+
# The BufferError raised for a non-contiguous buffer is not replaced
602+
# with a generic error.
603+
step2 = slice(None, None, 2)
604+
with self.assertRaises(BufferError):
605+
marshal.dumps(memoryview(bytearray(b'abcdef'))[step2])
606+
576607

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

584615
@unittest.skipIf(LARGE_SIZE > sys.maxsize, "test cannot run on 32-bit systems")
585616
class LargeValuesTestCase(unittest.TestCase):
586-
def check_unmarshallable(self, data):
587-
self.assertRaises(ValueError, marshal.dump, data, NullWriter())
617+
def check_unmarshallable(self, data, msg='object too large to marshal'):
618+
with self.assertRaisesRegex(ValueError, msg):
619+
marshal.dump(data, NullWriter())
620+
621+
@support.bigmemtest(size=LARGE_SIZE, memuse=4, dry_run=False)
622+
def test_int(self, size):
623+
# An int with more than SIZE32_MAX 15-bit digits.
624+
self.check_unmarshallable(1 << (15 * size), 'int too large to marshal')
588625

589626
@support.bigmemtest(size=LARGE_SIZE, memuse=2, dry_run=False)
590627
def test_bytes(self, size):
@@ -717,7 +754,10 @@ def testFrozenDict(self):
717754
self.helper(dictobj)
718755

719756
for version in range(6):
720-
with self.assertRaises(ValueError):
757+
with self.assertRaisesRegex(
758+
ValueError,
759+
'marshalling frozendict objects requires '
760+
'version 6 or higher'):
721761
marshal.dumps(dictobj, version)
722762

723763
def testModule(self):
@@ -786,7 +826,10 @@ def test_slice(self):
786826
self.helper(obj)
787827

788828
for version in range(5):
789-
with self.assertRaises(ValueError):
829+
with self.assertRaisesRegex(
830+
ValueError,
831+
'marshalling slice objects requires '
832+
'version 5 or higher'):
790833
marshal.dumps(obj, version)
791834

792835
@support.cpython_only
@@ -818,7 +861,7 @@ def test_write_to_file_error(self):
818861

819862
def test_write_unmarshallable_to_file(self):
820863
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
821-
with self.assertRaisesRegex(ValueError, 'unmarshallable object'):
864+
with self.assertRaisesRegex(ValueError, 'cannot marshal object objects'):
822865
_testcapi.pymarshal_write_object_to_file(object(), os_helper.TESTFN,
823866
marshal.version)
824867

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

0 commit comments

Comments
 (0)