From b3e8387ad2252eda012d8e2d454ea3e293144fbf Mon Sep 17 00:00:00 2001 From: Adam Chidlow Date: Fri, 25 Oct 2024 11:38:39 +0800 Subject: [PATCH] improve error messages for unsupported NamedTuple attribute access --- src/puyapy/awst_build/eb/tuple.py | 22 +- tests/test_expected_output/tuple.test | 282 ++++++++++++++------------ 2 files changed, 169 insertions(+), 135 deletions(-) diff --git a/src/puyapy/awst_build/eb/tuple.py b/src/puyapy/awst_build/eb/tuple.py index 6eb4d310b8..7d3f26316b 100644 --- a/src/puyapy/awst_build/eb/tuple.py +++ b/src/puyapy/awst_build/eb/tuple.py @@ -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( @@ -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): @@ -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 @@ -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 diff --git a/tests/test_expected_output/tuple.test b/tests/test_expected_output/tuple.test index 85dd4a1106..da1e496193 100644 --- a/tests/test_expected_output/tuple.test +++ b/tests/test_expected_output/tuple.test @@ -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