Skip to content

Commit

Permalink
improve error messages for unsupported NamedTuple attribute access
Browse files Browse the repository at this point in the history
  • Loading branch information
achidlow committed Oct 25, 2024
1 parent 8208478 commit b3e8387
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 135 deletions.
22 changes: 18 additions & 4 deletions src/puyapy/awst_build/eb/tuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@
logger = log.get_logger(__name__)


_TUPLE_MEMBERS: typing.Final = frozenset(("count", "index"))
_NAMED_TUPLE_MEMBERS: typing.Final = frozenset(("_asdict", "_replace"))
_NAMED_TUPLE_CLASS_MEMBERS: typing.Final = frozenset(("_make", "_fields", "_field_defaults"))


class GenericTupleTypeBuilder(GenericTypeBuilder):
@typing.override
def call(
Expand Down Expand Up @@ -138,6 +143,11 @@ def call(
)
return TupleExpressionBuilder(expr, pytype)

def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
if name in _NAMED_TUPLE_CLASS_MEMBERS:
raise CodeError("unsupported member access", location)
return super().member_access(name, location)


class TupleLiteralBuilder(InstanceBuilder[pytypes.TupleType], StaticSizedCollectionBuilder):
def __init__(self, items: Sequence[InstanceBuilder], location: SourceLocation):
Expand All @@ -156,8 +166,8 @@ def iterate_static(self) -> Sequence[InstanceBuilder]:

@typing.override
def member_access(self, name: str, location: SourceLocation) -> typing.Never:
if name in dir(tuple()): # noqa: C408
raise CodeError("method is not currently supported", location)
if name in _TUPLE_MEMBERS:
raise CodeError("unsupported member access", location)
raise CodeError("unrecognised member access", location)

@typing.override
Expand Down Expand Up @@ -303,8 +313,12 @@ def member_access(self, name: str, location: SourceLocation) -> NodeBuilder:
return builder_for_instance(item_typ, item_expr)
elif name == "_replace":
return _Replace(self, self.pytype, location)
if name in dir(tuple()): # noqa: C408
raise CodeError("method is not currently supported", location)
elif name in _NAMED_TUPLE_MEMBERS:
raise CodeError("unsupported member access", location)
elif name in _NAMED_TUPLE_CLASS_MEMBERS:
return NamedTupleTypeBuilder(self.pytype, self.source_location).member_access(name, location)
if name in _TUPLE_MEMBERS:
raise CodeError("unsupported member access", location)
raise CodeError("unrecognised member access", location)

@typing.override
Expand Down
282 changes: 151 additions & 131 deletions tests/test_expected_output/tuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -3,137 +3,157 @@
# type: ignore

## case: test_tuple_errors

import typing
from algopy import *


class TupleContract(Contract): ## W: Class test_tuple_errors.TupleContract is implicitly abstract

@subroutine
def test_tuple1(self) -> None:
tup = (UInt64(), Bytes())
tup.count(UInt64(1)) ## E: method is not currently supported

@subroutine
def test_tuple1b(self) -> None:
bad = (UInt64(), Bytes()).count(UInt64(1)) ## E: method is not currently supported

@subroutine
def test_tuple1c(self) -> None:
bad = (UInt64(), Bytes()).what # type: ignore[attr-defined, misc] ## E: unrecognised member access

@subroutine
def test_tuple1d(self) -> None:
tup = (UInt64(), Bytes())
bad = tup.what # type: ignore[attr-defined, misc] ## E: unrecognised member access

@subroutine
def test_tuple2(self) -> None:
log((UInt64(), Bytes())) # type: ignore[arg-type] ## E: cannot serialize tuple[algopy.UInt64, algopy.Bytes]

@subroutine
def test_tuple2b(self) -> None:
tup = (UInt64(), Bytes())
log(tup) # type: ignore[arg-type] ## E: cannot serialize tuple[algopy.UInt64, algopy.Bytes]

@subroutine
def test_tuple3(self) -> None:
tup = UInt64(), Bytes()
tup2 = tup * UInt64(2) ## E: can't multiply sequence by non-int-literal

@subroutine
def test_tuple3b(self) -> None:
tup = (UInt64(), Bytes()) * UInt64(2) ## E: can't multiply sequence by non-int-literal

@subroutine
def test_tuple4(self) -> None:
tup = (UInt64(), Bytes())
del tup ## E: tuple[algopy.UInt64, algopy.Bytes] is not valid as del target

@subroutine
def test_tuple4b(self) -> None:
a, b = UInt64(), Bytes()
del (a, b) ## E: cannot delete tuple literal
@subroutine
def test_tuple5(self) -> None:
tup = (UInt64(), Bytes())
-tup # type: ignore[operator] ## E: tuple[algopy.UInt64, algopy.Bytes] does not support unary '-' operator

@subroutine
def test_tuple5b(self) -> None:
tup = -(UInt64(), Bytes(),) # type: ignore[operator, misc] ## E: bad operand type for unary -: 'tuple'

@subroutine
def test_tuple6(self) -> None:
tup = (UInt64(), Bytes())
tup += ( ## E: tuple[algopy.UInt64, algopy.Bytes] does not support augmented assignment
UInt64(2),
) # type:ignore[assignment]

@subroutine
def test_tuple7(self) -> None:
tup = () ## E: empty tuples are not supported

@subroutine
def test_tuple8(self) -> None:
a, b = UInt64(), Bytes()
assert (a, b) != (b, a) # type: ignore[comparison-overlap] ## E: items at index 0 do not support comparison with operator '!='

@subroutine
def test_tuple9(self) -> None:
a, b = UInt64(), Bytes()
abc = (a, b) + Bytes() # type: ignore[operator, misc] ## E: can only concatenate tuple with other tuples

@subroutine
def test_tuple10(self) -> None:
a, b = UInt64(), Bytes()
abc = (a, b) + Bytes() # type: ignore[operator, misc] ## E: can only concatenate tuple with other tuples

@subroutine
def test_tuple11(self) -> None:
a, b = (UInt64(), UInt64())
abc = (a, b) or (b, a)

@subroutine
def test_slice1(self) -> None:
tip = UInt64(), Bytes()
top = tip[1:1] ## E: empty slices are not supported

@subroutine
def test_slice1b(self) -> None:
# this ok, including for coverage
top = (UInt64(), Bytes())[:]


@subroutine
def test_slice2(self) -> None:
tip = UInt64(), Bytes()
top = tip[:1]

@subroutine
def test_slice3(self) -> None:
tip = UInt64(), Bytes()
top = tip[UInt64(1):] ## E: tuples can only be indexed with literal values

@subroutine
def test_slice4(self) -> None:
tip = UInt64(), Bytes()
top = tip[:1:2] ## E: stride is not supported

@subroutine
def test_index1(self) -> None:
tip = UInt64(), Bytes()
top = tip[2] # type: ignore[misc] ## E: tuple index out of range

@subroutine
def test_index2(self) -> None:
tip = UInt64(), Bytes()
top = tip[UInt64(1)] ## E: tuples can only be indexed by int constants

@subroutine
def iterate_tuple(self, base: gtxn.TransactionBase) -> None:
for a in (base, gtxn.PaymentTransaction(1)):
assert a.fee > 0
for b in (gtxn.PaymentTransaction(1), gtxn.KeyRegistrationTransaction(2)): ## E: unable to iterate heterogeneous tuple without common base type
assert b.fee > 0

class MyNamedTuple(typing.NamedTuple):
x: UInt64
y: Bytes


@subroutine
def test_tuple1() -> None:
tup = (UInt64(), Bytes())
tup.count(UInt64(1)) ## E: unsupported member access

@subroutine
def test_tuple1b() -> None:
bad = (UInt64(), Bytes()).count(UInt64(1)) ## E: unsupported member access

@subroutine
def test_tuple1c() -> None:
bad = (UInt64(), Bytes()).what # type: ignore[attr-defined, misc] ## E: unrecognised member access

@subroutine
def test_tuple1d() -> None:
tup = (UInt64(), Bytes())
bad = tup.what # type: ignore[attr-defined, misc] ## E: unrecognised member access

@subroutine
def test_named_tuple1() -> None:
tup = MyNamedTuple(UInt64(), Bytes())
tup.count(UInt64(1)) ## E: unsupported member access

@subroutine
def test_named_tuple1b() -> None:
MyNamedTuple._make((UInt64(), Bytes())) ## E: unsupported member access

@subroutine
def test_named_tuple1c() -> None:
tup = MyNamedTuple(UInt64(), Bytes())
x = String(tup._fields[0]) ## E: unsupported member access

@subroutine
def test_named_tuple1d() -> None:
tup = MyNamedTuple(UInt64(), Bytes())
tup._asdict() ## E: unsupported member access

@subroutine
def test_tuple2() -> None:
log((UInt64(), Bytes())) # type: ignore[arg-type] ## E: cannot serialize tuple[algopy.UInt64, algopy.Bytes]

@subroutine
def test_tuple2b() -> None:
tup = (UInt64(), Bytes())
log(tup) # type: ignore[arg-type] ## E: cannot serialize tuple[algopy.UInt64, algopy.Bytes]

@subroutine
def test_tuple3() -> None:
tup = UInt64(), Bytes()
tup2 = tup * UInt64(2) ## E: can't multiply sequence by non-int-literal

@subroutine
def test_tuple3b() -> None:
tup = (UInt64(), Bytes()) * UInt64(2) ## E: can't multiply sequence by non-int-literal

@subroutine
def test_tuple4() -> None:
tup = (UInt64(), Bytes())
del tup ## E: tuple[algopy.UInt64, algopy.Bytes] is not valid as del target

@subroutine
def test_tuple4b() -> None:
a, b = UInt64(), Bytes()
del (a, b) ## E: cannot delete tuple literal
@subroutine
def test_tuple5() -> None:
tup = (UInt64(), Bytes())
-tup # type: ignore[operator] ## E: tuple[algopy.UInt64, algopy.Bytes] does not support unary '-' operator

@subroutine
def test_tuple5b() -> None:
tup = -(UInt64(), Bytes(),) # type: ignore[operator, misc] ## E: bad operand type for unary -: 'tuple'

@subroutine
def test_tuple6() -> None:
tup = (UInt64(), Bytes())
tup += ( ## E: tuple[algopy.UInt64, algopy.Bytes] does not support augmented assignment
UInt64(2),
) # type:ignore[assignment]

@subroutine
def test_tuple7() -> None:
tup = () ## E: empty tuples are not supported

@subroutine
def test_tuple8() -> None:
a, b = UInt64(), Bytes()
assert (a, b) != (b, a) # type: ignore[comparison-overlap] ## E: items at index 0 do not support comparison with operator '!='

@subroutine
def test_tuple9() -> None:
a, b = UInt64(), Bytes()
abc = (a, b) + Bytes() # type: ignore[operator, misc] ## E: can only concatenate tuple with other tuples

@subroutine
def test_tuple10() -> None:
a, b = UInt64(), Bytes()
abc = (a, b) + Bytes() # type: ignore[operator, misc] ## E: can only concatenate tuple with other tuples

@subroutine
def test_tuple11() -> None:
a, b = (UInt64(), UInt64())
abc = (a, b) or (b, a)

@subroutine
def test_slice1() -> None:
tip = UInt64(), Bytes()
top = tip[1:1] ## E: empty slices are not supported

@subroutine
def test_slice1b() -> None:
# this ok, including for coverage
top = (UInt64(), Bytes())[:]

@subroutine
def test_slice2() -> None:
tip = UInt64(), Bytes()
top = tip[:1]

@subroutine
def test_slice3() -> None:
tip = UInt64(), Bytes()
top = tip[UInt64(1):] ## E: tuples can only be indexed with literal values

@subroutine
def test_slice4() -> None:
tip = UInt64(), Bytes()
top = tip[:1:2] ## E: stride is not supported

@subroutine
def test_index1() -> None:
tip = UInt64(), Bytes()
top = tip[2] # type: ignore[misc] ## E: tuple index out of range

@subroutine
def test_index2() -> None:
tip = UInt64(), Bytes()
top = tip[UInt64(1)] ## E: tuples can only be indexed by int constants

@subroutine
def iterate_tuple(base: gtxn.TransactionBase) -> None:
for a in (base, gtxn.PaymentTransaction(1)):
assert a.fee > 0
for b in (gtxn.PaymentTransaction(1), gtxn.KeyRegistrationTransaction(2)): ## E: unable to iterate heterogeneous tuple without common base type
assert b.fee > 0

0 comments on commit b3e8387

Please sign in to comment.