02.03.01 — Le type Result[T]#
Fichier source :
src/ocarina/railway/result.py— zéro dépendance.
Code#
from dataclasses import dataclass, field
from typing import TypeGuard, final
class _BaseResult:
"""Base class ensuring both Ok and Fail have error attribute for type narrowing."""
error: Exception | None
@final
@dataclass(frozen=True)
class Ok[T](_BaseResult):
value: T
error: None = None
@final
@dataclass(frozen=True)
class Fail(_BaseResult):
error: Exception = field(default_factory=lambda: Exception("Unknown error"))
type Result[T] = Ok[T] | Fail
def is_ok[T](result: Result[T]) -> TypeGuard[Ok[T]]:
return isinstance(result, Ok)
def is_fail[T](result: Result[T]) -> TypeGuard[Fail]:
return isinstance(result, Fail)Six décisions de design à décortiquer#
1. _BaseResult comme parent commun#
class _BaseResult:
error: Exception | NoneBut unique : permettre à mypy d’accéder à result.error sans narrowing préalable. Sans cette base, on devrait écrire :
if is_fail(result):
print(result.error) # ok, narrowed…systématiquement. Avec la base partagée, on peut écrire dans certains cas :
print(result.error or "no error")…et le checker accepte parce que les deux classes exposent error. C’est une concession ergonomique, pas une invitation à contourner le narrowing : la valeur peut être None (Ok), donc dans 99% des cas on veut narrower avant d’accéder.
2. @final partout#
@final sur Ok[T] et Fail (et sur ActionStart, ActionChain, etc. plus loin).
- Pas d’héritage utilisateur —
class MyOk(Ok[T])est une erreur mypy. - Le type-checker peut traiter
Result[T] = Ok[T] | Failcomme une union fermée (« sealed »). - L’exhaustivité du
isinstanceest garantie.
3. @dataclass(frozen=True)#
- Immutabilité :
Ok(value=42).value = 43lèveFrozenInstanceError. - Génération automatique de
__init__,__repr__,__eq__,__hash__(puisque frozen). - Permet
Ok(42) == Ok(42)
4. Fail a un default_factory#
error: Exception = field(default_factory=lambda: Exception("Unknown error"))Pour pouvoir écrire Fail() sans argument.
5. type Result[T] = Ok[T] | Fail (PEP 695)#
Syntaxe PEP 695 introduite en Python 3.12 :
type Result[T] = Ok[T] | FailÉquivalent legacy :
from typing import TypeAlias, TypeVar, Union
T = TypeVar("T")
Result: TypeAlias = Union[Ok[T], Fail]Avantages PEP 695 :
- Lisibilité (aucun import).
- Pas de fuite du
Tdans le scope du module. - Type check plus fin.
6. TypeGuard[Ok[T]] / TypeGuard[Fail]#
def is_ok[T](result: Result[T]) -> TypeGuard[Ok[T]]:
return isinstance(result, Ok)Sans TypeGuard :
def is_ok(result): return isinstance(result, Ok)
r = some_action()
if is_ok(r):
print(r.value) # mypy ne sait pas que r est Ok[T] → errorAvec TypeGuard[Ok[T]], mypy narrows r à Ok[T] dans la branche True. C’est la pierre angulaire de l’usage typé du Result dans tout le reste du framework. Voir aussi aggregates/tests_layers.py qui décline le même pattern pour TestResult :
def is_test_result_ok(result: TestResult) -> TypeGuard[Ok[Any]]: ...
def is_test_result_fail(result: TestResult) -> TypeGuard[Fail]: ...
def is_test_result_skipped(result: TestResult) -> TypeGuard[None]: ...Exemples d’utilisation (extraits du framework)#
Construction directe#
return Ok(value=42)
return Fail(error=RuntimeError("nope"))
return Fail() # error = Exception("Unknown error")Discrimination#
result = run_action()
if is_fail(result):
failure_handler(result.error)
return ActionChain(has_failed=True, result=result)
success_handler()
return ActionChain(has_failed=False, result=result)Conversion d’une exception en Fail#
def run_action() -> Result[TPOM]:
try:
result_pom = action(pom)
return Ok(result_pom)
except Exception as exc: # noqa: BLE001
if on_failure:
return on_failure(pom, exc)
return Fail(error=exc)Extrait de create_act, c’est l’unique point où une exception est convertie en Fail afin de résoudre ce problème pour tout le reste de l’exécution.
Tests associés#
Le fichier tests/scenarios/test_railway_and_action_chain.py ouvre par un test direct sur ces discriminateurs :
@allure.title("is_ok and is_fail discriminate Ok from Fail")
def test_result_discriminators_split_ok_and_fail() -> None:
ok: Ok[int] = Ok(value=42)
fail = Fail(error=RuntimeError("x"))
assert is_ok(ok)
assert not is_fail(ok)
assert is_fail(fail)
assert not is_ok(fail)Et un fichier tests/dsl/testing_with_railway/test_types.yml (cf. ../../04-internal-tests/04-mypy-plugins-types.md) teste que reveal_type(...) retourne bien ActionStart[<Page>] après un act(...). Le typage est testé.