snekok 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- snekok-0.1.0/PKG-INFO +65 -0
- snekok-0.1.0/README.md +55 -0
- snekok-0.1.0/docs/research/README.md +40 -0
- snekok-0.1.0/docs/research/better-result.md +86 -0
- snekok-0.1.0/docs/research/returns.md +233 -0
- snekok-0.1.0/docs/result.md +92 -0
- snekok-0.1.0/pyproject.toml +47 -0
- snekok-0.1.0/src/snekok/__init__.py +0 -0
- snekok-0.1.0/src/snekok/py.typed +0 -0
- snekok-0.1.0/src/snekok/result.py +157 -0
- snekok-0.1.0/src/snekok/types.py +30 -0
- snekok-0.1.0/src/snekok/validation.py +40 -0
- snekok-0.1.0/tests/__init__.py +1 -0
- snekok-0.1.0/tests/test_result.py +159 -0
- snekok-0.1.0/tests/test_secret.py +147 -0
- snekok-0.1.0/tests/test_validation.py +75 -0
- snekok-0.1.0/tests/typecheck_result.py +63 -0
- snekok-0.1.0/tests/typecheck_secret.py +32 -0
- snekok-0.1.0/uv.lock +279 -0
snekok-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: snekok
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Small, typed tools for treating expected failures as values
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Requires-Dist: annotated-types>=0.7.0
|
|
7
|
+
Requires-Dist: pydantic>=2.13.4
|
|
8
|
+
Requires-Dist: typing-extensions>=4.15.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# snekok
|
|
12
|
+
|
|
13
|
+
Small, typed tools for treating expected failures as values in Python.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from snekok.result import Err, Ok, Result
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_port(raw: str) -> Result[int, str]:
|
|
20
|
+
if raw.isdecimal():
|
|
21
|
+
return Ok(int(raw))
|
|
22
|
+
return Err("port must be an integer")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
port = parse_port("8080")
|
|
26
|
+
if isinstance(port, Err):
|
|
27
|
+
print(port.error)
|
|
28
|
+
else:
|
|
29
|
+
print(port.unwrap())
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The `Result` API deliberately stays small: `Ok`, `Err`, `Result`, `unwrap`,
|
|
33
|
+
`unwrap_error`, and the `map`, `map_error`, `and_then`, and `and_then_async`
|
|
34
|
+
composition methods required by concrete consumers. It does not attempt to provide a
|
|
35
|
+
functional-programming framework.
|
|
36
|
+
|
|
37
|
+
## Validated scalar aliases
|
|
38
|
+
|
|
39
|
+
`NonEmptySecretStr`, `NonEmptyStr`, and `NonNegativeInt` are constrained nominal
|
|
40
|
+
aliases with Pydantic support:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from pydantic import BaseModel
|
|
44
|
+
|
|
45
|
+
from snekok.types import NonEmptySecretStr, NonEmptyStr, NonNegativeInt
|
|
46
|
+
from snekok.validation import validate_python
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ApiSettings(BaseModel):
|
|
50
|
+
api_key: NonEmptySecretStr
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
label = validate_python(NonEmptyStr, "hello").unwrap()
|
|
54
|
+
retry_count = validate_python(NonNegativeInt, 0).unwrap()
|
|
55
|
+
settings = ApiSettings.model_validate({"api_key": "secret-value"})
|
|
56
|
+
assert settings.api_key.get_secret_value() == "secret-value"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Pydantic rejects an empty value. Static type checkers also reject an ordinary
|
|
60
|
+
`SecretStr` where `NonEmptySecretStr` is required, preventing unvalidated secrets
|
|
61
|
+
from crossing the typed boundary.
|
|
62
|
+
|
|
63
|
+
See [`docs/result.md`](docs/result.md) for the Result contract. Pinned design references
|
|
64
|
+
for `dmmulroy/better-result` and `dry-python/returns` live in
|
|
65
|
+
[`docs/research/`](docs/research/).
|
snekok-0.1.0/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# snekok
|
|
2
|
+
|
|
3
|
+
Small, typed tools for treating expected failures as values in Python.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from snekok.result import Err, Ok, Result
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_port(raw: str) -> Result[int, str]:
|
|
10
|
+
if raw.isdecimal():
|
|
11
|
+
return Ok(int(raw))
|
|
12
|
+
return Err("port must be an integer")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
port = parse_port("8080")
|
|
16
|
+
if isinstance(port, Err):
|
|
17
|
+
print(port.error)
|
|
18
|
+
else:
|
|
19
|
+
print(port.unwrap())
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The `Result` API deliberately stays small: `Ok`, `Err`, `Result`, `unwrap`,
|
|
23
|
+
`unwrap_error`, and the `map`, `map_error`, `and_then`, and `and_then_async`
|
|
24
|
+
composition methods required by concrete consumers. It does not attempt to provide a
|
|
25
|
+
functional-programming framework.
|
|
26
|
+
|
|
27
|
+
## Validated scalar aliases
|
|
28
|
+
|
|
29
|
+
`NonEmptySecretStr`, `NonEmptyStr`, and `NonNegativeInt` are constrained nominal
|
|
30
|
+
aliases with Pydantic support:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from pydantic import BaseModel
|
|
34
|
+
|
|
35
|
+
from snekok.types import NonEmptySecretStr, NonEmptyStr, NonNegativeInt
|
|
36
|
+
from snekok.validation import validate_python
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ApiSettings(BaseModel):
|
|
40
|
+
api_key: NonEmptySecretStr
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
label = validate_python(NonEmptyStr, "hello").unwrap()
|
|
44
|
+
retry_count = validate_python(NonNegativeInt, 0).unwrap()
|
|
45
|
+
settings = ApiSettings.model_validate({"api_key": "secret-value"})
|
|
46
|
+
assert settings.api_key.get_secret_value() == "secret-value"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Pydantic rejects an empty value. Static type checkers also reject an ordinary
|
|
50
|
+
`SecretStr` where `NonEmptySecretStr` is required, preventing unvalidated secrets
|
|
51
|
+
from crossing the typed boundary.
|
|
52
|
+
|
|
53
|
+
See [`docs/result.md`](docs/result.md) for the Result contract. Pinned design references
|
|
54
|
+
for `dmmulroy/better-result` and `dry-python/returns` live in
|
|
55
|
+
[`docs/research/`](docs/research/).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Result design research
|
|
2
|
+
|
|
3
|
+
These notes preserve the primary-source research that informed snekok. They are
|
|
4
|
+
a design reference, not snekok's public contract; the contract lives in
|
|
5
|
+
[`../result.md`](../result.md).
|
|
6
|
+
|
|
7
|
+
## Snapshots
|
|
8
|
+
|
|
9
|
+
| Reference | Pinned source | Best used for |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| [`dmmulroy/better-result`](better-result.md) | 3.0.1, `75d0b106` | Result algebra, typed errors, generator composition, async, and transport tradeoffs |
|
|
12
|
+
| [`dry-python/returns`](returns.md) | 0.29.0, `cf5e2548` | Result algebra, typing, exception capture, pattern matching, and complexity tradeoffs |
|
|
13
|
+
|
|
14
|
+
## Quick comparison
|
|
15
|
+
|
|
16
|
+
| Concern | `better-result` | `returns` | Current snekok direction |
|
|
17
|
+
| --- | --- | --- | --- |
|
|
18
|
+
| Core shape | `Ok<T, E> | Err<T, E>` concrete classes | `Success[T]` / `Failure[E]` container | `Ok[T] | Err[E]` |
|
|
19
|
+
| Expected failures | Explicit `Err`; extraction may throw | Explicit `Failure`; extraction may throw | Expected failures are values; programmer faults remain exceptions |
|
|
20
|
+
| Composition | `map`, `mapError`, `andThen`, recovery, generator `yield*` | `map`, `bind`, `alt`, `lash`, pipelines | Add only operations demanded by real refactors |
|
|
21
|
+
| Exception capture | Explicit adapters; callback defects become `Panic` | Explicit `safe` / `future_safe` adapters | Never catch implicitly in `map` or composition |
|
|
22
|
+
| Async | Ordinary `Promise<Result>` plus helpers | Separate `FutureResult`/`IOResult` stack | Prefer async functions returning ordinary `Result` initially |
|
|
23
|
+
| Typing | Conditional types and overloads; no plugin | Covariance, HKT emulation, mypy plugin | Covariant, Pyright-native types without plugins |
|
|
24
|
+
| Validation | First error or partition; no accumulation type | First failure; no core accumulation type | Keep fail-fast semantics; separate validation if needed |
|
|
25
|
+
| Pattern matching | Discriminant, `match`, and tagged errors | Structural matching supported | Structural matching is the initial consumption API |
|
|
26
|
+
|
|
27
|
+
## Using these notes
|
|
28
|
+
|
|
29
|
+
Before adding a snekok operation:
|
|
30
|
+
|
|
31
|
+
1. Start from a concrete consumer refactor and its public test seam.
|
|
32
|
+
2. Check how both references name and type the operation.
|
|
33
|
+
3. Preserve explicit exception boundaries and ordinary Python discoverability.
|
|
34
|
+
4. Prefer a small method or function over HKT, point-free, or effect-container
|
|
35
|
+
machinery.
|
|
36
|
+
5. Record deliberate divergences in the relevant research note or public
|
|
37
|
+
contract.
|
|
38
|
+
|
|
39
|
+
Re-check upstream before relying on implementation details: these are immutable,
|
|
40
|
+
pinned snapshots, not claims about the projects' latest releases.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Research: dmmulroy/better-result
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Primary-source baseline: **better-result 3.0.1**, released **2026-08-11**, commit **`75d0b10654370597d4cad103c5c825d5acd7d3e1`**. It is a TypeScript Result library built around `Ok<T, E> | Err<T, E>`, typed error unions, generator composition, tagged `Error` subclasses, and a strict distinction between expected failures and defects. It is ESM-only, requires TypeScript 5.4 or newer, has no runtime dependencies, and builds to 10.66 kB minified or about 3.25 kB gzip.
|
|
6
|
+
|
|
7
|
+
The design is more relevant to our research than Better Auth. Better Auth showed one application's mixed exception/value boundaries. better-result is a deliberate Result API, so it gives us concrete answers about construction, composition, error typing, async work, extraction, collections, transport, and callback exceptions.
|
|
8
|
+
|
|
9
|
+
Its best ideas are the small discriminated runtime shape, inferred error unions, linear generator workflows, ordinary `Promise<Result>` async functions, and tagged domain errors. Its weak points come from ambition in the type layer. The 3.0.1 declaration file is 64 kB, overloads are extensive, and four open issues document inference or callback problems in `match`, partial error matching, taps, and detached tagged-error guards. Runtime values are also mutable despite readonly TypeScript declarations.
|
|
10
|
+
|
|
11
|
+
## Findings
|
|
12
|
+
|
|
13
|
+
1. **[informational] The runtime model is direct and inspectable.** `Result<T, E>` is `Ok<T, E> | Err<T, E>`. Both classes expose a serializable `status` discriminant. `Ok` stores `value`; `Err` stores `error`. Callers can narrow with the discriminant, instance guards, or static guards. The second type parameter on `Ok` and first on `Err` are phantom lanes used to preserve inference through unions and generators. This is less abstract than dry-python/returns' interface and HKT stack. [`core.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/core.ts) · [mental model](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/website/docs/getting-started/mental-model.mdx)
|
|
14
|
+
|
|
15
|
+
2. **[informational] The core branch operations use discoverable names.** `map`, `mapError`, `andThen`, `tryRecover`, `match`, `unwrapOr`, and `unwrap` cover normal composition. Async continuations and recovery have explicit `andThenAsync` and `tryRecoverAsync` forms. Recovery can widen the success lane; chaining unions the old and new error lanes. This is easier to discover than returns' `alt` and `lash`. Most combinators exist as methods plus static data-first and data-last functions. The three calling styles are convenient, but they account for many overloads. [`core.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/core.ts) · [`result.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/result.ts) · [transforming and chaining](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/website/docs/core/transforming-and-chaining.mdx)
|
|
16
|
+
|
|
17
|
+
3. **[informational] `Result.gen` is the signature feature.** `Ok[Symbol.iterator]` returns its value without yielding. `Err[Symbol.iterator]` yields itself once. `Result.gen` advances the supplied generator once, so each `yield*` unwraps an `Ok`, while the first `Err` becomes the generator's first yielded value and ends the workflow. Conditional types collect all possible yielded errors into the final union. Tagged errors are also iterable, which makes a direct `yield* new DomainError(...)` a typed guard clause. The implementation closes a short-circuited generator so `finally`, `Symbol.dispose`, and `Symbol.asyncDispose` cleanup runs. Body and cleanup exceptions become `Panic`. This is clever, readable at call sites, and well tested. It also depends on TypeScript-specific iterator typing that does not transfer cleanly to Python type checkers. [`core.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/core.ts) · [`result.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/result.ts) · [generator composition](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/website/docs/core/generator-composition.mdx)
|
|
18
|
+
|
|
19
|
+
4. **[informational] Async stays `Promise<Result<T, E>>`.** There is no `ResultAsync`, `FutureResult`, or effect-container family. `Result.await` only adds async iterator behavior for `Result.gen`; short pipelines use static combinators through `Promise.then`. This keeps the runtime and mental model much smaller than returns' `IOResult` and `FutureResult`. The cost is some syntactic ceremony: asynchronous generator users must write `yield* Result.await(operation())`, and rejected Promises outside explicit capture are defects. [`result.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/result.ts) · [async docs](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/website/docs/core/async-and-retries.mdx)
|
|
20
|
+
|
|
21
|
+
5. **[tradeoff] Callback exceptions usually become a separate thrown defect type.** `tryOrPanic` and `tryOrPanicAsync` wrap thrown or rejected callback failures in `Panic`, preserving the original value as `cause`. This applies to transforms, chaining, recovery, observation, exhaustive matching, generators, retry callbacks, and schema validation. Expected failures remain in `E`; bugs do not silently widen it to `unknown`. One exception is `matchErrorPartial`, whose selected handler and fallback run without the wrapper; tests confirm that a fallback exception propagates unchanged. The defect policy is broad but not universal. It is also more opinionated than returns, where exceptions inside ordinary `map` and `bind` callbacks propagate normally. Wrapping changes exception identity and adds another stack layer, so callers must understand that `except OriginalError` style handling would no longer work in a Python port. [`core.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/core.ts) · [`error.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/error.ts) · [panic docs](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/website/docs/errors/panic-and-defects.mdx)
|
|
22
|
+
|
|
23
|
+
6. **[informational] Exception capture is explicit at operation boundaries.** `Result.try` and `Result.tryPromise` either map a caught value with a caller-provided `catch` function or return `UnhandledException`. A throwing catch mapper is itself a defect and becomes `Panic`. `Result.try` deliberately rejects Promise-returning functions at the type level. `tryPromise` also has bounded retry, typed attempt context, static or dynamic delay, three backoff modes, jitter, retry predicates, and an optional `AbortSignal`. The signal stops pending delays and later attempts; the attempted operation only stops if user code forwards the signal. Bundling retry policy into a Result constructor is practical but well beyond a minimal algebra. [`result.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/result.ts) · [async and retries](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/website/docs/core/async-and-retries.mdx)
|
|
24
|
+
|
|
25
|
+
7. **[informational] Tagged errors make domain unions pleasant.** `TaggedError("Tag")<Props>` creates an `Error` subclass with a literal `_tag`, readonly typed properties, class-specific `.is`, JSON output, exhaustive instance `.match`, and generator support. `matchError` also works with structurally tagged error unions. `matchErrorPartial` can transform selected variants and leave the others intact. Adding a variant breaks an exhaustive handler at compile time, which gives errors-as-values much of the usability usually missing in TypeScript. Identity is nominal, however: concrete `.is` uses `instanceof`, so serialized values, other realms, and duplicate class copies need an explicit reconstruction boundary. [`error.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/error.ts) · [tagged errors](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/website/docs/errors/tagged-errors.mdx) · [matching errors](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/website/docs/errors/matching-errors.mdx)
|
|
26
|
+
|
|
27
|
+
8. **[medium] `Result.match` cannot currently infer divergent branch return types.** Its handlers share one generic return `T`, so TypeScript may infer `T` from the success branch and reject a differently shaped error branch instead of returning a union. Open issue [#111](https://github.com/dmmulroy/better-result/issues/111) has a minimal reproduction. Tagged-error matching uses a separate mapped return-union design and does not have the same limitation. This is a real ergonomic gap in the main exit operation at 3.0.1. Python overload design should avoid promising stronger inference than static checkers can deliver.
|
|
28
|
+
|
|
29
|
+
9. **[medium] The concrete tagged-error guard is unsafe as a detached callback.** `SomeError.is(value)` works because the static method evaluates `value instanceof this`. Passing `SomeError.is` directly to `filter` loses `this`; TypeScript accepts it in that context and runtime throws `TypeError`. Open issue [#112](https://github.com/dmmulroy/better-result/issues/112) documents the bug and the wrapper workaround. A guard factory should return a closed-over plain function, not depend on method binding. [`error.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/error.ts)
|
|
30
|
+
|
|
31
|
+
10. **[low] Pipeable helpers expose the limits of deferred generic inference.** Open issue [#110](https://github.com/dmmulroy/better-result/issues/110) shows that pipeable `matchErrorPartial` widens the fallback argument to the generic tagged-error base because the concrete union is unknown when handlers are declared. Open issue [#107](https://github.com/dmmulroy/better-result/issues/107) shows `tap` can over-narrow a later Promise pipeline without `NoInfer`, an explicit type argument, or a wrapper lambda. These are not runtime faults. They are evidence that supporting methods, data-first calls, and data-last calls with highly precise unions has a continuing maintenance cost.
|
|
32
|
+
|
|
33
|
+
11. **[tradeoff] Readonly means compile-time readonly, not immutable values.** `Ok.value`, `Err.error`, and tagged-error payload fields are declared readonly, but constructors do not freeze instances. A direct runtime check against the release build showed `Object.isFrozen(Result.ok(1)) === false`, assignment through an untyped reference changed its value, and tagged errors were likewise mutable. There is also no value equality; ordinary JavaScript reference equality applies. dry-python/returns is stronger here because its containers enforce immutability and value equality. A Python Result should use frozen dataclasses or equivalent runtime enforcement if those properties are part of its contract. [`core.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/core.ts) · [`error.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/error.ts)
|
|
34
|
+
|
|
35
|
+
12. **[informational] Collection helpers cover both short-circuit and full processing.** `Result.all` preserves tuple value types and returns the first input-order error. `allAsync` runs inputs concurrently, then applies the same rule. `partition` and `partitionAsync` preserve every success and error in separate ordered arrays. `flatten` joins nested error types. This is a useful small set. It does not accumulate independent validation errors into one `Err`; `partition` merely returns two arrays. Rejected raw Promises passed to async collection helpers become `Panic`. [`result.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/result.ts) · [collections](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/website/docs/core/collections.mdx)
|
|
36
|
+
|
|
37
|
+
13. **[tradeoff] Transport codecs are careful but large for a core Result package.** `Result.codec` accepts four Standard Schema validators: success and error schemas for serialization, then success and error schemas for deserialization. It validates the `{ status, value | error }` envelope, allows domain and wire types to differ, preserves synchronous or asynchronous schema behavior, and returns typed serialization errors. Unsafe forms turn schema mismatch into `Panic`. This fixes a real problem because JSON strips class prototypes and trusted types. It also contributes substantial conditional typing and ties transport policy to the Result package. A smaller library could expose the plain envelope and let schema integrations live in adapters. Tagged error `toJSON()` includes stack and serialized cause, so applications still need redaction policy when bypassing codecs. [`result.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/result.ts) · [`standard-schema.ts`](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/standard-schema.ts) · [codec docs](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/website/docs/serialization/result-codecs.mdx)
|
|
38
|
+
|
|
39
|
+
14. **[informational] The implementation is compact at runtime and serious about type tests.** The package has no runtime dependencies. The published 3.0.1 tarball contains seven files and reports 269,124 unpacked bytes, mostly declarations, maps, and README. The minified runtime bundle is 10,665 bytes and 3,268 bytes under gzip. The release source passed strict TypeScript checks, lint, formatting, 360 Vitest tests including 46 compile-time tests, build, and a TypeScript 5.4 consumer check. Tests include monad and functor laws, property-based collection and codec checks, generator cleanup, defect wrapping, and inference regressions. [package manifest](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/package.json) · [tests](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/result.test.ts) · [error type tests](https://github.com/dmmulroy/better-result/blob/75d0b10654370597d4cad103c5c825d5acd7d3e1/src/error.test-d.ts)
|
|
40
|
+
|
|
41
|
+
## Comparison with dry-python/returns
|
|
42
|
+
|
|
43
|
+
| Concern | better-result 3.0.1 | returns 0.26.0 |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| Runtime model | Two concrete classes and a union | ABC plus final Success and Failure variants |
|
|
46
|
+
| Composition | Methods, dual static functions, generator `yield*` | Methods, pointfree helpers, flow/pipe, generator-comprehension `do` |
|
|
47
|
+
| Error naming | `mapError`, `tryRecover` | `alt`, `lash` |
|
|
48
|
+
| Async | Ordinary `Promise<Result>` plus helpers | `FutureResult`, `IOResult`, and effect-aware companions |
|
|
49
|
+
| Typing support | Standard TypeScript conditional types and overloads | HKT emulation plus a mypy plugin for best inference |
|
|
50
|
+
| Domain errors | Built-in tagged `Error` factory and exhaustive matching | Error payload is generic; no comparable built-in tagged-error system |
|
|
51
|
+
| Callback exceptions | Wrapped and thrown as `Panic` | Ordinary callback exceptions propagate |
|
|
52
|
+
| Immutability and equality | Type-level readonly; reference equality | Runtime immutable; value equality |
|
|
53
|
+
| Validation accumulation | None; first error or partition | None in core; first error |
|
|
54
|
+
| Boundary extras | Retry and Standard Schema codecs included | Broader effect/container family instead |
|
|
55
|
+
|
|
56
|
+
better-result picks a better default vocabulary and a much smaller async model. returns has the stronger Python value semantics. Both become complicated when they try to recover precise typing for advanced composition syntax.
|
|
57
|
+
|
|
58
|
+
## Lessons for an ergonomic Python Result library
|
|
59
|
+
|
|
60
|
+
- Start with two frozen, value-equal variants and one obvious `Result[T, E]` union. Keep construction and narrowing plain: `Ok`, `Err`, `is_ok`, `is_err`, `value`, and `error`.
|
|
61
|
+
- Use `map_error` and `or_else` or `recover` rather than mathematical names. `and_then` is established enough to keep.
|
|
62
|
+
- Let async functions return `Awaitable[Result[T, E]]`. Do not add an effect-container family until a concrete use case requires it.
|
|
63
|
+
- Treat linear workflow syntax as optional sugar. better-result proves its value, but Python checkers cannot infer a union of every yielded error as cleanly. Typed method chaining and `match` must work without a plugin first.
|
|
64
|
+
- Keep expected domain failures separate from defects. Do not catch exceptions inside every callback by default. Either let them propagate unchanged or make defect wrapping an explicit, narrowly documented policy that preserves `__cause__`.
|
|
65
|
+
- Provide a typed exception-capture adapter at I/O boundaries. Catch `Exception`, not `BaseException`, and require explicit mapping when callers need a closed domain error type.
|
|
66
|
+
- Model tagged domain errors with frozen dataclasses and literal tags. Prefer guards that are plain functions. Do not rely on bound-method identity or nominal checks for deserialized values.
|
|
67
|
+
- Include `all`, `partition`, and `flatten`. State clearly that ordinary composition stops on the first error. Add a separate `Validation` type only if error accumulation is required.
|
|
68
|
+
- Keep transport validation and retry outside the initial core. Their policies and types can live in optional adapters without making every Result user learn them.
|
|
69
|
+
- Resist offering method, data-first, data-last, pointfree, generator, and pattern APIs all at once. better-result's open typing issues are a useful warning. One excellent method API plus Python pattern matching is a better first release.
|
|
70
|
+
- Make throwing extraction loud. `unwrap` should identify failed invariants, preserve the original error as cause, and stay out of routine control flow.
|
|
71
|
+
|
|
72
|
+
## Sources and validation
|
|
73
|
+
|
|
74
|
+
- [better-result 3.0.1 release](https://github.com/dmmulroy/better-result/releases/tag/v3.0.1), published 2026-08-11.
|
|
75
|
+
- Pinned release source at commit [`75d0b10654370597d4cad103c5c825d5acd7d3e1`](https://github.com/dmmulroy/better-result/tree/75d0b10654370597d4cad103c5c825d5acd7d3e1).
|
|
76
|
+
- Official source, tests, package manifest, release workflow, README, and documentation linked in the findings.
|
|
77
|
+
- Current open issues were queried individually with GitHub CLI rather than inferred from source: [#107](https://github.com/dmmulroy/better-result/issues/107), [#110](https://github.com/dmmulroy/better-result/issues/110), [#111](https://github.com/dmmulroy/better-result/issues/111), and [#112](https://github.com/dmmulroy/better-result/issues/112).
|
|
78
|
+
- Local validation in `/tmp/better-result-research-source`: `bun run check`, `bun run lint`, `bun run fmt:check`, `bun run test`, `bun run build`, then `bun run check:typescript-minimum`. All passed. The test run reported 360 passing tests and no type errors.
|
|
79
|
+
- Published npm metadata and tarball were checked directly for package contents, dependency count, provenance metadata, and bundle sizes.
|
|
80
|
+
|
|
81
|
+
## Gaps and residual risks
|
|
82
|
+
|
|
83
|
+
- This report targets the latest stable npm release, not unreleased `main`. The repository's main branch was ahead of 3.0.1 when checked.
|
|
84
|
+
- The four cited issues were open when checked and were created after 3.0.1. A later release may fix them.
|
|
85
|
+
- Download counts and popularity were intentionally excluded from design conclusions. They do not verify API quality.
|
|
86
|
+
- No broad downstream-code survey was performed. Compatibility findings come from the release's consumer test and current issue reports.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# `dry-python/returns`: Result and functional composition
|
|
2
|
+
|
|
3
|
+
Curated against `returns` **0.29.0**, commit
|
|
4
|
+
[`cf5e2548`](https://github.com/dry-python/returns/tree/cf5e25485921bd02dea637e6f12c163128ba295c).
|
|
5
|
+
The retained subagent report used 0.26.0; the core claims below were reconciled
|
|
6
|
+
with the separately inspected 0.29.0 checkout. Re-check current upstream before
|
|
7
|
+
copying signatures or implementation details.
|
|
8
|
+
|
|
9
|
+
## Executive summary
|
|
10
|
+
|
|
11
|
+
`returns.Result[T, E]` is an immutable, covariant, two-track container:
|
|
12
|
+
|
|
13
|
+
- `Success[T]` transforms and binds on the value track;
|
|
14
|
+
- `Failure[E]` preserves the failure or transforms/recovers on the error track;
|
|
15
|
+
- normal composition does not catch callback exceptions;
|
|
16
|
+
- extraction methods can raise `UnwrapFailedError`;
|
|
17
|
+
- exception capture is explicit through decorators such as `safe`;
|
|
18
|
+
- pattern matching and generator-based do-notation are supported;
|
|
19
|
+
- async/effectful work uses companion containers such as `FutureResult` and
|
|
20
|
+
`IOResult`;
|
|
21
|
+
- advanced HKT, decorator, and do-notation typing depends on a mypy plugin;
|
|
22
|
+
- core Result composition stops at the first failure rather than accumulating
|
|
23
|
+
validation errors.
|
|
24
|
+
|
|
25
|
+
The semantic rigor is valuable. The interface hierarchy, point-free mirror,
|
|
26
|
+
effect-container family, and checker plugin are too much initial surface for
|
|
27
|
+
snekok.
|
|
28
|
+
|
|
29
|
+
## Construction and representation
|
|
30
|
+
|
|
31
|
+
Callers construct explicit variants:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
Success(1)
|
|
35
|
+
Failure("invalid")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`Result` itself is an abstract container rather than a public two-argument
|
|
39
|
+
constructor. `Result.from_value` and `Result.from_failure` support generic
|
|
40
|
+
container-oriented construction. `ResultE[T]` aliases
|
|
41
|
+
`Result[T, Exception]`.
|
|
42
|
+
|
|
43
|
+
Both value and error parameters are covariant. Containers are immutable,
|
|
44
|
+
slotted, comparable by variant and payload, and hashable when their payload is
|
|
45
|
+
hashable.
|
|
46
|
+
|
|
47
|
+
Snekok keeps the explicit variant idea but uses Python-familiar `Ok` and `Err`
|
|
48
|
+
and represents `Result` directly as their union.
|
|
49
|
+
|
|
50
|
+
Sources: [`returns/result.py`](https://github.com/dry-python/returns/blob/0.29.0/returns/result.py),
|
|
51
|
+
[`container.py`](https://github.com/dry-python/returns/blob/0.29.0/returns/primitives/container.py),
|
|
52
|
+
[`types.py`](https://github.com/dry-python/returns/blob/0.29.0/returns/primitives/types.py).
|
|
53
|
+
|
|
54
|
+
## Value-track operations
|
|
55
|
+
|
|
56
|
+
| Operation | Success behavior | Failure behavior |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| `map(f)` | `Success(f(value))` | passes the failure through |
|
|
59
|
+
| `bind(f)` | runs `f(value)` and flattens its Result | passes the failure through |
|
|
60
|
+
| `apply(container)` | applies a wrapped function | preserves failure semantics |
|
|
61
|
+
| `value_or(default)` | returns the value | returns the eager default |
|
|
62
|
+
| `unwrap()` | returns the value | raises `UnwrapFailedError` |
|
|
63
|
+
|
|
64
|
+
Ordinary `bind` keeps one error type. Point-free `unify` supports a function
|
|
65
|
+
whose Result has another error type and widens the resulting error channel.
|
|
66
|
+
|
|
67
|
+
Snekok naming candidates remain `map`, `and_then`, and `unwrap_or`. Partial
|
|
68
|
+
extraction should be conspicuous and unnecessary in normal control flow.
|
|
69
|
+
|
|
70
|
+
Sources: [`returns/result.py`](https://github.com/dry-python/returns/blob/0.29.0/returns/result.py),
|
|
71
|
+
[`unify.py`](https://github.com/dry-python/returns/blob/0.29.0/returns/pointfree/unify.py).
|
|
72
|
+
|
|
73
|
+
## Error-track operations
|
|
74
|
+
|
|
75
|
+
| Operation | Failure behavior | Success behavior |
|
|
76
|
+
| --- | --- | --- |
|
|
77
|
+
| `alt(f)` | maps `E -> F` | passes the success through |
|
|
78
|
+
| `lash(f)` | runs `E -> Result[T, F]` and flattens | passes the success through |
|
|
79
|
+
| `failure()` | extracts the error | raises `UnwrapFailedError` |
|
|
80
|
+
| `swap()` | moves the error to success | moves the value to failure |
|
|
81
|
+
|
|
82
|
+
`alt` and `lash` are mathematically consistent with the wider library but less
|
|
83
|
+
discoverable to many Python users. For snekok, `map_err` and `or_else` or
|
|
84
|
+
`recover_with` are clearer candidates.
|
|
85
|
+
|
|
86
|
+
Source: [`returns/result.py`](https://github.com/dry-python/returns/blob/0.29.0/returns/result.py).
|
|
87
|
+
|
|
88
|
+
## Exception capture
|
|
89
|
+
|
|
90
|
+
`safe` wraps a synchronous function and catches `Exception` or configured
|
|
91
|
+
exception classes into `Failure`. `attempt` records the function's single input
|
|
92
|
+
as the failure payload instead of retaining the exception. Async equivalents,
|
|
93
|
+
including `future_safe` and `future_attempt`, produce `FutureResult`.
|
|
94
|
+
|
|
95
|
+
Important boundary rule: `map` and `bind` callbacks are not implicitly safe. If
|
|
96
|
+
a callback raises, the exception still propagates.
|
|
97
|
+
|
|
98
|
+
This is a strong precedent for snekok:
|
|
99
|
+
|
|
100
|
+
- exception conversion should have an explicit name;
|
|
101
|
+
- adapters should catch selected `Exception` subclasses, never control-flow
|
|
102
|
+
`BaseException` events;
|
|
103
|
+
- normal transformations should not silently alter exception behavior.
|
|
104
|
+
|
|
105
|
+
Sources: [`returns/result.py`](https://github.com/dry-python/returns/blob/0.29.0/returns/result.py),
|
|
106
|
+
[`returns/future.py`](https://github.com/dry-python/returns/blob/0.29.0/returns/future.py).
|
|
107
|
+
|
|
108
|
+
## Pattern matching
|
|
109
|
+
|
|
110
|
+
Containers expose positional matching:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
match outcome:
|
|
114
|
+
case Success(value):
|
|
115
|
+
use(value)
|
|
116
|
+
case Failure(error):
|
|
117
|
+
handle(error)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This is the clearest dependency-light consumption style and directly informed
|
|
121
|
+
snekok's initial API. Python itself does not universally enforce match
|
|
122
|
+
exhaustiveness, so checker configuration remains part of the contract.
|
|
123
|
+
|
|
124
|
+
Source: [`container.py`](https://github.com/dry-python/returns/blob/0.29.0/returns/primitives/container.py).
|
|
125
|
+
|
|
126
|
+
## Do-notation
|
|
127
|
+
|
|
128
|
+
`Result.do` consumes a generator expression:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
Result.do(first + second for first in Success(1) for second in Success(2))
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Success iteration yields a value. Failure iteration raises an internal unwrap
|
|
135
|
+
exception that `do` catches and converts back to that failure. The first failure
|
|
136
|
+
short-circuits, and the syntax supports one final yielded expression rather than
|
|
137
|
+
general statement-level do-notation.
|
|
138
|
+
|
|
139
|
+
The implementation is clever, but typing and maintenance costs outweigh the
|
|
140
|
+
benefit for a small initial library. Method chaining and `match` are simpler.
|
|
141
|
+
|
|
142
|
+
Sources: [`returns/result.py`](https://github.com/dry-python/returns/blob/0.29.0/returns/result.py),
|
|
143
|
+
[do-notation documentation](https://returns.readthedocs.io/en/latest/pages/do-notation.html).
|
|
144
|
+
|
|
145
|
+
## Point-free pipelines and interfaces
|
|
146
|
+
|
|
147
|
+
`flow(value, *functions)` executes left-to-right. `pipe(*functions)` builds a
|
|
148
|
+
reusable pipeline. `returns.pointfree` mirrors container methods with curried
|
|
149
|
+
helpers such as `map_`, `bind`, `alt`, `lash`, and `value_or`.
|
|
150
|
+
|
|
151
|
+
Underneath, capability interfaces split mapping, binding, error mapping,
|
|
152
|
+
recovery, and extraction into reusable abstractions. HKT emulation lets those
|
|
153
|
+
helpers target `Result`, `IOResult`, `FutureResult`, reader containers, and
|
|
154
|
+
other related types.
|
|
155
|
+
|
|
156
|
+
This is useful when cross-container polymorphism is a goal. It is excessive for
|
|
157
|
+
snekok until at least two real container implementations require the same
|
|
158
|
+
abstraction.
|
|
159
|
+
|
|
160
|
+
Sources: [`pipeline.py`](https://github.com/dry-python/returns/blob/0.29.0/returns/pipeline.py),
|
|
161
|
+
[`interfaces`](https://github.com/dry-python/returns/tree/0.29.0/returns/interfaces),
|
|
162
|
+
[`pointfree`](https://github.com/dry-python/returns/tree/0.29.0/returns/pointfree).
|
|
163
|
+
|
|
164
|
+
## Async and effect containers
|
|
165
|
+
|
|
166
|
+
`FutureResult[T, E]` represents asynchronous computation and resolves through
|
|
167
|
+
`IOResult[T, E]`, preserving that executing the computation is effectful.
|
|
168
|
+
Async-aware mapping and binding bridge ordinary Results, awaitables, Futures,
|
|
169
|
+
and FutureResults.
|
|
170
|
+
|
|
171
|
+
This models effects precisely but creates a substantial API and learning
|
|
172
|
+
surface. Snekok should initially let ordinary async functions return
|
|
173
|
+
`Result[T, E]`; a separate async abstraction should require concrete composition
|
|
174
|
+
pressure.
|
|
175
|
+
|
|
176
|
+
Source: [`returns/future.py`](https://github.com/dry-python/returns/blob/0.29.0/returns/future.py).
|
|
177
|
+
|
|
178
|
+
## Type-checker plugin
|
|
179
|
+
|
|
180
|
+
The mypy plugin supplies inference hooks for HKT emulation, decorators,
|
|
181
|
+
do-notation, partial/curry helpers, and related constructs that standard mypy
|
|
182
|
+
cannot represent precisely. Other checkers do not run mypy plugins.
|
|
183
|
+
|
|
184
|
+
Snekok should keep signatures understandable to ordinary Pyright and standard
|
|
185
|
+
PEP typing. Fewer abstractions are preferable to checker-specific magic.
|
|
186
|
+
|
|
187
|
+
Sources: [plugin documentation](https://returns.readthedocs.io/en/latest/pages/contrib/mypy_plugins.html),
|
|
188
|
+
[`returns_plugin.py`](https://github.com/dry-python/returns/blob/0.29.0/returns/contrib/mypy/returns_plugin.py).
|
|
189
|
+
|
|
190
|
+
## Validation semantics
|
|
191
|
+
|
|
192
|
+
`bind`, do-notation, applicative application, and collection helpers preserve
|
|
193
|
+
first-failure semantics. The core API has no accumulating `Validated` type,
|
|
194
|
+
non-empty error collection, or semigroup-constrained combination.
|
|
195
|
+
|
|
196
|
+
Snekok should not silently make ordinary Result composition accumulate errors.
|
|
197
|
+
If independent validation needs every error, add a distinct abstraction or an
|
|
198
|
+
explicit collection operation.
|
|
199
|
+
|
|
200
|
+
Sources: [`returns/result.py`](https://github.com/dry-python/returns/blob/0.29.0/returns/result.py),
|
|
201
|
+
[`iterables.py`](https://github.com/dry-python/returns/blob/0.29.0/returns/iterables.py).
|
|
202
|
+
|
|
203
|
+
## Ideas to retain or avoid
|
|
204
|
+
|
|
205
|
+
Retain:
|
|
206
|
+
|
|
207
|
+
- immutable value semantics and covariance;
|
|
208
|
+
- explicit variants and structural matching;
|
|
209
|
+
- typed mapping, binding, error mapping, and recovery when consumers need them;
|
|
210
|
+
- explicit exception-boundary adapters;
|
|
211
|
+
- clear first-failure semantics.
|
|
212
|
+
|
|
213
|
+
Rename or simplify:
|
|
214
|
+
|
|
215
|
+
- `alt` -> `map_err`;
|
|
216
|
+
- `lash` -> `or_else` or `recover_with`;
|
|
217
|
+
- prefer methods and IDE discovery over a full point-free mirror.
|
|
218
|
+
|
|
219
|
+
Avoid initially:
|
|
220
|
+
|
|
221
|
+
- HKT emulation and a capability-interface lattice;
|
|
222
|
+
- a custom checker plugin;
|
|
223
|
+
- do-notation;
|
|
224
|
+
- `IOResult`, reader, and async container families;
|
|
225
|
+
- implicit exception capture in transformations;
|
|
226
|
+
- mixing accumulating validation into Result's fail-fast semantics.
|
|
227
|
+
|
|
228
|
+
## Research limits
|
|
229
|
+
|
|
230
|
+
- This is a pinned source snapshot; latest upstream may differ.
|
|
231
|
+
- Advanced inference varies with mypy and plugin versions.
|
|
232
|
+
- Recommendations are interpretation, not verified upstream behavior and not a
|
|
233
|
+
commitment to snekok's future API.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Result
|
|
2
|
+
|
|
3
|
+
`Result[T, E]` represents either an expected success or an expected failure:
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from snekok.result import Err, Ok, Result
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def read_count(raw: str) -> Result[int, str]:
|
|
10
|
+
if not raw.isdecimal():
|
|
11
|
+
return Err("count must be an integer")
|
|
12
|
+
return Ok(int(raw))
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Variants
|
|
16
|
+
|
|
17
|
+
- `Result[T, E]` is the nominal base type shared by both variants.
|
|
18
|
+
- `Ok(value)` contains a successful `T` in `.value`.
|
|
19
|
+
- `Err(error)` contains a failed `E` in `.error`.
|
|
20
|
+
|
|
21
|
+
Both variants are immutable, slotted value types. Each carries a phantom type
|
|
22
|
+
for the absent channel, allowing transformations to preserve and widen both
|
|
23
|
+
covariant channels without reconstructing an enclosing result. The nominal base
|
|
24
|
+
keeps annotations and type-checker output compact.
|
|
25
|
+
|
|
26
|
+
## Consumption
|
|
27
|
+
|
|
28
|
+
Narrow a concrete variant when its field is needed, then unwrap the base type:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
outcome = read_count(raw)
|
|
32
|
+
if isinstance(outcome, Err):
|
|
33
|
+
report(outcome.error)
|
|
34
|
+
else:
|
|
35
|
+
consume(outcome.unwrap())
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`unwrap()` returns `T` and raises `RuntimeError` for an `Err`. `unwrap_error()`
|
|
39
|
+
returns `E` and raises `RuntimeError` for an `Ok`. Check the known variant before
|
|
40
|
+
calling either method when both outcomes are expected.
|
|
41
|
+
|
|
42
|
+
`Ok` and `Err` remain class-pattern compatible, but static type checkers cannot
|
|
43
|
+
consider a nominal class hierarchy sealed. A `match` over `Result[T, E]` therefore
|
|
44
|
+
needs a fallback case even when it handles both built-in variants.
|
|
45
|
+
|
|
46
|
+
## Composition
|
|
47
|
+
|
|
48
|
+
Use `map` to transform a success while preserving any error:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
label = read_count(raw).map(lambda count: f"count={count}")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Use `map_error` to translate an expected error while preserving a success:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
identified = read_count(raw).map_error(lambda message: ("invalid_count", message))
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use `and_then` to continue with another fallible operation. Existing and newly
|
|
61
|
+
introduced error channels are combined:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
def require_positive(count: int) -> Result[int, ValueError]:
|
|
65
|
+
if count <= 0:
|
|
66
|
+
return Err(ValueError("count must be positive"))
|
|
67
|
+
return Ok(count)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
positive = read_count(raw).and_then(require_positive)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`and_then_async` provides the same error propagation for an asynchronous
|
|
74
|
+
continuation:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
async def require_positive_async(count: int) -> Result[int, ValueError]:
|
|
78
|
+
return require_positive(count)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
validated = await read_count(raw).and_then_async(require_positive_async)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Transform callbacks are ordinary Python calls. Exceptions raised by them are not
|
|
85
|
+
caught or converted into `Err` values.
|
|
86
|
+
|
|
87
|
+
## Exception boundary
|
|
88
|
+
|
|
89
|
+
`Result` describes expected outcomes. It does not catch exceptions implicitly.
|
|
90
|
+
Programmer errors, cancellation, and broken invariants remain exceptions.
|
|
91
|
+
Exception-capture helpers will be introduced only when real boundary code
|
|
92
|
+
establishes their required semantics.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "snekok"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Small, typed tools for treating expected failures as values"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"annotated-types>=0.7.0",
|
|
9
|
+
"pydantic>=2.13.4",
|
|
10
|
+
"typing-extensions>=4.15.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[dependency-groups]
|
|
14
|
+
dev = [
|
|
15
|
+
"ruff>=0.15.18",
|
|
16
|
+
"snektest",
|
|
17
|
+
"ty==0.0.75",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.uv.sources]
|
|
21
|
+
snektest = { url = "https://github.com/crpier/snektest/archive/b6ff08544eade05804a2fdfd894c2c854145e537.tar.gz" }
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["hatchling"]
|
|
25
|
+
build-backend = "hatchling.build"
|
|
26
|
+
|
|
27
|
+
[tool.ty.rules]
|
|
28
|
+
all = "error"
|
|
29
|
+
missing-override-decorator = "ignore"
|
|
30
|
+
|
|
31
|
+
[tool.ruff.lint]
|
|
32
|
+
select = [
|
|
33
|
+
"A", "ANN", "ARG", "ASYNC", "B", "BLE", "C4", "C90", "COM", "DTZ",
|
|
34
|
+
"E", "EM", "ERA", "EXE", "F", "FBT", "FURB", "G", "I", "INP", "ISC",
|
|
35
|
+
"LOG", "N", "PERF", "PGH", "PIE", "PL", "PLE", "PLR", "PLW", "PTH",
|
|
36
|
+
"PYI", "Q", "RET", "RSE", "RUF", "S", "SIM", "SLF", "SLOT", "T10",
|
|
37
|
+
"T20", "TC", "TID", "TRY", "UP", "W",
|
|
38
|
+
]
|
|
39
|
+
ignore = [
|
|
40
|
+
"COM812", # Conflicts with the formatter.
|
|
41
|
+
"E501", # The formatter handles ordinary line length.
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint.per-file-ignores]
|
|
45
|
+
"tests/**" = [
|
|
46
|
+
"PLW3201", # Exercise frozen dataclass assignment through its public runtime behavior.
|
|
47
|
+
]
|
|
File without changes
|
|
File without changes
|