Skip to content

Commit 877cf10

Browse files
refactor: TokenInfo readonly (800) (#915)
Signed-off-by: Antonio Ceppellini <antonio.ceppellini@gmail.com> Signed-off-by: AntonioCeppellini <128388022+AntonioCeppellini@users.noreply.github.com>
1 parent 301e4c3 commit 877cf10

File tree

3 files changed

+79
-213
lines changed

3 files changed

+79
-213
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
2222

2323
- bot workflows to include new changelog entry
2424
- Removed duplicate import of transaction_pb2 in transaction.py
25+
- Refactor `TokenInfo` into an immutable dataclass, remove all setters, and rewrite `_from_proto` as a pure factory for consistent parsing [#800]
2526
- feat: Add string representation method for `CustomFractionalFee` class and update `custom_fractional_fee.py` example.
2627
- Moved query examples to their respective domain folders to improve structure matching.
2728

29+
2830
### Fixed
2931

3032
- fixed workflow: changelog check with improved sensitivity to deletions, additions, new releases

src/hiero_sdk_python/tokens/token_info.py

Lines changed: 48 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from hiero_sdk_python.hapi.services import token_get_info_pb2 as hapi_pb
2929

3030

31-
@dataclass
31+
@dataclass(frozen=True)
3232
class TokenInfo:
3333
"""Data class for basic token details: ID, name, and symbol."""
3434
token_id: Optional[TokenId] = None
@@ -70,113 +70,6 @@ class TokenInfo:
7070
)
7171

7272

73-
# === setter methods ===
74-
def set_admin_key(self, admin_key: PublicKey) -> "TokenInfo":
75-
"""Set the admin key."""
76-
self.admin_key = admin_key
77-
return self
78-
79-
80-
def set_kyc_key(self, kyc_key: PublicKey) -> "TokenInfo":
81-
"""Set the KYC key."""
82-
self.kyc_key = kyc_key
83-
return self
84-
85-
86-
def set_freeze_key(self, freeze_key: PublicKey) -> "TokenInfo":
87-
"""Set the freeze key."""
88-
self.freeze_key = freeze_key
89-
return self
90-
91-
92-
def set_wipe_key(self, wipe_key: PublicKey) -> "TokenInfo":
93-
"""Set the wipe key."""
94-
self.wipe_key = wipe_key
95-
return self
96-
97-
98-
def set_supply_key(self, supply_key: PublicKey) -> "TokenInfo":
99-
"""Set the supply key."""
100-
self.supply_key = supply_key
101-
return self
102-
103-
104-
def set_metadata_key(self, metadata_key: PublicKey) -> "TokenInfo":
105-
"""Set the metadata key."""
106-
self.metadata_key = metadata_key
107-
return self
108-
109-
def set_fee_schedule_key(self, fee_schedule_key: PublicKey) -> "TokenInfo":
110-
"""Set the fee schedule key."""
111-
self.fee_schedule_key = fee_schedule_key
112-
return self
113-
114-
def set_default_freeze_status(self, freeze_status: TokenFreezeStatus) -> "TokenInfo":
115-
"""Set the default freeze status."""
116-
self.default_freeze_status = freeze_status
117-
return self
118-
119-
120-
def set_default_kyc_status(self, kyc_status: TokenKycStatus) -> "TokenInfo":
121-
"""Set the default KYC status."""
122-
self.default_kyc_status = kyc_status
123-
return self
124-
125-
126-
def set_auto_renew_account(self, account: AccountId) -> "TokenInfo":
127-
"""Set the auto-renew account."""
128-
self.auto_renew_account = account
129-
return self
130-
131-
132-
def set_auto_renew_period(self, period: Duration) -> "TokenInfo":
133-
"""Set the auto-renew period."""
134-
self.auto_renew_period = period
135-
return self
136-
137-
138-
def set_expiry(self, expiry: Timestamp) -> "TokenInfo":
139-
"""Set the token expiry."""
140-
self.expiry = expiry
141-
return self
142-
143-
def set_pause_key(self, pause_key: PublicKey) -> "TokenInfo":
144-
"""Set the pause key."""
145-
self.pause_key = pause_key
146-
return self
147-
148-
def set_pause_status(self, pause_status: TokenPauseStatus) -> "TokenInfo":
149-
"""Set the pause status."""
150-
self.pause_status = pause_status
151-
return self
152-
153-
154-
def set_supply_type(self, supply_type: SupplyType | int) -> "TokenInfo":
155-
"""Set the supply type."""
156-
self.supply_type = (
157-
supply_type
158-
if isinstance(supply_type, SupplyType)
159-
else SupplyType(supply_type)
160-
)
161-
return self
162-
163-
164-
def set_metadata(self, metadata: bytes) -> "TokenInfo":
165-
"""Set the token metadata."""
166-
self.metadata = metadata
167-
return self
168-
169-
def set_custom_fees(self, custom_fees: List[Any]) -> "TokenInfo":
170-
"""Set the custom fees."""
171-
self.custom_fees = custom_fees
172-
return self
173-
174-
175-
# === helpers ===
176-
177-
178-
179-
18073
@staticmethod
18174
def _get(proto_obj, *names):
18275
"""Get the first present attribute from a list of possible names (camelCase/snake_case)."""
@@ -185,6 +78,15 @@ def _get(proto_obj, *names):
18578
return getattr(proto_obj, n)
18679
return None
18780

81+
@staticmethod
82+
def _public_key_from_oneof(key_msg) -> Optional[PublicKey]:
83+
"""
84+
Extract a PublicKey from a key oneof, or None if not present.
85+
"""
86+
if key_msg is not None and hasattr(key_msg, "WhichOneof") and key_msg.WhichOneof("key"):
87+
return PublicKey._from_proto(key_msg)
88+
return None
89+
18890
# === conversions ===
18991
@classmethod
19092
def _from_proto(cls, proto_obj: hapi_pb.TokenInfo) -> "TokenInfo":
@@ -193,60 +95,56 @@ def _from_proto(cls, proto_obj: hapi_pb.TokenInfo) -> "TokenInfo":
19395
:param proto_obj: The token_get_info_pb2.TokenInfo object.
19496
:return: An instance of TokenInfo.
19597
"""
196-
tokenInfoObject = TokenInfo(
197-
token_id=TokenId._from_proto(proto_obj.tokenId),
198-
name=proto_obj.name,
199-
symbol=proto_obj.symbol,
200-
decimals=proto_obj.decimals,
201-
total_supply=proto_obj.totalSupply,
202-
treasury=AccountId._from_proto(proto_obj.treasury),
203-
is_deleted=proto_obj.deleted,
204-
memo=proto_obj.memo,
205-
token_type=TokenType(proto_obj.tokenType),
206-
max_supply=proto_obj.maxSupply,
207-
ledger_id=proto_obj.ledger_id,
208-
metadata=proto_obj.metadata,
209-
)
210-
211-
tokenInfoObject.set_custom_fees(cls._parse_custom_fees(proto_obj))
98+
kwargs: Dict[str, Any] = {
99+
"token_id": TokenId._from_proto(proto_obj.tokenId),
100+
"name": proto_obj.name,
101+
"symbol": proto_obj.symbol,
102+
"decimals": proto_obj.decimals,
103+
"total_supply": proto_obj.totalSupply,
104+
"treasury": AccountId._from_proto(proto_obj.treasury),
105+
"is_deleted": proto_obj.deleted,
106+
"memo": proto_obj.memo,
107+
"token_type": TokenType(proto_obj.tokenType),
108+
"max_supply": proto_obj.maxSupply,
109+
"ledger_id": proto_obj.ledger_id,
110+
"metadata": proto_obj.metadata,
111+
"custom_fees": cls._parse_custom_fees(proto_obj),
112+
}
212113

213114
key_sources = [
214-
(("adminKey",), "set_admin_key"),
215-
(("kycKey",), "set_kyc_key"),
216-
(("freezeKey",), "set_freeze_key"),
217-
(("wipeKey",), "set_wipe_key"),
218-
(("supplyKey",), "set_supply_key"),
219-
(("metadataKey", "metadata_key"), "set_metadata_key"),
220-
(("feeScheduleKey", "fee_schedule_key"),"set_fee_schedule_key"),
221-
(("pauseKey", "pause_key"), "set_pause_key"),
115+
(("adminKey",), "admin_key"),
116+
(("kycKey",), "kyc_key"),
117+
(("freezeKey",), "freeze_key"),
118+
(("wipeKey",), "wipe_key"),
119+
(("supplyKey",), "supply_key"),
120+
(("metadataKey", "metadata_key"), "metadata_key"),
121+
(("feeScheduleKey", "fee_schedule_key"),"fee_schedule_key"),
122+
(("pauseKey", "pause_key"), "pause_key"),
222123
]
223-
for names, setter in key_sources:
124+
for names, attr_name in key_sources:
224125
key_msg = cls._get(proto_obj, *names)
225-
cls._copy_key_if_present(tokenInfoObject, setter, key_msg)
126+
public_key = cls._public_key_from_oneof(key_msg)
127+
if public_key is not None:
128+
kwargs[attr_name] = public_key
226129

227130
conv_map = [
228-
(("defaultFreezeStatus",), tokenInfoObject.set_default_freeze_status, TokenFreezeStatus._from_proto),
229-
(("defaultKycStatus",), tokenInfoObject.set_default_kyc_status, TokenKycStatus._from_proto),
230-
(("autoRenewAccount",), tokenInfoObject.set_auto_renew_account, AccountId._from_proto),
231-
(("autoRenewPeriod",), tokenInfoObject.set_auto_renew_period, Duration._from_proto),
232-
(("expiry",), tokenInfoObject.set_expiry, Timestamp._from_protobuf),
233-
(("pauseStatus", "pause_status"), tokenInfoObject.set_pause_status, TokenPauseStatus._from_proto),
234-
(("supplyType",), tokenInfoObject.set_supply_type, SupplyType),
131+
(("defaultFreezeStatus",), "default_freeze_status", TokenFreezeStatus._from_proto),
132+
(("defaultKycStatus",), "default_kyc_status", TokenKycStatus._from_proto),
133+
(("autoRenewAccount",), "auto_renew_account", AccountId._from_proto),
134+
(("autoRenewPeriod",), "auto_renew_period", Duration._from_proto),
135+
(("expiry",), "expiry", Timestamp._from_protobuf),
136+
(("pauseStatus", "pause_status"), "pause_status", TokenPauseStatus._from_proto),
137+
(("supplyType",), "supply_type", SupplyType),
235138
]
236-
for names, setter, conv in conv_map:
139+
140+
for names, attr_name, conv in conv_map:
237141
val = cls._get(proto_obj, *names)
238142
if val is not None:
239-
setter(conv(val))
143+
kwargs[attr_name] = conv(val)
240144

241-
return tokenInfoObject
145+
return cls(**kwargs)
242146

243147
# === helpers ===
244-
@staticmethod
245-
def _copy_key_if_present(dst: "TokenInfo", setter: str, key_msg) -> None:
246-
# In proto3, keys are a oneof; check presence via WhichOneof
247-
if key_msg is not None and hasattr(key_msg, "WhichOneof") and key_msg.WhichOneof("key"):
248-
getattr(dst, setter)(PublicKey._from_proto(key_msg))
249-
250148
@staticmethod
251149
def _parse_custom_fees(proto_obj) -> List[CustomFee]:
252150
out: List[CustomFee] = []

tests/unit/test_token_info.py

Lines changed: 29 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import pytest
22

3+
from dataclasses import FrozenInstanceError, replace
4+
35
import hiero_sdk_python.hapi.services.basic_types_pb2
46
from hiero_sdk_python.tokens.token_info import TokenInfo, TokenId, AccountId, Timestamp
57
from hiero_sdk_python.crypto.private_key import PrivateKey
@@ -77,50 +79,10 @@ def test_token_info_initialization(token_info):
7779
assert token_info.expiry is None
7880
assert token_info.pause_key is None
7981

80-
def test_setters(token_info):
81-
public_key = PrivateKey.generate_ed25519().public_key()
82-
token_info.set_admin_key(public_key)
83-
assert token_info.admin_key == public_key
84-
85-
token_info.set_kyc_key(public_key)
86-
assert token_info.kyc_key == public_key
87-
88-
token_info.set_freeze_key(public_key)
89-
assert token_info.freeze_key == public_key
90-
91-
token_info.set_wipe_key(public_key)
92-
assert token_info.wipe_key == public_key
93-
94-
token_info.set_supply_key(public_key)
95-
assert token_info.supply_key == public_key
96-
97-
token_info.set_fee_schedule_key(public_key)
98-
assert token_info.fee_schedule_key == public_key
99-
100-
token_info.set_default_freeze_status(TokenFreezeStatus.FROZEN)
101-
assert token_info.default_freeze_status == TokenFreezeStatus.FROZEN
102-
103-
token_info.set_default_kyc_status(TokenKycStatus.GRANTED)
104-
assert token_info.default_kyc_status == TokenKycStatus.GRANTED
105-
106-
token_info.set_auto_renew_account(AccountId(0, 0, 300))
107-
assert token_info.auto_renew_account == AccountId(0, 0, 300)
108-
109-
token_info.set_auto_renew_period(Duration(3600))
110-
assert token_info.auto_renew_period == Duration(3600)
111-
112-
expiry = Timestamp(1625097600, 0)
113-
token_info.set_expiry(expiry)
114-
assert token_info.expiry == expiry
115-
116-
token_info.set_pause_key(public_key)
117-
assert token_info.pause_key == public_key
118-
119-
token_info.set_pause_status(TokenPauseStatus.PAUSED)
120-
assert token_info.pause_status == TokenPauseStatus.PAUSED
121-
122-
token_info.set_supply_type(SupplyType.INFINITE)
123-
assert token_info.supply_type == SupplyType.INFINITE
82+
def test_token_info_is_immutable(token_info):
83+
"""TokenInfo deve essere immutabile (dataclass frozen)."""
84+
with pytest.raises(FrozenInstanceError):
85+
token_info.name = "Changed"
12486

12587
def test_from_proto(proto_token_info):
12688
public_key = PrivateKey.generate_ed25519().public_key()
@@ -165,27 +127,31 @@ def test_from_proto(proto_token_info):
165127
assert token_info.auto_renew_period == Duration(3600)
166128
assert token_info.expiry == Timestamp(1625097600, 0)
167129
assert token_info.pause_key.to_bytes_raw() == public_key.to_bytes_raw()
168-
assert token_info.pause_status == TokenPauseStatus.PAUSED.value
169-
assert token_info.supply_type.value == SupplyType.INFINITE.value
130+
assert token_info.pause_status == TokenPauseStatus.PAUSED
131+
assert token_info.supply_type == SupplyType.INFINITE
170132

171133
def test_to_proto(token_info):
172134
public_key = PrivateKey.generate_ed25519().public_key()
173-
token_info.set_admin_key(public_key)
174-
token_info.set_kyc_key(public_key)
175-
token_info.set_freeze_key(public_key)
176-
token_info.set_wipe_key(public_key)
177-
token_info.set_supply_key(public_key)
178-
token_info.set_fee_schedule_key(public_key)
179-
token_info.set_pause_key(public_key)
180-
token_info.set_default_freeze_status(TokenFreezeStatus.FROZEN)
181-
token_info.set_default_kyc_status(TokenKycStatus.GRANTED)
182-
token_info.set_auto_renew_account(AccountId(0, 0, 300))
183-
token_info.set_auto_renew_period(Duration(3600))
184-
token_info.set_expiry(Timestamp(1625097600, 0))
185-
token_info.set_pause_status(TokenPauseStatus.PAUSED)
186-
token_info.set_supply_type(SupplyType.INFINITE)
187135

188-
proto = token_info._to_proto()
136+
full_token_info = replace(
137+
token_info,
138+
admin_key=public_key,
139+
kyc_key=public_key,
140+
freeze_key=public_key,
141+
wipe_key=public_key,
142+
supply_key=public_key,
143+
fee_schedule_key=public_key,
144+
pause_key=public_key,
145+
default_freeze_status=TokenFreezeStatus.FROZEN,
146+
default_kyc_status=TokenKycStatus.GRANTED,
147+
auto_renew_account=AccountId(0, 0, 300),
148+
auto_renew_period=Duration(3600),
149+
expiry=Timestamp(1625097600, 0),
150+
pause_status=TokenPauseStatus.PAUSED,
151+
supply_type=SupplyType.INFINITE,
152+
)
153+
154+
proto = full_token_info._to_proto()
189155

190156
assert proto.tokenId == TokenId(0, 0, 100)._to_proto()
191157
assert proto.name == "TestToken"
@@ -212,7 +178,7 @@ def test_to_proto(token_info):
212178
assert proto.autoRenewPeriod == Duration(3600)._to_proto()
213179
assert proto.expiry == Timestamp(1625097600, 0)._to_protobuf()
214180
assert proto.pause_key.ed25519 == public_key.to_bytes_raw()
215-
assert proto.pause_status == TokenPauseStatus.PAUSED.value
181+
assert proto.pause_status == TokenPauseStatus.PAUSED
216182

217183
def test_str_representation(token_info):
218184
expected = (
@@ -223,4 +189,4 @@ def test_str_representation(token_info):
223189
f"token_type={token_info.token_type}, max_supply={token_info.max_supply}, "
224190
f"ledger_id={token_info.ledger_id!r}, metadata={token_info.metadata!r})"
225191
)
226-
assert str(token_info) == expected
192+
assert str(token_info) == expected

0 commit comments

Comments
 (0)