Skip to content

Commit 3dc01e2

Browse files
authored
fix: adding string representation to PasswordHash and EncryptedString (#598)
Add `__repr__` for string representation to `PasswordHash` and `EncryptedString`
1 parent 7b7cd60 commit 3dc01e2

5 files changed

Lines changed: 67 additions & 3 deletions

File tree

advanced_alchemy/alembic/templates/asyncio/script.py.mako

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ from typing import TYPE_CHECKING
1111

1212
import sqlalchemy as sa
1313
from alembic import op
14-
from advanced_alchemy.types import EncryptedString, EncryptedText, GUID, ORA_JSONB, DateTimeUTC, StoredObject, PasswordHash
14+
from advanced_alchemy.types import EncryptedString, EncryptedText, GUID, ORA_JSONB, DateTimeUTC, StoredObject, PasswordHash, FernetBackend
15+
from advanced_alchemy.types.encrypted_string import PGCryptoBackend
16+
from advanced_alchemy.types.password_hash.argon2 import Argon2Hasher
17+
from advanced_alchemy.types.password_hash.passlib import PasslibHasher
18+
from advanced_alchemy.types.password_hash.pwdlib import PwdlibHasher
1519
from sqlalchemy import Text # noqa: F401
1620
${imports if imports else ""}
1721
if TYPE_CHECKING:
@@ -25,6 +29,12 @@ sa.ORA_JSONB = ORA_JSONB
2529
sa.EncryptedString = EncryptedString
2630
sa.EncryptedText = EncryptedText
2731
sa.StoredObject = StoredObject
32+
sa.PasswordHash = PasswordHash
33+
sa.Argon2Hasher = Argon2Hasher
34+
sa.PasslibHasher = PasslibHasher
35+
sa.PwdlibHasher = PwdlibHasher
36+
sa.FernetBackend = FernetBackend
37+
sa.PGCryptoBackend = PGCryptoBackend
2838

2939
# revision identifiers, used by Alembic.
3040
revision = ${repr(up_revision)}

advanced_alchemy/alembic/templates/sync/script.py.mako

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ from typing import TYPE_CHECKING
1111

1212
import sqlalchemy as sa
1313
from alembic import op
14-
from advanced_alchemy.types import EncryptedString, EncryptedText, GUID, ORA_JSONB, DateTimeUTC, StoredObject, PasswordHash
14+
from advanced_alchemy.types import EncryptedString, EncryptedText, GUID, ORA_JSONB, DateTimeUTC, StoredObject, PasswordHash, FernetBackend
15+
from advanced_alchemy.types.encrypted_string import PGCryptoBackend
16+
from advanced_alchemy.types.password_hash.argon2 import Argon2Hasher
17+
from advanced_alchemy.types.password_hash.passlib import PasslibHasher
18+
from advanced_alchemy.types.password_hash.pwdlib import PwdlibHasher
1519
from sqlalchemy import Text # noqa: F401
1620
${imports if imports else ""}
1721
if TYPE_CHECKING:
@@ -25,6 +29,12 @@ sa.ORA_JSONB = ORA_JSONB
2529
sa.EncryptedString = EncryptedString
2630
sa.EncryptedText = EncryptedText
2731
sa.StoredObject = StoredObject
32+
sa.PasswordHash = PasswordHash
33+
sa.Argon2Hasher = Argon2Hasher
34+
sa.PasslibHasher = PasslibHasher
35+
sa.PwdlibHasher = PwdlibHasher
36+
sa.FernetBackend = FernetBackend
37+
sa.PGCryptoBackend = PGCryptoBackend
2838

2939
# revision identifiers, used by Alembic.
3040
revision = ${repr(up_revision)}

advanced_alchemy/types/encrypted_string.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,11 @@ def __init__(
243243
self.backend = backend()
244244
self.length = length
245245

246+
def __repr__(self) -> str:
247+
"""Return a string representation of the EncryptedString."""
248+
key_repr = self.key.__name__ if callable(self.key) else repr(self.key)
249+
return f"EncryptedString(key={key_repr}, backend={self.backend.__class__.__name__}, length={self.length})"
250+
246251
@property
247252
def python_type(self) -> type[str]:
248253
"""Returns the Python type for this type decorator.

advanced_alchemy/types/password_hash/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ def __init__(self, backend: "HashingBackend", length: int = 128) -> None:
107107
super().__init__(length=length)
108108
self.backend = backend
109109

110+
def __repr__(self) -> str:
111+
"""Return a string representation of the PasswordHash."""
112+
return f"PasswordHash(backend=sa.{self.backend.__class__.__name__}(), length={self.length})"
113+
110114
@property
111115
def python_type(self) -> "type[str]":
112116
"""Returns the Python type for this type decorator.

tests/integration/test_password_hash.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from sqlalchemy.orm import Mapped, Session, mapped_column, sessionmaker
1111

1212
from advanced_alchemy.base import BigIntBase
13-
from advanced_alchemy.types import PasswordHash
13+
from advanced_alchemy.types import EncryptedString, PasswordHash
14+
from advanced_alchemy.types.encrypted_string import FernetBackend, PGCryptoBackend
1415
from advanced_alchemy.types.password_hash.argon2 import Argon2Hasher
1516
from advanced_alchemy.types.password_hash.base import HashedPassword
1617
from advanced_alchemy.types.password_hash.passlib import PasslibHasher
@@ -194,3 +195,37 @@ async def test_password_hash_async(
194195
await db_session.flush()
195196
await db_session.refresh(user2)
196197
assert user2.argon2_password is None
198+
199+
200+
def test_password_hash_repr() -> None:
201+
"""Test __repr__() method for PasswordHash with different backends."""
202+
# Test Argon2Hasher backend
203+
argon2_hash = PasswordHash(backend=Argon2Hasher(), length=128)
204+
assert repr(argon2_hash) == "PasswordHash(backend=sa.Argon2Hasher(), length=128)"
205+
206+
# Test PasslibHasher backend
207+
passlib_hash = PasswordHash(backend=PasslibHasher(context=CryptContext(schemes=["argon2"])), length=256)
208+
assert repr(passlib_hash) == "PasswordHash(backend=sa.PasslibHasher(), length=256)"
209+
210+
# Test PwdlibHasher backend
211+
pwdlib_hash = PasswordHash(backend=PwdlibHasher(hasher=PwdlibArgon2Hasher()), length=512)
212+
assert repr(pwdlib_hash) == "PasswordHash(backend=sa.PwdlibHasher(), length=512)"
213+
214+
215+
def test_encrypted_string_repr() -> None:
216+
"""Test __repr__() method for EncryptedString with different backends."""
217+
# Test FernetBackend (default)
218+
enc_str_fernet = EncryptedString(key="test_key", backend=FernetBackend, length=100)
219+
assert repr(enc_str_fernet) == "EncryptedString(key='test_key', backend=FernetBackend, length=100)"
220+
221+
# Test PGCryptoBackend
222+
enc_str_pgcrypto = EncryptedString(key=b"test_bytes_key", backend=PGCryptoBackend, length=200)
223+
assert repr(enc_str_pgcrypto) == "EncryptedString(key=b'test_bytes_key', backend=PGCryptoBackend, length=200)"
224+
225+
# Test with callable key
226+
def get_key() -> str:
227+
return "dynamic_key"
228+
229+
# The repr should include the callable object itself
230+
enc_str_callable = EncryptedString(key=get_key, backend=FernetBackend)
231+
assert repr(enc_str_callable) == "EncryptedString(key=get_key, backend=FernetBackend, length=None)"

0 commit comments

Comments
 (0)