Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
## [Unreleased]

### Added
- Refactored dataclass string representations: simplified enum detection with `isinstance(value, enum.Enum)` and added `hasattr` in `dataclass_strings_test.py` .
- Refactored token classes to use dataclass-driven `__str__` and `__repr__` generation. [#999](https://github.com/hiero-ledger/hiero-sdk-python/issues/999)
- Added unit test for 'endpoint.py' to increase coverage.
- Automated assignment guard for `advanced` issues; requires completion of at least one `good first issue` and one `intermediate` issue before assignment (exempts maintainers, committers, and triage members). (#1142)
- Added Hbar object support for TransferTransaction HBAR transfers:
Expand Down
30 changes: 29 additions & 1 deletion src/hiero_sdk_python/tokens/custom_fee.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import typing
from abc import ABC, abstractmethod

from hiero_sdk_python.utils.dataclass_strings import _format_value_helper

if typing.TYPE_CHECKING:
from hiero_sdk_python.account.account_id import AccountId
from hiero_sdk_python.client.client import Client
Expand Down Expand Up @@ -148,4 +150,30 @@ def __eq__(self, other: object) -> bool:
if not isinstance(other, CustomFee):
return NotImplemented

return self.fee_collector_account_id == other.fee_collector_account_id and self.all_collectors_are_exempt == other.all_collectors_are_exempt
return self.fee_collector_account_id == other.fee_collector_account_id and self.all_collectors_are_exempt == other.all_collectors_are_exempt

def __str__(self) -> str:
"""Return a dynamic string representation including all public instance attributes.

This method dynamically inspects all public (non-underscore-prefixed) instance
attributes, ensuring that new fields added to subclasses are automatically
included without manual updates, while avoiding exposure of internal state.
"""
fields = []
for key, value in self.__dict__.items():
# Skip private or internal attributes
if key.startswith("_"):
continue
formatted_value = _format_value_helper(value)
fields.append(f"{key}={formatted_value}")

class_name = self.__class__.__name__
if len(fields) <= 3:
return f"{class_name}({', '.join(fields)})"
else:
fields_str = ",\n ".join(fields)
return f"{class_name}(\n {fields_str}\n)"

def __repr__(self) -> str:
"""Return a string representation for debugging."""
return self.__str__()
3 changes: 2 additions & 1 deletion src/hiero_sdk_python/tokens/token_relationship.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
from hiero_sdk_python.tokens.token_freeze_status import TokenFreezeStatus
from hiero_sdk_python.tokens.token_id import TokenId
from hiero_sdk_python.tokens.token_kyc_status import TokenKycStatus
from hiero_sdk_python.utils.dataclass_strings import DataclassStringMixin

@dataclass
class TokenRelationship:
class TokenRelationship(DataclassStringMixin):
"""
Represents a relationship between an account and a token.

Expand Down
5 changes: 3 additions & 2 deletions src/hiero_sdk_python/tokens/token_update_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
)
from hiero_sdk_python.hapi.services import token_update_pb2, transaction_pb2
from hiero_sdk_python.utils.key_utils import Key, key_to_proto
from hiero_sdk_python.utils.dataclass_strings import DataclassStringMixin

@dataclass
class TokenUpdateParams:
class TokenUpdateParams(DataclassStringMixin):
"""
Represents token attributes that can be updated.

Expand All @@ -46,7 +47,7 @@ class TokenUpdateParams:
expiration_time: Optional[Timestamp] = None

@dataclass
class TokenUpdateKeys:
class TokenUpdateKeys(DataclassStringMixin):
"""
Represents cryptographic keys that can be updated for a token.
Does not include treasury_key which is for transaction signing.
Expand Down
186 changes: 186 additions & 0 deletions src/hiero_sdk_python/utils/dataclass_strings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
"""
hiero_sdk_python.utils.dataclass_strings
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Provides automatic __str__ and __repr__ generation for dataclasses.

This module eliminates manual maintenance of string methods by dynamically
generating them based on dataclass fields. When new fields are added, they
are automatically included in the string representation.

Usage:
@dataclass
class MyTokenClass(DataclassStringMixin):
field1: str
field2: Optional[int] = None

# Or use the decorator approach:
@auto_str_repr
@dataclass
class MyTokenClass:
field1: str
field2: Optional[int] = None
"""

import dataclasses
import enum
from typing import Any


def _is_enum(value: Any) -> bool:
"""Check if a value is an Enum instance."""
return isinstance(value, enum.Enum)


def _format_bytes(value: bytes) -> str:
"""Format bytes value with truncation for display."""
if len(value) > 20:
return f"b'{value[:20].hex()}...'"
return f"b'{value.hex()}'"


def _format_value_helper(value: Any) -> str:
"""Format a field value for string representation."""
if value is None:
return "None"

if isinstance(value, str):
return f"'{value}'"

if isinstance(value, bytes):
return _format_bytes(value)

if _is_enum(value):
return f"{value.__class__.__name__}.{value.name}"

return str(value)


class DataclassStringMixin:
"""
Mixin class that provides automatic __str__ and __repr__ implementations
for dataclasses based on their fields.

This mixin automatically generates string representations that include
all dataclass fields, handling None values and nested objects appropriately.

Features:
- Zero maintenance: new fields are automatically included
- Proper None handling for optional fields
- Clean formatting (multi-line for >3 fields)
- Enum value formatting (shows EnumClass.VALUE)
- String quoting for clarity
- Dictionary conversion for serialization

Example:
>>> @dataclass
... class TokenInfo(DataclassStringMixin):
... token_id: str
... symbol: Optional[str] = None
... balance: int = 0
...
>>> token = TokenInfo("0.0.123", "TEST", 1000)
>>> str(token)
"TokenInfo(token_id='0.0.123', symbol='TEST', balance=1000)"
"""

def __str__(self) -> str:
"""Generate string representation dynamically from dataclass fields."""
if not dataclasses.is_dataclass(self):
return f"{self.__class__.__name__}()"

field_strings = []
for field in dataclasses.fields(self.__class__):
field_value = getattr(self, field.name)
formatted_value = self._format_field_value(field_value)
field_strings.append(f"{field.name}={formatted_value}")

# Choose formatting based on number of fields
class_name = self.__class__.__name__
if len(field_strings) <= 3:
return f"{class_name}({', '.join(field_strings)})"
else:
fields_str = ",\n ".join(field_strings)
return f"{class_name}(\n {fields_str}\n)"

def __repr__(self) -> str:
"""Return string representation for debugging."""
return self.__str__()

def _format_field_value(self, value: Any) -> str:
"""Format a field value for string representation."""
return _format_value_helper(value)

def to_dict(self) -> dict[str, Any]:
"""Convert dataclass to dictionary for serialization.

Returns:
dict[str, Any]: Dictionary representation of the dataclass.
"""
if not dataclasses.is_dataclass(self):
return {}

result = {}
for field in dataclasses.fields(self.__class__):
value = getattr(self, field.name)
if hasattr(value, 'to_dict'):
result[field.name] = value.to_dict()
elif dataclasses.is_dataclass(value) and not isinstance(value, type):
result[field.name] = dataclasses.asdict(value)
else:
result[field.name] = value
return result


def auto_str_repr(cls):
"""
Class decorator that automatically adds __str__ and __repr__ methods
to dataclasses using dynamic field introspection.

This decorator is an alternative to using DataclassStringMixin inheritance.

Usage:
@auto_str_repr
@dataclass
class TokenClass:
field1: str
field2: Optional[int] = None

Args:
cls: The dataclass to decorate.

Returns:
The decorated class with __str__ and __repr__ methods.

Raises:
TypeError: If the decorated class is not a dataclass.
"""
if not dataclasses.is_dataclass(cls):
raise TypeError(f"@auto_str_repr can only be applied to dataclasses, got {cls.__name__}")

def _format_value(value: Any) -> str:
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _format_value function duplicates logic from DataclassStringMixin._format_field_value. Consider extracting this shared formatting logic into a standalone function to reduce duplication and improve maintainability.

Copilot uses AI. Check for mistakes.
"""Format a field value for string representation."""
return _format_value_helper(value)

def __str__(self) -> str:
"""Generate string representation dynamically from dataclass fields."""
field_strings = []
for field in dataclasses.fields(self.__class__):
field_value = getattr(self, field.name)
formatted_value = _format_value(field_value)
field_strings.append(f"{field.name}={formatted_value}")

if len(field_strings) <= 3:
return f"{cls.__name__}({', '.join(field_strings)})"
else:
fields_str = ",\n ".join(field_strings)
return f"{cls.__name__}(\n {fields_str}\n)"

def __repr__(self) -> str:
"""Return string representation for debugging."""
return self.__str__()

cls.__str__ = __str__
cls.__repr__ = __repr__

return cls
Loading