Skip to content

Commit

Permalink
refactor: improving handling of initial value for implicit global/loc…
Browse files Browse the repository at this point in the history
…al state keys
  • Loading branch information
aorumbayev committed Aug 12, 2024
1 parent c3ada65 commit bcd3ee9
Show file tree
Hide file tree
Showing 10 changed files with 402 additions and 100 deletions.
13 changes: 5 additions & 8 deletions src/algopy_testing/models/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,17 +143,14 @@ def __setattr__(self, name: str, value: typing.Any) -> None:
match value:
case (algopy_testing.Box() | algopy_testing.BoxRef()) as box if not box._key:
box._key = name_bytes
case (algopy_testing.GlobalState() | algopy_testing.LocalState()) as state:
case algopy_testing.GlobalState() as state:
state.app_id = _get_self_or_active_app_id(self)
if not state._key:
state.set_key(name_bytes)
case algopy_testing.LocalState() as state:
state.app_id = _get_self_or_active_app_id(self)
if not state._key:
state._key = name_bytes
if isinstance(state, algopy_testing.GlobalState) and hasattr(
state, "_initial_value"
):
state._value = state._initial_value
del state._initial_value
elif hasattr(state, "_value"):
state._value = state._value
case algopy_testing.BoxMap() as box_map if box_map._key_prefix is None:
box_map._key_prefix = name_bytes
case Bytes() | UInt64() | BytesBacked() | UInt64Backed() | bool():
Expand Down
107 changes: 59 additions & 48 deletions src/algopy_testing/state/global_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,78 +45,89 @@ def __init__(
) -> None:
self.description = description
self.app_id = lazy_context.active_group.active_app_id

if key == "":
self._key = Bytes() # Indicate that this key needs to be set later
else:
match key:
case bytes(bytes_key):
self._key = Bytes(bytes_key)
case Bytes() as key_bytes:
self._key = key_bytes
case str(str_key):
self._key = String(str_key).bytes
case String() as key_str:
self._key = key_str.bytes
case _:
raise ValueError("Key must be bytes or str")
self._key: Bytes | None = None
self._pending_value: _T | None = None

if isinstance(type_or_value, type):
self.type_: type[_T] = type_or_value
else:
self.type_ = type(type_or_value)
self._initial_value = (
type_or_value # Store initial value, but don't set in global state yet
)
self._pending_value = type_or_value

self.set_key(key)

def set_key(self, key: Bytes | String | bytes | str) -> None:
"""Set the key and apply any pending value.
Pending values are used for implicit keys in Contract
subclasses. They're stored until the 'Contract''s initialization
sets the key.
"""
match key:
case bytes(bytes_key):
self._key = Bytes(bytes_key)
case Bytes() as key_bytes:
self._key = key_bytes
case str(str_key):
self._key = String(str_key).bytes
case String() as key_str:
self._key = key_str.bytes
case _:
raise ValueError("Key must be bytes or str")

if self._key and self._pending_value is not None:
self.value = self._pending_value
self._pending_value = None

@property
def key(self) -> algopy.Bytes:
"""Provides access to the raw storage key."""
if self._key is None:
raise ValueError("Key is not set")
return self._key

@property
def value(self) -> _T:
if self._value is None:
raise ValueError("Value is not set")
return self._value
if self._key is None:
if self._pending_value is not None:
return self._pending_value
raise ValueError("Key is not set")
app_data = lazy_context.get_app_data(self.app_id)
try:
native = app_data.get_global_state(self._key.value)
except KeyError as e:
raise ValueError("Value is not set") from e
return deserialize(self.type_, native)

@value.setter
def value(self, value: _T) -> None:
self._value = value
if self._key is None:
self._pending_value = value
else:
app_data = lazy_context.get_app_data(self.app_id)
app_data.set_global_state(self._key.value, serialize(value))

@value.deleter
def value(self) -> None:
self._value = None

@property
def _value(self) -> _T | None:
if self._key is None:
return None # Key not set yet, so no value
app_data = lazy_context.get_app_data(self.app_id)
try:
native = app_data.get_global_state(self._key.value)
except KeyError:
return None
self._pending_value = None
else:
return deserialize(self.type_, native)

@_value.setter
def _value(self, value: _T | None) -> None:
if self._key is None:
raise ValueError("Cannot set value before key is set")
native = None if value is None else serialize(value)
app_data = lazy_context.get_app_data(self.app_id)
app_data.set_global_state(self._key.value, native)
app_data = lazy_context.get_app_data(self.app_id)
app_data.set_global_state(self._key.value, None)

def __bool__(self) -> bool:
return self._value is not None
return self._key is not None or self._pending_value is not None

def get(self, default: _T | None = None) -> _T:
if self._value is not None:
return self._value
if default is not None:
return default
return self.type_()
try:
return self.value
except ValueError:
if default is not None:
return default
return self.type_()

def maybe(self) -> tuple[_T | None, bool]:
return self._value, self._value is not None
try:
return self.value, True
except ValueError:
return None, False
22 changes: 22 additions & 0 deletions tests/artifacts/StateOps/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ class GlobalStateContract(ARC4Contract):
def __init__(self) -> None:
self.implicit_key_arc4_uint = GlobalState(arc4.UInt64(1337))
self.implicit_key_arc4_string = GlobalState(arc4.String("Hello"))
self.arc4_uint = GlobalState(arc4.UInt64(1337), key="explicit_key_arc4_uint")
self.arc4_string = GlobalState(arc4.String("Hello"), key="explicit_key_arc4_string")

@arc4.abimethod()
def get_implicit_key_arc4_uint(self) -> arc4.UInt64:
Expand All @@ -372,6 +374,14 @@ def get_implicit_key_arc4_uint(self) -> arc4.UInt64:
def get_implicit_key_arc4_string(self) -> arc4.String:
return self.implicit_key_arc4_string.value

@arc4.abimethod()
def get_arc4_uint(self) -> arc4.UInt64:
return self.arc4_uint.value

@arc4.abimethod()
def get_arc4_string(self) -> arc4.String:
return self.arc4_string.value


class LocalStateContract(ARC4Contract):
def __init__(self) -> None:
Expand All @@ -381,11 +391,15 @@ def __init__(self) -> None:
self.implicit_key_arc4_string = LocalState(
arc4.String,
)
self.arc4_uint = LocalState(arc4.UInt64, key="explicit_key_arc4_uint")
self.arc4_string = LocalState(arc4.String, key="explicit_key_arc4_string")

@arc4.abimethod(allow_actions=["OptIn"])
def opt_in(self) -> None:
self.implicit_key_arc4_uint[Global.creator_address] = arc4.UInt64(1337)
self.implicit_key_arc4_string[Global.creator_address] = arc4.String("Hello")
self.arc4_uint[Global.creator_address] = arc4.UInt64(1337)
self.arc4_string[Global.creator_address] = arc4.String("Hello")

@arc4.abimethod()
def get_implicit_key_arc4_uint(self, a: Account) -> arc4.UInt64:
Expand All @@ -394,3 +408,11 @@ def get_implicit_key_arc4_uint(self, a: Account) -> arc4.UInt64:
@arc4.abimethod()
def get_implicit_key_arc4_string(self, a: Account) -> arc4.String:
return self.implicit_key_arc4_string[a]

@arc4.abimethod()
def get_arc4_uint(self, a: Account) -> arc4.UInt64:
return self.arc4_uint[a]

@arc4.abimethod()
def get_arc4_string(self, a: Account) -> arc4.String:
return self.arc4_string[a]
104 changes: 93 additions & 11 deletions tests/artifacts/StateOps/data/GlobalStateContract.approval.teal
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ main_entrypoint@2:
// tests/artifacts/StateOps/contract.py:362
// class GlobalStateContract(ARC4Contract):
txn NumAppArgs
bz main_bare_routing@8
bz main_bare_routing@10
method "get_implicit_key_arc4_uint()uint64"
method "get_implicit_key_arc4_string()string"
method "get_arc4_uint()uint64"
method "get_arc4_string()string"
txna ApplicationArgs 0
match main_get_implicit_key_arc4_uint_route@4 main_get_implicit_key_arc4_string_route@5
match main_get_implicit_key_arc4_uint_route@4 main_get_implicit_key_arc4_string_route@5 main_get_arc4_uint_route@6 main_get_arc4_string_route@7
err // reject transaction

main_get_implicit_key_arc4_uint_route@4:
// tests/artifacts/StateOps/contract.py:367
// tests/artifacts/StateOps/contract.py:369
// @arc4.abimethod()
txn OnCompletion
!
Expand All @@ -33,7 +35,7 @@ main_get_implicit_key_arc4_uint_route@4:
return

main_get_implicit_key_arc4_string_route@5:
// tests/artifacts/StateOps/contract.py:371
// tests/artifacts/StateOps/contract.py:373
// @arc4.abimethod()
txn OnCompletion
!
Expand All @@ -48,7 +50,39 @@ main_get_implicit_key_arc4_string_route@5:
int 1
return

main_bare_routing@8:
main_get_arc4_uint_route@6:
// tests/artifacts/StateOps/contract.py:377
// @arc4.abimethod()
txn OnCompletion
!
assert // OnCompletion is NoOp
txn ApplicationID
assert // is not creating
callsub get_arc4_uint
byte 0x151f7c75
swap
concat
log
int 1
return

main_get_arc4_string_route@7:
// tests/artifacts/StateOps/contract.py:381
// @arc4.abimethod()
txn OnCompletion
!
assert // OnCompletion is NoOp
txn ApplicationID
assert // is not creating
callsub get_arc4_string
byte 0x151f7c75
swap
concat
log
int 1
return

main_bare_routing@10:
// tests/artifacts/StateOps/contract.py:362
// class GlobalStateContract(ARC4Contract):
txn OnCompletion
Expand All @@ -63,17 +97,17 @@ main_bare_routing@8:

// tests.artifacts.StateOps.contract.GlobalStateContract.get_implicit_key_arc4_uint() -> bytes:
get_implicit_key_arc4_uint:
// tests/artifacts/StateOps/contract.py:367-368
// tests/artifacts/StateOps/contract.py:369-370
// @arc4.abimethod()
// def get_implicit_key_arc4_uint(self) -> arc4.UInt64:
proto 0 1
// tests/artifacts/StateOps/contract.py:369
// tests/artifacts/StateOps/contract.py:371
// return self.implicit_key_arc4_uint.value
int 0
// tests/artifacts/StateOps/contract.py:364
// self.implicit_key_arc4_uint = GlobalState(arc4.UInt64(1337))
byte "implicit_key_arc4_uint"
// tests/artifacts/StateOps/contract.py:369
// tests/artifacts/StateOps/contract.py:371
// return self.implicit_key_arc4_uint.value
app_global_get_ex
assert // check self.implicit_key_arc4_uint exists
Expand All @@ -82,23 +116,61 @@ get_implicit_key_arc4_uint:

// tests.artifacts.StateOps.contract.GlobalStateContract.get_implicit_key_arc4_string() -> bytes:
get_implicit_key_arc4_string:
// tests/artifacts/StateOps/contract.py:371-372
// tests/artifacts/StateOps/contract.py:373-374
// @arc4.abimethod()
// def get_implicit_key_arc4_string(self) -> arc4.String:
proto 0 1
// tests/artifacts/StateOps/contract.py:373
// tests/artifacts/StateOps/contract.py:375
// return self.implicit_key_arc4_string.value
int 0
// tests/artifacts/StateOps/contract.py:365
// self.implicit_key_arc4_string = GlobalState(arc4.String("Hello"))
byte "implicit_key_arc4_string"
// tests/artifacts/StateOps/contract.py:373
// tests/artifacts/StateOps/contract.py:375
// return self.implicit_key_arc4_string.value
app_global_get_ex
assert // check self.implicit_key_arc4_string exists
retsub


// tests.artifacts.StateOps.contract.GlobalStateContract.get_arc4_uint() -> bytes:
get_arc4_uint:
// tests/artifacts/StateOps/contract.py:377-378
// @arc4.abimethod()
// def get_arc4_uint(self) -> arc4.UInt64:
proto 0 1
// tests/artifacts/StateOps/contract.py:379
// return self.arc4_uint.value
int 0
// tests/artifacts/StateOps/contract.py:366
// self.arc4_uint = GlobalState(arc4.UInt64(1337), key="explicit_key_arc4_uint")
byte "explicit_key_arc4_uint"
// tests/artifacts/StateOps/contract.py:379
// return self.arc4_uint.value
app_global_get_ex
assert // check self.arc4_uint exists
retsub


// tests.artifacts.StateOps.contract.GlobalStateContract.get_arc4_string() -> bytes:
get_arc4_string:
// tests/artifacts/StateOps/contract.py:381-382
// @arc4.abimethod()
// def get_arc4_string(self) -> arc4.String:
proto 0 1
// tests/artifacts/StateOps/contract.py:383
// return self.arc4_string.value
int 0
// tests/artifacts/StateOps/contract.py:367
// self.arc4_string = GlobalState(arc4.String("Hello"), key="explicit_key_arc4_string")
byte "explicit_key_arc4_string"
// tests/artifacts/StateOps/contract.py:383
// return self.arc4_string.value
app_global_get_ex
assert // check self.arc4_string exists
retsub


// tests.artifacts.StateOps.contract.GlobalStateContract.__init__() -> void:
__init__:
// tests/artifacts/StateOps/contract.py:363
Expand All @@ -114,4 +186,14 @@ __init__:
byte "implicit_key_arc4_string"
byte 0x000548656c6c6f
app_global_put
// tests/artifacts/StateOps/contract.py:366
// self.arc4_uint = GlobalState(arc4.UInt64(1337), key="explicit_key_arc4_uint")
byte "explicit_key_arc4_uint"
byte 0x0000000000000539
app_global_put
// tests/artifacts/StateOps/contract.py:367
// self.arc4_string = GlobalState(arc4.String("Hello"), key="explicit_key_arc4_string")
byte "explicit_key_arc4_string"
byte 0x000548656c6c6f
app_global_put
retsub
Loading

0 comments on commit bcd3ee9

Please sign in to comment.