Skip to content

Instantly share code, notes, and snippets.

@qexat
Last active March 12, 2024 14:07
Show Gist options
  • Save qexat/cfa25c740d959aa6a68bc02bf019512f to your computer and use it in GitHub Desktop.
Save qexat/cfa25c740d959aa6a68bc02bf019512f to your computer and use it in GitHub Desktop.
Python implementation of Solution 3 from Let's Get Rusty's "Improve your Rust APIs with the type state pattern" video
"""
PoC of an equivalent Python implementation of the third solution borrowed from the following video:
<https://www.youtube.com/watch?v=_ccDqRTx-JU>
Important note: Python static type checkers are independent from the interpreter, and type checking failing
does not prevent from running code. Considering that, the following code does NOT perform runtime checks for
safety. `add_password()` is still technically accessible, even through a `PasswordManager` object in a locked
state (i.e. `PasswordManager[Literal[True]]`).
The program also requires the following dependencies:
- `result` (installable from the PyPI)
"""
from __future__ import annotations
from typing import Generic
from typing import Literal
from typing import TYPE_CHECKING
from typing import TypeVar
# pip install result
from result import Err
from result import Ok
from result import Result
LockState = TypeVar("LockState", Literal[True], Literal[False])
class PasswordError(ValueError):
pass
class PasswordManager(Generic[LockState]):
@classmethod
def new(
cls: type[PasswordManager[Literal[True]]],
master_pass: str,
) -> PasswordManager[Literal[True]]:
return cls(master_pass)
def __init__(
self,
master_pass: str,
passwords: dict[str, str] = {},
is_locked: LockState = True,
) -> None:
self.__master_pass = master_pass
self.__passwords: dict[str, str] = passwords
if TYPE_CHECKING: # <=> PhantomData, sort of
self.__is_locked = is_locked
def unlock(
self: PasswordManager[Literal[True]],
master_pass: str,
) -> Result[PasswordManager[Literal[False]], PasswordError]:
if self.__master_pass != master_pass:
return Err(PasswordError("invalid master password"))
return Ok(PasswordManager(self.__master_pass, self.__passwords, False))
def lock(self: PasswordManager[Literal[False]]) -> PasswordManager[Literal[True]]:
return PasswordManager(self.__master_pass, self.__passwords, True)
def list_passwords(self: PasswordManager[Literal[False]]) -> dict[str, str]:
return self.__passwords
def add_password(
self: PasswordManager[Literal[False]],
username: str,
password: str,
) -> None:
self.__passwords[username] = password
def encryption(self) -> str:
todoǃ()
@property
def version(self) -> str:
return "1.0.0"
# Don't get fooled, it's not an exclamation mark, it's a retroflex click symbol x)
def todoǃ():
raise SystemExit(1)
def main() -> None:
manager = PasswordManager.new("password123")
# manager.add_password("qexat", "abcdefg") # Pyright & MyPy report an error here, as expected
match manager.unlock("password123"):
case Ok(manager):
pass
case Err(e):
raise e
print(manager.list_passwords())
manager.add_password("username", "gEnErIcS_<3") # No error reported here!
manager = manager.lock()
if __name__ == "__main__":
main()
@qexat
Copy link
Author

qexat commented Mar 1, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment