Skip to content

Commit 399fa12

Browse files
committed
Improve CPython compatibility for value unpacking
1 parent 7ff5f51 commit 399fa12

File tree

6 files changed

+61
-71
lines changed

6 files changed

+61
-71
lines changed

monic/expressions/__init__.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
#
66

77
from monic.expressions.context import ExpressionsContext
8-
from monic.expressions.exceptions import (
9-
SecurityError,
10-
UnsupportedUnpackingError,
11-
)
8+
from monic.expressions.exceptions import SecurityError
129
from monic.expressions.interpreter import ExpressionsInterpreter
1310
from monic.expressions.parser import ExpressionsParser
1411
from monic.expressions.registry import (
@@ -26,7 +23,6 @@
2623
"ExpressionsParser",
2724
# Exceptions
2825
"SecurityError",
29-
"UnsupportedUnpackingError",
3026
# Registry
3127
"monic_bind",
3228
"monic_bind_module",

monic/expressions/exceptions.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,3 @@
77

88
class SecurityError(Exception):
99
"""Raised when dangerous operations are detected."""
10-
11-
12-
class UnsupportedUnpackingError(Exception):
13-
"""Raised when an unsupported unpacking pattern is encountered."""

monic/expressions/interpreter.py

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@
2020
from dataclasses import dataclass, field
2121

2222
from monic.expressions.context import ExpressionsContext
23-
from monic.expressions.exceptions import (
24-
SecurityError,
25-
UnsupportedUnpackingError,
26-
)
23+
from monic.expressions.exceptions import SecurityError
2724
from monic.expressions.registry import registry
2825

2926

@@ -178,7 +175,6 @@ def __init__(self, context: ExpressionsContext | None = None) -> None:
178175
"TimeoutError": TimeoutError,
179176
"RuntimeError": RuntimeError,
180177
"SecurityError": SecurityError,
181-
"UnsupportedUnpackingError": UnsupportedUnpackingError,
182178
}
183179

184180
# Add registered objects to global environment
@@ -622,8 +618,7 @@ def _handle_unpacking_target(self, target: ast.AST, value: t.Any) -> None:
622618
value: The value being assigned
623619
624620
Raises:
625-
UnsupportedUnpackingError: If an unsupported unpacking pattern is
626-
encountered
621+
TypeError: If an unsupported unpacking pattern is encountered
627622
"""
628623
if isinstance(target, ast.Name):
629624
self._handle_name_target(target, value)
@@ -634,9 +629,7 @@ def _handle_unpacking_target(self, target: ast.AST, value: t.Any) -> None:
634629
elif isinstance(target, (ast.Tuple, ast.List)):
635630
self._handle_sequence_unpacking(target, value)
636631
else:
637-
raise UnsupportedUnpackingError(
638-
f"Unsupported unpacking target type: {type(target).__name__}"
639-
)
632+
raise TypeError(f"cannot unpack {type(target).__name__}")
640633

641634
def _handle_sequence_unpacking(
642635
self,
@@ -650,13 +643,17 @@ def _handle_sequence_unpacking(
650643
value: Value to unpack
651644
652645
Raises:
653-
ValueError: If value cannot be unpacked
654-
UnsupportedUnpackingError: If unpacking pattern is not supported
646+
TypeError: If value cannot be unpacked
647+
ValueError: If there are too many or too few values to unpack
648+
SyntaxError: If multiple starred expressions are used
655649
"""
656650
with ScopeContext(self):
657651
try:
658652
if not hasattr(value, "__iter__"):
659-
raise ValueError("Cannot unpack non-iterable value")
653+
raise TypeError(
654+
f"cannot unpack non-iterable {type(value).__name__} "
655+
"object"
656+
)
660657

661658
# Check for starred expressions (extended unpacking)
662659
starred_indices = [
@@ -666,8 +663,8 @@ def _handle_sequence_unpacking(
666663
]
667664

668665
if len(starred_indices) > 1:
669-
raise UnsupportedUnpackingError(
670-
"Cannot use multiple starred expressions in assignment"
666+
raise SyntaxError(
667+
"multiple starred expressions in assignment"
671668
)
672669

673670
if starred_indices:
@@ -683,15 +680,21 @@ def _handle_sequence_unpacking(
683680
# Standard unpacking without starred expression
684681
value_list = list(value)
685682
if len(value_list) < len(target.elts):
686-
raise ValueError("Not enough values to unpack")
683+
raise ValueError(
684+
"not enough values to unpack (expected "
685+
f"{len(target.elts)}, got {len(value_list)})"
686+
)
687687
elif len(value_list) > len(target.elts):
688-
raise ValueError("Too many values to unpack")
688+
raise ValueError(
689+
"too many values to unpack (expected "
690+
f"{len(target.elts)})"
691+
)
689692

690693
# Unpack each element
691694
for tgt, val in zip(target.elts, value):
692695
self._handle_unpacking_target(tgt, val)
693-
except (TypeError, ValueError) as e:
694-
raise UnsupportedUnpackingError(str(e)) from e
696+
except (TypeError, ValueError, SyntaxError) as e:
697+
raise type(e)(str(e)) from e
695698

696699
def _handle_starred_unpacking(
697700
self,
@@ -710,7 +713,7 @@ def _handle_starred_unpacking(
710713
711714
Raises:
712715
ValueError: If there are not enough values to unpack
713-
UnsupportedUnpackingError: If unpacking pattern is not supported
716+
TypeError: If target is not a valid unpacking target
714717
"""
715718
with ScopeContext(self):
716719
iter_value = iter(value)
@@ -721,7 +724,7 @@ def _handle_starred_unpacking(
721724
try:
722725
self._handle_unpacking_target(tgt, next(iter_value))
723726
except StopIteration as e:
724-
raise ValueError("Not enough values to unpack") from e
727+
raise ValueError("not enough values to unpack") from e
725728

726729
# Collect remaining elements for the starred target
727730
starred_values = list(iter_value)
@@ -733,7 +736,7 @@ def _handle_starred_unpacking(
733736
if after_star_count > 0:
734737
# Make sure there are enough elements
735738
if len(starred_values) < after_star_count:
736-
raise ValueError("Not enough values to unpack")
739+
raise ValueError("not enough values to unpack")
737740

738741
# Separate starred values
739742
starred_list = starred_values[:-after_star_count]
@@ -742,6 +745,8 @@ def _handle_starred_unpacking(
742745
# Assign starred target
743746
if isinstance(starred_target.value, ast.Name):
744747
self._set_name_value(starred_target.value.id, starred_list)
748+
else:
749+
raise TypeError("starred assignment target must be a name")
745750

746751
# Assign elements after starred
747752
after_elements = target_elts[star_index + 1 :]
@@ -754,6 +759,8 @@ def _handle_starred_unpacking(
754759
self._set_name_value(
755760
starred_target.value.id, starred_values
756761
)
762+
else:
763+
raise TypeError("starred assignment target must be a name")
757764

758765
def visit_NamedExpr(self, node: ast.NamedExpr) -> t.Any:
759766
"""Handle named expressions (walrus operator).
@@ -1725,7 +1732,7 @@ def process_generator(
17251732
self._handle_unpacking_target(
17261733
generator.target, item
17271734
)
1728-
except UnsupportedUnpackingError:
1735+
except (TypeError, ValueError, SyntaxError):
17291736
if isinstance(generator.target, ast.Name):
17301737
self._set_name_value(generator.target.id, item)
17311738
else:
@@ -1803,14 +1810,16 @@ def _process_generator_item(
18031810
bool: Whether all conditions are met
18041811
18051812
Raises:
1806-
UnsupportedUnpackingError: If target unpacking fails
1813+
TypeError: If value is not iterable
1814+
ValueError: If target unpacking fails
1815+
SyntaxError: If multiple starred expressions are used
18071816
"""
18081817
# Restore environment from before this generator's loop
18091818
self.local_env = current_env.copy()
18101819

18111820
try:
18121821
self._handle_unpacking_target(generator.target, item)
1813-
except UnsupportedUnpackingError:
1822+
except (TypeError, ValueError, SyntaxError):
18141823
if isinstance(generator.target, ast.Name):
18151824
self._set_name_value(generator.target.id, item)
18161825
else:

tests/expressions/test_interpreter_coverage1.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from monic.expressions import (
1212
ExpressionsParser,
1313
ExpressionsInterpreter,
14-
UnsupportedUnpackingError,
1514
SecurityError,
1615
monic_bind,
1716
)
@@ -386,7 +385,7 @@ def test_error_handling_edge_cases():
386385
"""
387386
tree = parser.parse(code)
388387
with pytest.raises(
389-
UnsupportedUnpackingError, match="Cannot unpack non-iterable"
388+
TypeError, match="cannot unpack non-iterable int object"
390389
):
391390
interpreter.execute(tree)
392391

@@ -395,19 +394,15 @@ def test_error_handling_edge_cases():
395394
a, b = [1, 2, 3] # Too many values
396395
"""
397396
tree = parser.parse(code)
398-
with pytest.raises(
399-
UnsupportedUnpackingError, match="Too many values to unpack"
400-
):
397+
with pytest.raises(ValueError, match="too many values to unpack"):
401398
interpreter.execute(tree)
402399

403400
# Test not enough values to unpack
404401
code = """
405402
a, b, c = [1, 2] # Not enough values
406403
"""
407404
tree = parser.parse(code)
408-
with pytest.raises(
409-
UnsupportedUnpackingError, match="Not enough values to unpack"
410-
):
405+
with pytest.raises(ValueError, match="not enough values to unpack"):
411406
interpreter.execute(tree)
412407

413408
# Test invalid starred unpacking
@@ -416,8 +411,8 @@ def test_error_handling_edge_cases():
416411
"""
417412
tree = parser.parse(code)
418413
with pytest.raises(
419-
UnsupportedUnpackingError,
420-
match="Cannot use multiple starred expressions",
414+
SyntaxError,
415+
match="multiple starred expressions in assignment",
421416
):
422417
interpreter.execute(tree)
423418

tests/expressions/test_interpreter_coverage4.py

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from monic.expressions import (
1212
ExpressionsParser,
1313
ExpressionsInterpreter,
14-
UnsupportedUnpackingError,
1514
)
1615
from monic.expressions.registry import registry, NamespaceProxy, Registry
1716

@@ -293,33 +292,31 @@ def test_unpacking_errors():
293292
a, b = [1, 2, 3]
294293
result = "should not reach here"
295294
except ValueError as e:
296-
if str(e) == "Too many values to unpack":
295+
if str(e) == "too many values to unpack (expected 2)":
297296
result = "too many values"
298297
else:
299298
result = str(e)
300299
result
301300
"""
302301
tree = parser.parse(code)
303-
with pytest.raises(UnsupportedUnpackingError) as exc_info:
304-
interpreter.execute(tree)
305-
assert str(exc_info.value) == "Too many values to unpack"
302+
result = interpreter.execute(tree)
303+
assert result == "too many values"
306304

307305
# Test not enough values
308306
code = """
309307
try:
310308
a, b, c = [1, 2]
311309
result = "should not reach here"
312310
except ValueError as e:
313-
if str(e) == "Not enough values to unpack":
311+
if str(e) == "not enough values to unpack (expected 3, got 2)":
314312
result = "not enough values"
315313
else:
316314
result = str(e)
317315
result
318316
"""
319317
tree = parser.parse(code)
320-
with pytest.raises(UnsupportedUnpackingError) as exc_info:
321-
interpreter.execute(tree)
322-
assert str(exc_info.value) == "Not enough values to unpack"
318+
result = interpreter.execute(tree)
319+
assert result == "not enough values"
323320

324321

325322
def test_match_basic_sequence():
@@ -432,12 +429,10 @@ def test_unpacking_with_multiple_starred_expressions():
432429
parser = ExpressionsParser()
433430
interpreter = ExpressionsInterpreter()
434431

435-
with pytest.raises(UnsupportedUnpackingError) as exc_info:
432+
with pytest.raises(SyntaxError) as exc_info:
436433
interpreter.execute(parser.parse(code))
437434

438-
assert "Cannot use multiple starred expressions in assignment" in str(
439-
exc_info.value
440-
)
435+
assert "multiple starred expressions in assignment" in str(exc_info.value)
441436

442437

443438
def test_unpacking_with_insufficient_values():
@@ -449,10 +444,10 @@ def test_unpacking_with_insufficient_values():
449444
parser = ExpressionsParser()
450445
interpreter = ExpressionsInterpreter()
451446

452-
with pytest.raises(UnsupportedUnpackingError) as exc_info:
447+
with pytest.raises(ValueError) as exc_info:
453448
interpreter.execute(parser.parse(code))
454449

455-
assert "Not enough values to unpack" in str(exc_info.value)
450+
assert "not enough values to unpack" in str(exc_info.value)
456451

457452

458453
def test_unpacking_with_too_many_values():
@@ -464,10 +459,10 @@ def test_unpacking_with_too_many_values():
464459
parser = ExpressionsParser()
465460
interpreter = ExpressionsInterpreter()
466461

467-
with pytest.raises(UnsupportedUnpackingError) as exc_info:
462+
with pytest.raises(ValueError) as exc_info:
468463
interpreter.execute(parser.parse(code))
469464

470-
assert "Too many values to unpack" in str(exc_info.value)
465+
assert "too many values to unpack" in str(exc_info.value)
471466

472467

473468
def test_unpacking_non_iterable():
@@ -479,10 +474,10 @@ def test_unpacking_non_iterable():
479474
parser = ExpressionsParser()
480475
interpreter = ExpressionsInterpreter()
481476

482-
with pytest.raises(UnsupportedUnpackingError) as exc_info:
477+
with pytest.raises(TypeError) as exc_info:
483478
interpreter.execute(parser.parse(code))
484479

485-
assert "Cannot unpack non-iterable value" in str(exc_info.value)
480+
assert "cannot unpack non-iterable int object" in str(exc_info.value)
486481

487482

488483
def test_unpacking_with_attribute_target():

tests/expressions/test_interpreter_error_handling.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
ExpressionsParser,
1111
ExpressionsInterpreter,
1212
SecurityError,
13-
UnsupportedUnpackingError,
1413
)
1514

1615

@@ -45,28 +44,28 @@ def test_unpacking_errors():
4544

4645
# Test unpacking non-iterable
4746
with pytest.raises(
48-
UnsupportedUnpackingError, match="Cannot unpack non-iterable value"
47+
TypeError, match="cannot unpack non-iterable int object"
4948
):
5049
interpreter.execute(parser.parse("a, b = 42"))
5150

5251
# Test too many values to unpack
5352
with pytest.raises(
54-
UnsupportedUnpackingError,
55-
match="Not enough values to unpack",
53+
ValueError,
54+
match="not enough values to unpack",
5655
):
5756
interpreter.execute(parser.parse("a, b, c = [1, 2]"))
5857

5958
# Test too few values to unpack
6059
with pytest.raises(
61-
UnsupportedUnpackingError,
62-
match="Too many values to unpack",
60+
ValueError,
61+
match="too many values to unpack",
6362
):
6463
interpreter.execute(parser.parse("a, b = [1, 2, 3]"))
6564

6665
# Test multiple starred expressions
6766
with pytest.raises(
68-
UnsupportedUnpackingError,
69-
match="Cannot use multiple starred expressions in assignment",
67+
SyntaxError,
68+
match="multiple starred expressions in assignment",
7069
):
7170
interpreter.execute(parser.parse("*a, *b = [1, 2, 3]"))
7271

0 commit comments

Comments
 (0)