Skip to content

Commit 339ea27

Browse files
authored
Fix UnboundLocalError when all struct packings fail in calc_packing (#942)
When every packing attempt in calc_packing() raises PackingError, the final 'raise PackingError(f"PACKING FAILED: {details}")' referenced the 'except ... as details' target after it had gone out of scope. Per Python 3 semantics the exception target is deleted at the end of the except block, so 'details' was unbound at the raise, producing an UnboundLocalError that masked the real PackingError and its message. Keep the last PackingError in a separate variable so the final raise reports the actual layout failure. Add regression tests for calc_packing. Fixes #937
1 parent f62fbc0 commit 339ea27

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

comtypes/test/test_packing.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import unittest
2+
3+
from comtypes.tools import typedesc
4+
from comtypes.tools.codegenerator.packing import PackingError, calc_packing
5+
6+
7+
# Sizes, alignments and offsets are expressed in bits, matching the values
8+
# produced by ``comtypes.tools.tlbparser`` (e.g. a 32-bit ``int``).
9+
def _make_field(name, size, align, offset):
10+
typ = typedesc.FundamentalType("int", size, align)
11+
return typedesc.Field(name, typ, None, offset)
12+
13+
14+
class CalcPackingTest(unittest.TestCase):
15+
def test_returns_none_for_default_packing(self):
16+
# A naturally laid out single-field struct needs no explicit packing.
17+
field = _make_field("a", 32, 32, 0)
18+
struct = typedesc.Structure(
19+
"Good", align=32, members=[field], bases=[], size=32
20+
)
21+
self.assertIsNone(calc_packing(struct, [field]))
22+
23+
def test_incomplete_struct_returns_none(self):
24+
field = _make_field("a", 32, 32, 0)
25+
struct = typedesc.Structure(
26+
"Incomplete", align=32, members=[field], bases=[], size=None
27+
)
28+
self.assertIsNone(calc_packing(struct, [field]))
29+
30+
def test_raises_packing_error_with_details_when_layout_is_inconsistent(self):
31+
# The declared field offset does not match the computed layout, so every
32+
# packing attempt raises ``PackingError`` and ``calc_packing`` must
33+
# re-raise with the underlying reason.
34+
#
35+
# Regression test for gh-937: the final ``raise`` referenced the
36+
# ``except ... as`` target after it had been cleared, raising
37+
# ``UnboundLocalError`` and masking the real ``PackingError``.
38+
field = _make_field("x", 32, 32, 64)
39+
struct = typedesc.Structure("Bad", align=32, members=[field], bases=[], size=96)
40+
with self.assertRaises(PackingError) as ctx:
41+
calc_packing(struct, [field])
42+
message = str(ctx.exception)
43+
self.assertIn("PACKING FAILED", message)
44+
# The original failure reason must be preserved, not lost.
45+
self.assertIn("field x offset", message)
46+
47+
48+
if __name__ == "__main__":
49+
unittest.main()

comtypes/tools/codegenerator/packing.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,23 @@ def _calc_packing(struct, fields, pack, isStruct):
4444
def calc_packing(struct, fields):
4545
# try several packings, starting with unspecified packing
4646
isStruct = isinstance(struct, typedesc.Structure)
47+
last_error = None
4748
for pack in [None, 16 * 8, 8 * 8, 4 * 8, 2 * 8, 1 * 8]:
4849
try:
4950
_calc_packing(struct, fields, pack, isStruct)
5051
except PackingError as details:
52+
# The exception target is cleared when the ``except`` block ends
53+
# (see https://docs.python.org/3/reference/compound_stmts.html#except-clause),
54+
# so keep a reference for the final error message below.
55+
last_error = details
5156
continue
5257
else:
5358
if pack is None:
5459
return None
5560

5661
return int(pack / 8)
5762

58-
raise PackingError(f"PACKING FAILED: {details}")
63+
raise PackingError(f"PACKING FAILED: {last_error}")
5964

6065

6166
class PackingError(Exception):

0 commit comments

Comments
 (0)