effecton 0.2.1__tar.gz → 0.3.1__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.
- effecton-0.3.1/.gitignore +26 -0
- effecton-0.3.1/CHANGELOG.md +54 -0
- effecton-0.3.1/PKG-INFO +43 -0
- effecton-0.3.1/README.md +25 -0
- {effecton-0.2.1 → effecton-0.3.1}/pyproject.toml +6 -1
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/__init__.py +26 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/effect.py +56 -5
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/exit.py +9 -3
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/pytest_plugin.py +23 -2
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/run_async.py +7 -2
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/run_sync.py +14 -3
- effecton-0.3.1/src/effecton/std/cli.py +658 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/std/clock.py +9 -4
- effecton-0.3.1/src/effecton/std/duration.py +32 -0
- effecton-0.3.1/src/effecton/std/file_system.py +816 -0
- effecton-0.3.1/src/effecton/std/http_client.py +372 -0
- effecton-0.3.1/src/effecton/std/path.py +68 -0
- effecton-0.3.1/src/effecton/std/process.py +69 -0
- effecton-0.3.1/src/effecton/std/random.py +119 -0
- effecton-0.3.1/src/effecton/std/retry.py +54 -0
- effecton-0.3.1/src/effecton/std/schedule.py +79 -0
- effecton-0.3.1/src/effecton/std/schema.py +816 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/std/timeout.py +6 -4
- effecton-0.3.1/src/effecton/std/tracer.py +289 -0
- effecton-0.2.1/.gitignore +0 -13
- effecton-0.2.1/CHANGELOG.md +0 -29
- effecton-0.2.1/PKG-INFO +0 -513
- effecton-0.2.1/README.md +0 -497
- {effecton-0.2.1 → effecton-0.3.1}/LICENSE +0 -0
- {effecton-0.2.1 → effecton-0.3.1}/conftest.py +0 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/attempt.py +0 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/catch.py +0 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/gen.py +0 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/implicit_requirement.py +0 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/provide.py +0 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/py.typed +0 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/run_main.py +0 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/std/__init__.py +0 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/std/fiber.py +0 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/std/logger.py +0 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/std/pretty_logger.py +0 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/std/race.py +0 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/std/scope.py +0 -0
- {effecton-0.2.1 → effecton-0.3.1}/src/effecton/suspend.py +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# Copied from the repo root by the release workflow
|
|
13
|
+
packages/effecton/LICENSE
|
|
14
|
+
|
|
15
|
+
# Docs site (vocs)
|
|
16
|
+
docs/node_modules
|
|
17
|
+
docs/dist
|
|
18
|
+
docs/.vocs
|
|
19
|
+
docs/src/pages.gen.ts
|
|
20
|
+
# Generated by `uv run api-reference` (docs predev/prebuild)
|
|
21
|
+
docs/src/pages/api.md
|
|
22
|
+
|
|
23
|
+
# Agent working artifacts: implementation plans and SDD ledgers stay local.
|
|
24
|
+
# Design specs are committed under docs/specs instead (see AGENTS.md).
|
|
25
|
+
docs/superpowers/
|
|
26
|
+
.superpowers/
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# effecton
|
|
2
|
+
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Add E.Schema: two-way schemas with Struct classes, combinators, guarded transforms (`to=`, `S.instance_of`) and `Check` refinements applied with `schema.check(...)`, whose decode and encode fail with a ParseError listing every issue ([#32](https://github.com/krzkaczor/effecton/pull/32))
|
|
8
|
+
- Add E.Cli: annotated Args classes decoded through E.Schema, nested commands, --help, --version and a UsageError carrying the reason; add E.Process.argv() ([#34](https://github.com/krzkaczor/effecton/pull/34))
|
|
9
|
+
|
|
10
|
+
## 0.3.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- Launch the docs site: a landing page, an introduction, and a generated API Reference, with every Python snippet type-checked by ty at build time. ([#26](https://github.com/krzkaczor/effecton/pull/26))
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Add the E.HttpClient service: SyncLive and AsyncLive on httpx2, Test with canned responses or a handler, Request/Response values and filter_status_ok; HTTP client libraries are banned outside the service ([#24](https://github.com/krzkaczor/effecton/pull/24))
|
|
19
|
+
- sleep, timeout and Schedule.spaced/exponential accept a duration as timedelta's keyword parts (seconds=5, minutes=1, ...) in place of a timedelta. ([#26](https://github.com/krzkaczor/effecton/pull/26))
|
|
20
|
+
- Add the `E.Tracer` service: `E.with_span(effect, name, kind=..., **attributes)` opens a span around an effect (also as `effect.with_span`), `E.annotate_current_span` adds attributes, `E.current_span` reads the open span, `E.Tracer.Live` is the in-memory default, `E.Tracer.Test` records its spans, and the `test_tracer` pytest fixture provides one ([#25](https://github.com/krzkaczor/effecton/pull/25))
|
|
21
|
+
- Add `effect.retry(schedule, until=...)` with `E.Schedule.recurs`, `E.Schedule.spaced`, and `E.Schedule.exponential` ([#19](https://github.com/krzkaczor/effecton/pull/19))
|
|
22
|
+
- Effect.retry takes an optional schedule and a `times` keyword, as in Effect-TS: `retry(times=3)` retries up to three times without waiting, `retry(schedule, times=5)` caps the schedule at five more recurrences, and `retry()` with neither recurs forever. ([#26](https://github.com/krzkaczor/effecton/pull/26))
|
|
23
|
+
- Add `schedule.jittered(min=0.8, max=1.2)`, which scales every delay by a factor drawn through `E.random()`; schedule steps are now effects, so build a plain custom schedule with `E.Schedule.from_delays(...)` and an effectful one with `E.Schedule(steps=...)` ([#22](https://github.com/krzkaczor/effecton/pull/22))
|
|
24
|
+
- Add the `E.FileSystem` service (`SyncLive`, `AsyncLive` on aiofiles, in-memory `Test`) the immutable `E.Path`, and the `E.Process` service (`cwd`, `home`); stdlib path and file APIs are banned outside the service ([#23](https://github.com/krzkaczor/effecton/pull/23))
|
|
25
|
+
- `on_exit` also accepts a function receiving the effect's `Exit` and returning the finalizer, so cleanup can depend on how the effect settled ([#25](https://github.com/krzkaczor/effecton/pull/25))
|
|
26
|
+
- Add the `E.Random` implicit service: `E.random()` resolves it, its methods keep the `random` module names (`random`, `uniform`, `randint`, `choice`, `shuffle`), `E.Random.Live` is the default, `E.Random.Test(seed)` is deterministic, and the `test_random` pytest fixture provides one seeded with 0 ([#22](https://github.com/krzkaczor/effecton/pull/22))
|
|
27
|
+
|
|
28
|
+
## 0.2.1
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- Ship a pytest plugin, registered through the pytest11 entry point so it loads wherever effecton is installed. A test that returns an Effect (typically an @E.gen function) runs under the async runner and a failure is reported as its cause. The test_clock fixture provides an E.Clock.Test to the test's effect. ([#16](https://github.com/krzkaczor/effecton/pull/16))
|
|
33
|
+
- Add E.now() and E.sleep() backed by the implicit E.Clock service, with SyncLive, AsyncLive and Test clocks; each runner installs the matching live clock. ([#16](https://github.com/krzkaczor/effecton/pull/16))
|
|
34
|
+
- Add `E.run_main` to execute sync and async programs with failure logging, defect tracebacks, custom exit codes, and graceful signal handling. ([#14](https://github.com/krzkaczor/effecton/pull/14))
|
|
35
|
+
- Add E.fork, E.Fiber (join, wait, poll, interrupt) and E.yield_now backed by asyncio tasks, and make the Test clock's adjust and set_time return effects. ([#16](https://github.com/krzkaczor/effecton/pull/16))
|
|
36
|
+
- Add E.race_first(left, right) and E.timeout(duration) ([#18](https://github.com/krzkaczor/effecton/pull/18))
|
|
37
|
+
|
|
38
|
+
## 0.2.0
|
|
39
|
+
|
|
40
|
+
### Minor Changes
|
|
41
|
+
|
|
42
|
+
- Add `run_async`, `coroutine` and `attempt_async` for awaitable-backed effects ([#12](https://github.com/krzkaczor/effecton/pull/12))
|
|
43
|
+
- Split the runners: `run_sync` and `run_async` now return the value and raise on failure (the error, the defect, or `UnhandledDefect` for a non-exception defect); `run_sync_exit`, `run_async_exit` and the coroutine `run_async_coroutine` return the `Exit` ([#13](https://github.com/krzkaczor/effecton/pull/13))
|
|
44
|
+
|
|
45
|
+
### Patch Changes
|
|
46
|
+
|
|
47
|
+
- Add `Effect.catch`: handle one error type and subtract it from the error channel. ([#10](https://github.com/krzkaczor/effecton/pull/10))
|
|
48
|
+
- Add `Interrupt` as a third `Cause` state, produced when a cancellation unwinds `run_async` ([#12](https://github.com/krzkaczor/effecton/pull/12))
|
|
49
|
+
|
|
50
|
+
## 0.1.1
|
|
51
|
+
|
|
52
|
+
### Patch Changes
|
|
53
|
+
|
|
54
|
+
- Migrate from mypy to ty. Simplify api where possible. ([#1](https://github.com/krzkaczor/effecton/pull/1))
|
effecton-0.3.1/PKG-INFO
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: effecton
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: A typed effect system for Python, inspired by Effect-TS
|
|
5
|
+
Project-URL: Repository, https://github.com/krzkaczor/effecton
|
|
6
|
+
Author-email: Krzysztof Kaczor <chris@kaczor.io>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: dependency-injection,effect-system,effect-ts,functional,typed
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.14
|
|
14
|
+
Requires-Dist: aiofiles>=25.1
|
|
15
|
+
Requires-Dist: httpx2>=2.12
|
|
16
|
+
Requires-Dist: typing-extensions>=4.16.0
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="https://effecton.dev">
|
|
21
|
+
<picture>
|
|
22
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/krzkaczor/effecton/main/docs/public/logo-wordmark-white.svg">
|
|
23
|
+
<img alt="effecton" src="https://raw.githubusercontent.com/krzkaczor/effecton/main/docs/public/logo-wordmark.svg" width="320">
|
|
24
|
+
</picture>
|
|
25
|
+
</a>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
<p align="center">A typed effect system for Python, inspired by <a href="https://effect.website/">Effect-TS</a>.</p>
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<a href="https://pypi.org/project/effecton/"><img alt="PyPI" src="https://img.shields.io/pypi/v/effecton?logo=pypi&logoColor=white"></a>
|
|
32
|
+
<a href="https://discord.gg/fNhY7AxMyh"><img alt="Discord" src="https://img.shields.io/badge/Discord-join%20chat-5865F2?logo=discord&logoColor=white"></a>
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<a href="https://effecton.dev/introduction">Documentation</a> ·
|
|
37
|
+
<a href="https://effecton.dev/api">API Reference</a> ·
|
|
38
|
+
<a href="https://github.com/krzkaczor/effecton/blob/main/CONTRIBUTING.md">Contributing</a>
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
uv add effecton
|
|
43
|
+
```
|
effecton-0.3.1/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://effecton.dev">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/krzkaczor/effecton/main/docs/public/logo-wordmark-white.svg">
|
|
5
|
+
<img alt="effecton" src="https://raw.githubusercontent.com/krzkaczor/effecton/main/docs/public/logo-wordmark.svg" width="320">
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">A typed effect system for Python, inspired by <a href="https://effect.website/">Effect-TS</a>.</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://pypi.org/project/effecton/"><img alt="PyPI" src="https://img.shields.io/pypi/v/effecton?logo=pypi&logoColor=white"></a>
|
|
14
|
+
<a href="https://discord.gg/fNhY7AxMyh"><img alt="Discord" src="https://img.shields.io/badge/Discord-join%20chat-5865F2?logo=discord&logoColor=white"></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="https://effecton.dev/introduction">Documentation</a> ·
|
|
19
|
+
<a href="https://effecton.dev/api">API Reference</a> ·
|
|
20
|
+
<a href="https://github.com/krzkaczor/effecton/blob/main/CONTRIBUTING.md">Contributing</a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
uv add effecton
|
|
25
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "effecton"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "A typed effect system for Python, inspired by Effect-TS"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -14,6 +14,8 @@ classifiers = [
|
|
|
14
14
|
]
|
|
15
15
|
requires-python = ">=3.14"
|
|
16
16
|
dependencies = [
|
|
17
|
+
"aiofiles>=25.1",
|
|
18
|
+
"httpx2>=2.12",
|
|
17
19
|
"typing-extensions>=4.16.0",
|
|
18
20
|
]
|
|
19
21
|
|
|
@@ -23,6 +25,9 @@ Repository = "https://github.com/krzkaczor/effecton"
|
|
|
23
25
|
[project.entry-points.pytest11]
|
|
24
26
|
effecton = "effecton.pytest_plugin"
|
|
25
27
|
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = ["types-aiofiles>=25.1"]
|
|
30
|
+
|
|
26
31
|
[build-system]
|
|
27
32
|
requires = ["hatchling"]
|
|
28
33
|
build-backend = "hatchling.build"
|
|
@@ -28,7 +28,14 @@ from effecton.run_sync import (
|
|
|
28
28
|
run_sync,
|
|
29
29
|
run_sync_exit,
|
|
30
30
|
)
|
|
31
|
+
from effecton.std import cli as Cli
|
|
31
32
|
from effecton.std import clock as Clock
|
|
33
|
+
from effecton.std import file_system as FileSystem
|
|
34
|
+
from effecton.std import http_client as HttpClient
|
|
35
|
+
from effecton.std import process as Process
|
|
36
|
+
from effecton.std import random as Random
|
|
37
|
+
from effecton.std import schema as Schema
|
|
38
|
+
from effecton.std import tracer as Tracer
|
|
32
39
|
from effecton.std.clock import _now as now
|
|
33
40
|
from effecton.std.clock import _sleep as sleep
|
|
34
41
|
from effecton.std.fiber import Fiber, fork, yield_now
|
|
@@ -50,15 +57,22 @@ from effecton.std.logger import (
|
|
|
50
57
|
log_trace,
|
|
51
58
|
log_warning,
|
|
52
59
|
)
|
|
60
|
+
from effecton.std.path import Path
|
|
53
61
|
from effecton.std.pretty_logger import PrettyFormatter, pretty_logger
|
|
54
62
|
from effecton.std.race import race_first
|
|
63
|
+
from effecton.std.random import _random as random
|
|
64
|
+
from effecton.std.schedule import Schedule
|
|
55
65
|
from effecton.std.scope import Scope, acquire_and_release, add_finalizer, scoped
|
|
56
66
|
from effecton.std.timeout import TimeoutException, timeout
|
|
67
|
+
from effecton.std.tracer import _annotate_current_span as annotate_current_span
|
|
68
|
+
from effecton.std.tracer import _current_span as current_span
|
|
69
|
+
from effecton.std.tracer import _with_span as with_span
|
|
57
70
|
from effecton.suspend import suspend
|
|
58
71
|
|
|
59
72
|
__all__ = [
|
|
60
73
|
"AsyncEffectInSyncRun",
|
|
61
74
|
"Cause",
|
|
75
|
+
"Cli",
|
|
62
76
|
"Clock",
|
|
63
77
|
"CurrentLogAnnotations",
|
|
64
78
|
"CurrentLogLevel",
|
|
@@ -72,24 +86,34 @@ __all__ = [
|
|
|
72
86
|
"Fail",
|
|
73
87
|
"Failure",
|
|
74
88
|
"Fiber",
|
|
89
|
+
"FileSystem",
|
|
90
|
+
"HttpClient",
|
|
75
91
|
"ImplicitRequirement",
|
|
76
92
|
"Interrupt",
|
|
77
93
|
"LogData",
|
|
78
94
|
"LogLevel",
|
|
79
95
|
"MinimumLogLevel",
|
|
80
96
|
"MissingRequirement",
|
|
97
|
+
"Path",
|
|
81
98
|
"PrettyFormatter",
|
|
99
|
+
"Process",
|
|
100
|
+
"Random",
|
|
101
|
+
"Schedule",
|
|
102
|
+
"Schema",
|
|
82
103
|
"Scope",
|
|
83
104
|
"Severity",
|
|
84
105
|
"Succeeded",
|
|
85
106
|
"TimeoutException",
|
|
107
|
+
"Tracer",
|
|
86
108
|
"UnhandledDefect",
|
|
87
109
|
"acquire_and_release",
|
|
88
110
|
"add_finalizer",
|
|
111
|
+
"annotate_current_span",
|
|
89
112
|
"annotate_logs",
|
|
90
113
|
"attempt",
|
|
91
114
|
"attempt_async",
|
|
92
115
|
"coroutine",
|
|
116
|
+
"current_span",
|
|
93
117
|
"die",
|
|
94
118
|
"fail",
|
|
95
119
|
"fork",
|
|
@@ -105,6 +129,7 @@ __all__ = [
|
|
|
105
129
|
"pretty_logger",
|
|
106
130
|
"provide_implicit",
|
|
107
131
|
"race_first",
|
|
132
|
+
"random",
|
|
108
133
|
"require",
|
|
109
134
|
"require_implicit",
|
|
110
135
|
"run_async",
|
|
@@ -119,5 +144,6 @@ __all__ = [
|
|
|
119
144
|
"suspend",
|
|
120
145
|
"sync",
|
|
121
146
|
"timeout",
|
|
147
|
+
"with_span",
|
|
122
148
|
"yield_now",
|
|
123
149
|
]
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
from collections.abc import Awaitable, Callable, Generator
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from datetime import timedelta
|
|
4
|
-
from typing import TYPE_CHECKING, Any, Literal, Never, final
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Literal, Never, Unpack, final, overload
|
|
5
5
|
|
|
6
6
|
from typing_extensions import TypeForm
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from effecton.catch import CatchBinder
|
|
10
|
+
from effecton.exit import Exit, Failure, Succeeded
|
|
10
11
|
from effecton.provide import ProvideBinder
|
|
12
|
+
from effecton.std.duration import Parts
|
|
13
|
+
from effecton.std.schedule import Schedule
|
|
11
14
|
from effecton.std.scope import Scope
|
|
12
15
|
from effecton.std.timeout import TimeoutException
|
|
16
|
+
from effecton.std.tracer import SpanKind
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
@dataclass(frozen=True)
|
|
@@ -69,7 +73,32 @@ class Effect[A, E: EffectonError = Never, R = Never]:
|
|
|
69
73
|
|
|
70
74
|
return CatchBinder(effect=self, error_type=error_type)
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
@overload
|
|
77
|
+
def on_exit[R2](
|
|
78
|
+
self, finalizer: Effect[Any, Never, R2]
|
|
79
|
+
) -> Effect[A, E, R | R2]: ...
|
|
80
|
+
|
|
81
|
+
@overload
|
|
82
|
+
def on_exit[R2](
|
|
83
|
+
# Spelled out rather than Exit[A, E]: ty infers variance through
|
|
84
|
+
# the alias as invariant, which would break covariance in A and E.
|
|
85
|
+
self,
|
|
86
|
+
finalizer: Callable[[Succeeded[A] | Failure[E]], Effect[Any, Never, R2]],
|
|
87
|
+
) -> Effect[A, E, R | R2]: ...
|
|
88
|
+
|
|
89
|
+
def on_exit(
|
|
90
|
+
self,
|
|
91
|
+
finalizer: Effect[Any, Any, Any] | Callable[[Any], Effect[Any, Any, Any]],
|
|
92
|
+
) -> Effect[Any, Any, Any]:
|
|
93
|
+
"""Run a finalizer once this effect settles, on any outcome.
|
|
94
|
+
|
|
95
|
+
The finalizer is an effect, or a function receiving the Exit
|
|
96
|
+
(Succeeded, or Failure carrying Fail, Die or Interrupt) that
|
|
97
|
+
returns one. A finalizer that fails or raises replaces the
|
|
98
|
+
outcome with its defect.
|
|
99
|
+
"""
|
|
100
|
+
if isinstance(finalizer, Effect):
|
|
101
|
+
return OnExit(self, lambda _: finalizer)
|
|
73
102
|
return OnExit(self, finalizer)
|
|
74
103
|
|
|
75
104
|
def provide[T](self, requirement_type: TypeForm[T]) -> ProvideBinder[A, E, R, T]:
|
|
@@ -84,10 +113,32 @@ class Effect[A, E: EffectonError = Never, R = Never]:
|
|
|
84
113
|
|
|
85
114
|
return scoped(self)
|
|
86
115
|
|
|
87
|
-
def timeout(
|
|
116
|
+
def timeout(
|
|
117
|
+
self, duration: timedelta | None = None, **parts: Unpack[Parts]
|
|
118
|
+
) -> Effect[A, E | TimeoutException, R]:
|
|
119
|
+
"""Fail with TimeoutException after a timedelta or its parts (seconds=...)."""
|
|
88
120
|
from effecton.std.timeout import timeout
|
|
89
121
|
|
|
90
|
-
return timeout(duration)(self)
|
|
122
|
+
return timeout(duration, **parts)(self)
|
|
123
|
+
|
|
124
|
+
def retry(
|
|
125
|
+
self,
|
|
126
|
+
schedule: Schedule | None = None,
|
|
127
|
+
*,
|
|
128
|
+
times: int | None = None,
|
|
129
|
+
until: Callable[[E], bool] | None = None,
|
|
130
|
+
) -> Effect[A, E, R]:
|
|
131
|
+
"""Re-run on failure while the schedule recurs, at most times more times."""
|
|
132
|
+
from effecton.std.retry import retry
|
|
133
|
+
|
|
134
|
+
return retry(self, schedule, times=times, until=until)
|
|
135
|
+
|
|
136
|
+
def with_span(
|
|
137
|
+
self, name: str, *, kind: SpanKind = "internal", **attributes: object
|
|
138
|
+
) -> Effect[A, E, R]:
|
|
139
|
+
from effecton.std.tracer import _with_span
|
|
140
|
+
|
|
141
|
+
return _with_span(self, name, kind=kind, **attributes)
|
|
91
142
|
|
|
92
143
|
def __iter__(self) -> Generator[Effect[A, E, R], Any, A]:
|
|
93
144
|
"""Make ``x = yield from effect`` infer ``x`` as A inside @gen.
|
|
@@ -163,7 +214,7 @@ class ProvideRequirement[A, E: EffectonError, R](Effect[A, E, R]):
|
|
|
163
214
|
@dataclass(frozen=True)
|
|
164
215
|
class OnExit[A, E: EffectonError, R](Effect[A, E, R]):
|
|
165
216
|
first: Effect[Any, Any, Any]
|
|
166
|
-
finalizer: Effect[Any, Any, Any]
|
|
217
|
+
finalizer: Callable[[Exit[Any, Any]], Effect[Any, Any, Any]]
|
|
167
218
|
kind: Literal["on_exit"] = "on_exit"
|
|
168
219
|
|
|
169
220
|
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from typing import Any, Literal, Never, assert_never, final
|
|
2
|
+
from typing import Any, Generic, Literal, Never, TypeVar, assert_never, final
|
|
3
3
|
|
|
4
4
|
from effecton.effect import Cause, Die, EffectonError, Fail, Interrupt
|
|
5
5
|
|
|
6
|
+
# Old-style TypeVars declare the covariance ty does not infer for a
|
|
7
|
+
# generic dataclass; without it, an Exit in an Effect method signature
|
|
8
|
+
# would make A and E invariant. See the ty inference notes in CLAUDE.md.
|
|
9
|
+
A = TypeVar("A", covariant=True)
|
|
10
|
+
E = TypeVar("E", bound=EffectonError, covariant=True, default=Never)
|
|
11
|
+
|
|
6
12
|
|
|
7
13
|
@final
|
|
8
14
|
@dataclass(frozen=True)
|
|
9
|
-
class Succeeded[A]:
|
|
15
|
+
class Succeeded(Generic[A]): # noqa: UP046
|
|
10
16
|
value: A
|
|
11
17
|
kind: Literal["succeeded"] = "succeeded"
|
|
12
18
|
|
|
13
19
|
|
|
14
20
|
@final
|
|
15
21
|
@dataclass(frozen=True)
|
|
16
|
-
class Failure[E
|
|
22
|
+
class Failure(Generic[E]):
|
|
17
23
|
cause: Cause[E]
|
|
18
24
|
kind: Literal["failure"] = "failure"
|
|
19
25
|
|
|
@@ -6,7 +6,10 @@ effecton is installed. A test that returns an Effect, typically a
|
|
|
6
6
|
failure raises its cause, so pytest reports a typed error, a defect or
|
|
7
7
|
an interruption like any exception. A test that requests the test_clock
|
|
8
8
|
fixture has that clock provided to its effect, so E.now(), E.sleep()
|
|
9
|
-
and the clock's movers all see the same clock
|
|
9
|
+
and the clock's movers all see the same clock; likewise test_random
|
|
10
|
+
provides a seeded Random, so every draw through E.random() is
|
|
11
|
+
reproducible, and test_tracer provides a recording Tracer, so every
|
|
12
|
+
span the test opens can be inspected afterwards.
|
|
10
13
|
"""
|
|
11
14
|
|
|
12
15
|
import inspect
|
|
@@ -18,7 +21,7 @@ import pytest
|
|
|
18
21
|
from effecton.effect import Effect
|
|
19
22
|
from effecton.exit import unwrap
|
|
20
23
|
from effecton.run_async import run_async_exit
|
|
21
|
-
from effecton.std import clock
|
|
24
|
+
from effecton.std import clock, random, tracer
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
@pytest.fixture
|
|
@@ -27,6 +30,18 @@ def test_clock() -> clock.Test:
|
|
|
27
30
|
return clock.Test()
|
|
28
31
|
|
|
29
32
|
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def test_random() -> random.Test:
|
|
35
|
+
"""A Random seeded with 0, provided to the test's effect."""
|
|
36
|
+
return random.Test()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def test_tracer() -> tracer.Test:
|
|
41
|
+
"""A recording Tracer, provided to the test's effect."""
|
|
42
|
+
return tracer.Test()
|
|
43
|
+
|
|
44
|
+
|
|
30
45
|
@pytest.hookimpl(tryfirst=True)
|
|
31
46
|
def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> bool | None:
|
|
32
47
|
function = pyfuncitem.obj
|
|
@@ -42,6 +57,12 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> bool | None:
|
|
|
42
57
|
if "test_clock" in arguments:
|
|
43
58
|
provided = cast("clock.Test", arguments["test_clock"])
|
|
44
59
|
effect = effect.provide(clock.Protocol)(provided)
|
|
60
|
+
if "test_random" in arguments:
|
|
61
|
+
seeded = cast("random.Test", arguments["test_random"])
|
|
62
|
+
effect = effect.provide(random.Protocol)(seeded)
|
|
63
|
+
if "test_tracer" in arguments:
|
|
64
|
+
recording = cast("tracer.Test", arguments["test_tracer"])
|
|
65
|
+
effect = effect.provide(tracer.Protocol)(recording)
|
|
45
66
|
unwrap(run_async_exit(effect))
|
|
46
67
|
elif result is not None:
|
|
47
68
|
warnings.warn(
|
|
@@ -29,6 +29,7 @@ from effecton.run_sync import (
|
|
|
29
29
|
OnExitFrame,
|
|
30
30
|
RestoreEnv,
|
|
31
31
|
default_or_die,
|
|
32
|
+
exit_of,
|
|
32
33
|
run_fn_or_die,
|
|
33
34
|
)
|
|
34
35
|
from effecton.std import clock
|
|
@@ -179,7 +180,9 @@ async def _interpret[A, E: EffectonError](
|
|
|
179
180
|
case OnExitFrame(finalizer):
|
|
180
181
|
stack.append(Finalizing(outcome=current))
|
|
181
182
|
finalizing += 1
|
|
182
|
-
current =
|
|
183
|
+
current = guarded(
|
|
184
|
+
run_fn_or_die, finalizer, exit_of(current)
|
|
185
|
+
)
|
|
183
186
|
break
|
|
184
187
|
case Finalizing(outcome):
|
|
185
188
|
finalizing -= 1
|
|
@@ -205,7 +208,9 @@ async def _interpret[A, E: EffectonError](
|
|
|
205
208
|
case OnExitFrame(finalizer):
|
|
206
209
|
stack.append(Finalizing(outcome=current))
|
|
207
210
|
finalizing += 1
|
|
208
|
-
current =
|
|
211
|
+
current = guarded(
|
|
212
|
+
run_fn_or_die, finalizer, exit_of(current)
|
|
213
|
+
)
|
|
209
214
|
break
|
|
210
215
|
case Finalizing():
|
|
211
216
|
# The finalizer died; its defect replaces the
|
|
@@ -65,7 +65,7 @@ class RestoreEnv:
|
|
|
65
65
|
@final
|
|
66
66
|
@dataclass(frozen=True)
|
|
67
67
|
class OnExitFrame:
|
|
68
|
-
finalizer: Effect[Any, Any, Any]
|
|
68
|
+
finalizer: Callable[[Exit[Any, Any]], Effect[Any, Any, Any]]
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
Frame = FlatMap[Any, Any, Any] | OnFailure[Any, Any, Any] | RestoreEnv | OnExitFrame
|
|
@@ -103,7 +103,8 @@ def run_sync_exit[A, E: EffectonError](effect: Effect[A, E]) -> Exit[A, E]:
|
|
|
103
103
|
case RestoreEnv():
|
|
104
104
|
env = item.env
|
|
105
105
|
case OnExitFrame(finalizer):
|
|
106
|
-
|
|
106
|
+
cleanup = run_fn_or_die(finalizer, exit_of(current))
|
|
107
|
+
current = cleanup.flat_map(resume(current)) # ty: ignore[invalid-assignment]
|
|
107
108
|
break
|
|
108
109
|
case FlatMap():
|
|
109
110
|
current = run_fn_or_die(item.and_then, value)
|
|
@@ -123,7 +124,8 @@ def run_sync_exit[A, E: EffectonError](effect: Effect[A, E]) -> Exit[A, E]:
|
|
|
123
124
|
case RestoreEnv():
|
|
124
125
|
env = item.env
|
|
125
126
|
case OnExitFrame(finalizer):
|
|
126
|
-
|
|
127
|
+
cleanup = run_fn_or_die(finalizer, exit_of(current))
|
|
128
|
+
current = cleanup.flat_map(resume(current)) # ty: ignore[invalid-assignment]
|
|
127
129
|
break
|
|
128
130
|
case FlatMap():
|
|
129
131
|
continue
|
|
@@ -195,6 +197,15 @@ def run_fn_or_die(f: Callable[[Any], Effect[Any, Any]], value: object) -> Node:
|
|
|
195
197
|
return FailCause(cause=Die(defect=e))
|
|
196
198
|
|
|
197
199
|
|
|
200
|
+
def exit_of(outcome: Success[Any] | FailCause[Any]) -> Exit[Any, Any]:
|
|
201
|
+
"""The settled outcome as the Exit an on_exit finalizer receives."""
|
|
202
|
+
match outcome:
|
|
203
|
+
case Success(value):
|
|
204
|
+
return Succeeded(value=value)
|
|
205
|
+
case FailCause(cause):
|
|
206
|
+
return Failure(cause=cause)
|
|
207
|
+
|
|
208
|
+
|
|
198
209
|
# Captures the current outcome by closure.
|
|
199
210
|
def resume(outcome: Node) -> Callable[[Any], Effect[Any, Any, Any]]:
|
|
200
211
|
def resume(_: object) -> Effect[Any, Any, Any]:
|