pykardia 0.1.0.dev1__py3-none-any.whl
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.
- pykardia/__init__.py +139 -0
- pykardia/__main__.py +5 -0
- pykardia/catalog.py +17 -0
- pykardia/cli.py +103 -0
- pykardia/core/__init__.py +103 -0
- pykardia/core/cleanup.py +128 -0
- pykardia/core/config.py +213 -0
- pykardia/core/context.py +619 -0
- pykardia/core/deps.py +53 -0
- pykardia/core/effect.py +209 -0
- pykardia/core/error.py +149 -0
- pykardia/core/events.py +145 -0
- pykardia/core/fiber.py +243 -0
- pykardia/core/id_table.py +55 -0
- pykardia/core/json.py +295 -0
- pykardia/core/key.py +88 -0
- pykardia/core/logger.py +186 -0
- pykardia/core/plugin.py +262 -0
- pykardia/core/registry.py +107 -0
- pykardia/core/root.py +149 -0
- pykardia/core/runtime.py +146 -0
- pykardia/core/scope.py +190 -0
- pykardia/core/service.py +123 -0
- pykardia/core/state.py +555 -0
- pykardia/core/status.py +14 -0
- pykardia/core/value.py +51 -0
- pykardia/core/wire.py +179 -0
- pykardia/decorators.py +144 -0
- pykardia/fs/__init__.py +18 -0
- pykardia/fs/filter.py +92 -0
- pykardia/fs/glob.py +121 -0
- pykardia/fs/native.py +63 -0
- pykardia/fs/service.py +55 -0
- pykardia/fs/watcher.py +216 -0
- pykardia/guest/__init__.py +18 -0
- pykardia/guest/abi.py +75 -0
- pykardia/guest/api.py +90 -0
- pykardia/guest/export.py +225 -0
- pykardia/guest/meta.py +68 -0
- pykardia/guest/native.py +122 -0
- pykardia/guest/process.py +91 -0
- pykardia/guest/runtime.py +70 -0
- pykardia/guest/templates/wasm.c +124 -0
- pykardia/guest/wasm_build.py +252 -0
- pykardia/hmr/__init__.py +37 -0
- pykardia/hmr/dylib.py +185 -0
- pykardia/hmr/graph.py +212 -0
- pykardia/hmr/guest.py +292 -0
- pykardia/hmr/hmr.py +124 -0
- pykardia/hmr/process.py +170 -0
- pykardia/hmr/transaction.py +287 -0
- pykardia/hmr/wasm.py +173 -0
- pykardia/hmr/watch.py +88 -0
- pykardia/include/__init__.py +27 -0
- pykardia/include/document.py +143 -0
- pykardia/include/identity.py +76 -0
- pykardia/include/include.py +614 -0
- pykardia/include/journal.py +204 -0
- pykardia/include/patch.py +297 -0
- pykardia/include/tree.py +105 -0
- pykardia/include/yaml.py +432 -0
- pykardia/loader/__init__.py +63 -0
- pykardia/loader/_unicode.py +3242 -0
- pykardia/loader/backend.py +95 -0
- pykardia/loader/catalog.py +83 -0
- pykardia/loader/expr.py +562 -0
- pykardia/loader/group.py +25 -0
- pykardia/loader/handles.py +288 -0
- pykardia/loader/loader.py +915 -0
- pykardia/loader/options.py +195 -0
- pykardia/loader/state.py +333 -0
- pykardia/logger_console/__init__.py +19 -0
- pykardia/logger_console/config.py +38 -0
- pykardia/logger_console/exporter.py +85 -0
- pykardia/logger_console/format.py +139 -0
- pykardia/logger_console/plugin.py +18 -0
- pykardia/prelude.py +23 -0
- pykardia/py.typed +0 -0
- pykardia/timer/__init__.py +16 -0
- pykardia/timer/context.py +13 -0
- pykardia/timer/schedule.py +85 -0
- pykardia/timer/service.py +221 -0
- pykardia/timer/ticks.py +96 -0
- pykardia/version.py +3 -0
- pykardia-0.1.0.dev1.dist-info/METADATA +70 -0
- pykardia-0.1.0.dev1.dist-info/RECORD +91 -0
- pykardia-0.1.0.dev1.dist-info/WHEEL +5 -0
- pykardia-0.1.0.dev1.dist-info/entry_points.txt +2 -0
- pykardia-0.1.0.dev1.dist-info/licenses/LICENSE +674 -0
- pykardia-0.1.0.dev1.dist-info/licenses/NOTICE +50 -0
- pykardia-0.1.0.dev1.dist-info/top_level.txt +1 -0
pykardia/__init__.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""PyKardia: revertible effects and reactive services."""
|
|
2
|
+
|
|
3
|
+
from pykardia import fs, guest, hmr, include, loader, logger_console, prelude, timer
|
|
4
|
+
from pykardia.catalog import catalog
|
|
5
|
+
from pykardia.core import (
|
|
6
|
+
AccessError,
|
|
7
|
+
AggregateError,
|
|
8
|
+
AlreadyProvided,
|
|
9
|
+
Check,
|
|
10
|
+
ConfigCommitted,
|
|
11
|
+
Context,
|
|
12
|
+
Deps,
|
|
13
|
+
Disposal,
|
|
14
|
+
Disposer,
|
|
15
|
+
EffectHandle,
|
|
16
|
+
EffectMeta,
|
|
17
|
+
EffectScope,
|
|
18
|
+
ErasedConfig,
|
|
19
|
+
Error,
|
|
20
|
+
Event,
|
|
21
|
+
Exporter,
|
|
22
|
+
Fiber,
|
|
23
|
+
FiberId,
|
|
24
|
+
FiberState,
|
|
25
|
+
ForeignBinding,
|
|
26
|
+
IdTable,
|
|
27
|
+
ImplId,
|
|
28
|
+
Inactive,
|
|
29
|
+
InactiveEffect,
|
|
30
|
+
Inject,
|
|
31
|
+
Injected,
|
|
32
|
+
Interrupted,
|
|
33
|
+
Issue,
|
|
34
|
+
JsonValue,
|
|
35
|
+
Key,
|
|
36
|
+
Level,
|
|
37
|
+
Logger,
|
|
38
|
+
Message,
|
|
39
|
+
NoConfig,
|
|
40
|
+
NotProvided,
|
|
41
|
+
Overlay,
|
|
42
|
+
Panicked,
|
|
43
|
+
PathSegment,
|
|
44
|
+
Plugin,
|
|
45
|
+
PluginDef,
|
|
46
|
+
PluginInserted,
|
|
47
|
+
PluginRemoved,
|
|
48
|
+
PyKardiaError,
|
|
49
|
+
RealmId,
|
|
50
|
+
Registry,
|
|
51
|
+
Replaced,
|
|
52
|
+
RuntimeKey,
|
|
53
|
+
Service,
|
|
54
|
+
ServiceChanged,
|
|
55
|
+
Settle,
|
|
56
|
+
StatusChanged,
|
|
57
|
+
Svc,
|
|
58
|
+
TypeMismatch,
|
|
59
|
+
Undeclared,
|
|
60
|
+
ValidationError,
|
|
61
|
+
decode,
|
|
62
|
+
hyphenate,
|
|
63
|
+
merge_shallow,
|
|
64
|
+
)
|
|
65
|
+
from pykardia.decorators import plugin, service
|
|
66
|
+
from pykardia.version import __version__
|
|
67
|
+
|
|
68
|
+
__all__: list[str] = [
|
|
69
|
+
"AccessError",
|
|
70
|
+
"AggregateError",
|
|
71
|
+
"AlreadyProvided",
|
|
72
|
+
"Check",
|
|
73
|
+
"ConfigCommitted",
|
|
74
|
+
"Context",
|
|
75
|
+
"Deps",
|
|
76
|
+
"Disposal",
|
|
77
|
+
"Disposer",
|
|
78
|
+
"EffectHandle",
|
|
79
|
+
"EffectMeta",
|
|
80
|
+
"EffectScope",
|
|
81
|
+
"ErasedConfig",
|
|
82
|
+
"Error",
|
|
83
|
+
"Event",
|
|
84
|
+
"Exporter",
|
|
85
|
+
"Fiber",
|
|
86
|
+
"FiberId",
|
|
87
|
+
"FiberState",
|
|
88
|
+
"ForeignBinding",
|
|
89
|
+
"IdTable",
|
|
90
|
+
"ImplId",
|
|
91
|
+
"Inactive",
|
|
92
|
+
"InactiveEffect",
|
|
93
|
+
"Inject",
|
|
94
|
+
"Injected",
|
|
95
|
+
"Interrupted",
|
|
96
|
+
"Issue",
|
|
97
|
+
"JsonValue",
|
|
98
|
+
"PyKardiaError",
|
|
99
|
+
"Key",
|
|
100
|
+
"Level",
|
|
101
|
+
"Logger",
|
|
102
|
+
"Message",
|
|
103
|
+
"NoConfig",
|
|
104
|
+
"NotProvided",
|
|
105
|
+
"Overlay",
|
|
106
|
+
"Panicked",
|
|
107
|
+
"PathSegment",
|
|
108
|
+
"Plugin",
|
|
109
|
+
"PluginDef",
|
|
110
|
+
"PluginInserted",
|
|
111
|
+
"PluginRemoved",
|
|
112
|
+
"RealmId",
|
|
113
|
+
"Registry",
|
|
114
|
+
"Replaced",
|
|
115
|
+
"RuntimeKey",
|
|
116
|
+
"Service",
|
|
117
|
+
"ServiceChanged",
|
|
118
|
+
"Settle",
|
|
119
|
+
"StatusChanged",
|
|
120
|
+
"Svc",
|
|
121
|
+
"TypeMismatch",
|
|
122
|
+
"Undeclared",
|
|
123
|
+
"ValidationError",
|
|
124
|
+
"__version__",
|
|
125
|
+
"catalog",
|
|
126
|
+
"decode",
|
|
127
|
+
"fs",
|
|
128
|
+
"guest",
|
|
129
|
+
"hmr",
|
|
130
|
+
"hyphenate",
|
|
131
|
+
"include",
|
|
132
|
+
"loader",
|
|
133
|
+
"logger_console",
|
|
134
|
+
"merge_shallow",
|
|
135
|
+
"plugin",
|
|
136
|
+
"prelude",
|
|
137
|
+
"service",
|
|
138
|
+
"timer",
|
|
139
|
+
]
|
pykardia/__main__.py
ADDED
pykardia/catalog.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Unified catalog for built-in plugins and their original Cordis package aliases."""
|
|
2
|
+
|
|
3
|
+
from pykardia.fs import FsPlugin
|
|
4
|
+
from pykardia.include import IncludePlugin
|
|
5
|
+
from pykardia.loader import Catalog, builtins
|
|
6
|
+
from pykardia.logger_console import ConsolePlugin
|
|
7
|
+
from pykardia.timer import TimerPlugin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def catalog() -> Catalog:
|
|
11
|
+
"""Create a fresh catalog with all built-ins and no implicit HMR module graph."""
|
|
12
|
+
result = builtins()
|
|
13
|
+
result.register(TimerPlugin).alias("@cordisjs/plugin-timer", "timer")
|
|
14
|
+
result.register(ConsolePlugin).alias("@cordisjs/plugin-logger-console", "logger-console")
|
|
15
|
+
result.register(FsPlugin).alias("@cordisjs/plugin-fs-node", "fs")
|
|
16
|
+
result.register(IncludePlugin).alias("@cordisjs/plugin-include", "include")
|
|
17
|
+
return result
|
pykardia/cli.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Run a live file-backed pykardia application until Ctrl-C."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import signal
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Awaitable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import FrameType
|
|
9
|
+
|
|
10
|
+
from pykardia.catalog import catalog
|
|
11
|
+
from pykardia.core.context import Context
|
|
12
|
+
from pykardia.core.plugin import PluginDef
|
|
13
|
+
from pykardia.fs import FsPlugin
|
|
14
|
+
from pykardia.hmr import DylibSource, Hmr, ModuleCode, ModuleGraph, ModuleSource, WasmSource
|
|
15
|
+
from pykardia.loader import Catalog, Loader, Resolver
|
|
16
|
+
from pykardia.logger_console import ConsoleConfig, ConsoleExporter
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> int:
|
|
20
|
+
"""Start with the first path argument, defaulting to pykardia.yml."""
|
|
21
|
+
return asyncio.run(start(sys.argv[1] if len(sys.argv) > 1 else "pykardia.yml"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def start(path: str) -> int:
|
|
25
|
+
"""Report startup or cleanup errors through the app logger and return its exit status."""
|
|
26
|
+
root = Context()
|
|
27
|
+
status = 0
|
|
28
|
+
try:
|
|
29
|
+
await run(root, path)
|
|
30
|
+
except Exception as error:
|
|
31
|
+
root.logger_named("app").log_error(error)
|
|
32
|
+
status = 1
|
|
33
|
+
try:
|
|
34
|
+
await root.fiber().dispose()
|
|
35
|
+
except Exception as error:
|
|
36
|
+
root.logger_named("app").log_error(error)
|
|
37
|
+
status = 1
|
|
38
|
+
return status
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def run(root: Context, path: str) -> None:
|
|
42
|
+
"""Boot console, fs, loader and watched include in source order, then await Ctrl-C."""
|
|
43
|
+
root.add_exporter(ConsoleExporter(ConsoleConfig()))
|
|
44
|
+
root.plugin(FsPlugin, None)
|
|
45
|
+
plugins = catalog()
|
|
46
|
+
graph = ModuleGraph(GuestFiles())
|
|
47
|
+
plugins.register_def("hmr", Hmr.plugin(graph)).alias("@cordisjs/plugin-hmr", "hmr")
|
|
48
|
+
resolver = Plugins(plugins, graph, Path(path).absolute().parent)
|
|
49
|
+
await root.plugin_def(Loader.plugin(resolver)).settled()
|
|
50
|
+
loader = root.get(Loader).arc()
|
|
51
|
+
loader.set_logs(True)
|
|
52
|
+
id = await loader.create({"name": "include", "config": {"path": path, "watch": True}})
|
|
53
|
+
fiber = loader.resolve(id).inner_fiber()
|
|
54
|
+
if fiber is not None and (error := fiber.error()) is not None:
|
|
55
|
+
raise error
|
|
56
|
+
await interrupted()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def interrupted() -> None:
|
|
60
|
+
"""Wait for Ctrl-C or Windows Ctrl-Break and restore the previous signal handlers."""
|
|
61
|
+
loop = asyncio.get_running_loop()
|
|
62
|
+
stopped = asyncio.Event()
|
|
63
|
+
signals = (signal.SIGINT, signal.SIGBREAK) if sys.platform == "win32" else (signal.SIGINT,)
|
|
64
|
+
previous = {number: signal.getsignal(number) for number in signals}
|
|
65
|
+
|
|
66
|
+
def stop(number: int, frame: FrameType | None) -> None:
|
|
67
|
+
"""Wake the event loop even when it is waiting on Windows I/O."""
|
|
68
|
+
loop.call_soon_threadsafe(stopped.set)
|
|
69
|
+
|
|
70
|
+
for number in signals:
|
|
71
|
+
signal.signal(number, stop)
|
|
72
|
+
try:
|
|
73
|
+
await stopped.wait()
|
|
74
|
+
finally:
|
|
75
|
+
for number, handler in previous.items():
|
|
76
|
+
signal.signal(number, handler)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Plugins(Resolver):
|
|
80
|
+
"""Resolve built-in names before loading guest paths relative to the root config file."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, plugins: Catalog, graph: ModuleGraph, base: Path) -> None:
|
|
83
|
+
"""Retain a shared catalog, module graph and lexical base directory."""
|
|
84
|
+
self.catalog, self.graph, self.base = plugins, graph, base
|
|
85
|
+
|
|
86
|
+
def resolve(self, name: str) -> Awaitable[PluginDef]:
|
|
87
|
+
"""Snapshot built-ins or import the corresponding guest module."""
|
|
88
|
+
if self.catalog.get(name) is not None:
|
|
89
|
+
return self.catalog.resolve(name)
|
|
90
|
+
return self.graph.resolve(str(self.base / name))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class GuestFiles(ModuleSource):
|
|
94
|
+
"""Select WASM for .wasm/.wat and the native C ABI for other file extensions."""
|
|
95
|
+
|
|
96
|
+
def __init__(self) -> None:
|
|
97
|
+
"""Retain one shared WASM engine and a native loader."""
|
|
98
|
+
self.wasm, self.dylib = WasmSource(), DylibSource()
|
|
99
|
+
|
|
100
|
+
def load(self, id: str) -> ModuleCode:
|
|
101
|
+
"""Load the selected real backend without treating executables as CLI process guests."""
|
|
102
|
+
source = self.wasm if Path(id).suffix in (".wasm", ".wat") else self.dylib
|
|
103
|
+
return source.load(id)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Core runtime entry points."""
|
|
2
|
+
|
|
3
|
+
from pykardia.core.cleanup import Disposal, Disposer
|
|
4
|
+
from pykardia.core.config import ErasedConfig, Issue, NoConfig, PathSegment, ValidationError, decode
|
|
5
|
+
from pykardia.core.context import Context, merge_shallow
|
|
6
|
+
from pykardia.core.deps import Deps, Injected
|
|
7
|
+
from pykardia.core.effect import EffectHandle, EffectMeta, EffectScope
|
|
8
|
+
from pykardia.core.error import (
|
|
9
|
+
AccessError,
|
|
10
|
+
AggregateError,
|
|
11
|
+
AlreadyProvided,
|
|
12
|
+
Error,
|
|
13
|
+
ForeignBinding,
|
|
14
|
+
Inactive,
|
|
15
|
+
InactiveEffect,
|
|
16
|
+
Interrupted,
|
|
17
|
+
NotProvided,
|
|
18
|
+
Panicked,
|
|
19
|
+
PyKardiaError,
|
|
20
|
+
TypeMismatch,
|
|
21
|
+
Undeclared,
|
|
22
|
+
)
|
|
23
|
+
from pykardia.core.events import (
|
|
24
|
+
ConfigCommitted,
|
|
25
|
+
Event,
|
|
26
|
+
PluginInserted,
|
|
27
|
+
PluginRemoved,
|
|
28
|
+
Replaced,
|
|
29
|
+
ServiceChanged,
|
|
30
|
+
StatusChanged,
|
|
31
|
+
)
|
|
32
|
+
from pykardia.core.fiber import Fiber, Settle
|
|
33
|
+
from pykardia.core.id_table import IdTable
|
|
34
|
+
from pykardia.core.key import FiberId, ImplId, Key, RealmId
|
|
35
|
+
from pykardia.core.logger import Exporter, Level, Logger, Message, hyphenate
|
|
36
|
+
from pykardia.core.plugin import Inject, Plugin, PluginDef, RuntimeKey
|
|
37
|
+
from pykardia.core.registry import Registry
|
|
38
|
+
from pykardia.core.scope import Overlay
|
|
39
|
+
from pykardia.core.service import Service, Svc
|
|
40
|
+
from pykardia.core.state import Check
|
|
41
|
+
from pykardia.core.status import FiberState
|
|
42
|
+
from pykardia.core.value import JsonValue
|
|
43
|
+
|
|
44
|
+
__all__: list[str] = [
|
|
45
|
+
"AccessError",
|
|
46
|
+
"AggregateError",
|
|
47
|
+
"AlreadyProvided",
|
|
48
|
+
"Check",
|
|
49
|
+
"ConfigCommitted",
|
|
50
|
+
"Context",
|
|
51
|
+
"Deps",
|
|
52
|
+
"Disposal",
|
|
53
|
+
"Disposer",
|
|
54
|
+
"EffectHandle",
|
|
55
|
+
"EffectMeta",
|
|
56
|
+
"EffectScope",
|
|
57
|
+
"ErasedConfig",
|
|
58
|
+
"Error",
|
|
59
|
+
"Event",
|
|
60
|
+
"Exporter",
|
|
61
|
+
"Fiber",
|
|
62
|
+
"FiberId",
|
|
63
|
+
"FiberState",
|
|
64
|
+
"ForeignBinding",
|
|
65
|
+
"IdTable",
|
|
66
|
+
"ImplId",
|
|
67
|
+
"Inactive",
|
|
68
|
+
"InactiveEffect",
|
|
69
|
+
"Inject",
|
|
70
|
+
"Injected",
|
|
71
|
+
"Interrupted",
|
|
72
|
+
"Issue",
|
|
73
|
+
"JsonValue",
|
|
74
|
+
"PyKardiaError",
|
|
75
|
+
"Key",
|
|
76
|
+
"Level",
|
|
77
|
+
"Logger",
|
|
78
|
+
"Message",
|
|
79
|
+
"NoConfig",
|
|
80
|
+
"NotProvided",
|
|
81
|
+
"Overlay",
|
|
82
|
+
"Panicked",
|
|
83
|
+
"PathSegment",
|
|
84
|
+
"Plugin",
|
|
85
|
+
"PluginDef",
|
|
86
|
+
"PluginInserted",
|
|
87
|
+
"PluginRemoved",
|
|
88
|
+
"RealmId",
|
|
89
|
+
"Registry",
|
|
90
|
+
"Replaced",
|
|
91
|
+
"RuntimeKey",
|
|
92
|
+
"Service",
|
|
93
|
+
"ServiceChanged",
|
|
94
|
+
"Settle",
|
|
95
|
+
"StatusChanged",
|
|
96
|
+
"Svc",
|
|
97
|
+
"TypeMismatch",
|
|
98
|
+
"Undeclared",
|
|
99
|
+
"ValidationError",
|
|
100
|
+
"decode",
|
|
101
|
+
"hyphenate",
|
|
102
|
+
"merge_shallow",
|
|
103
|
+
]
|
pykardia/core/cleanup.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Reversible actions with a synchronous prefix and sequential LIFO cleanup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Awaitable, Callable, Generator
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
from pykardia.core.runtime import Completion, Runtime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Disposal:
|
|
13
|
+
"""Observe cleanup that has already started and proceeds without its observer."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, completion: Completion[None]) -> None:
|
|
16
|
+
"""Wrap the runtime-owned result of a cleanup sequence."""
|
|
17
|
+
self._completion = completion
|
|
18
|
+
self._consumed = False
|
|
19
|
+
|
|
20
|
+
def is_ready(self) -> bool:
|
|
21
|
+
"""Return whether cleanup finished synchronously, even after async completion."""
|
|
22
|
+
return self._completion.is_ready()
|
|
23
|
+
|
|
24
|
+
def _take_ready(self) -> None:
|
|
25
|
+
"""Consume a synchronous result, matching Disposal::Ready's one-shot result."""
|
|
26
|
+
if not self._consumed:
|
|
27
|
+
self._consumed = True
|
|
28
|
+
self._completion.result()
|
|
29
|
+
|
|
30
|
+
async def wait(self) -> None:
|
|
31
|
+
"""Observe cleanup without propagating waiter cancellation into its task."""
|
|
32
|
+
if self.is_ready():
|
|
33
|
+
self._take_ready()
|
|
34
|
+
else:
|
|
35
|
+
await self._completion
|
|
36
|
+
|
|
37
|
+
def __await__(self) -> Generator[object, None, None]:
|
|
38
|
+
"""Wait for the cleanup's result."""
|
|
39
|
+
return self.wait().__await__()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Disposable(Protocol):
|
|
43
|
+
"""An effect handle whose inverse can join another cleanup sequence."""
|
|
44
|
+
|
|
45
|
+
def dispose(self) -> Disposal:
|
|
46
|
+
"""Start cleanup and return its completion."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Disposer:
|
|
51
|
+
"""An inverse consumed at most once; its callback runs when disposal starts."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, action: Callable[[], Awaitable[None] | None]) -> None:
|
|
54
|
+
"""Retain a callback that may finish immediately or return asynchronous work."""
|
|
55
|
+
self._action: Callable[[], Awaitable[None] | None] | None = action
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def new(cls, action: Callable[[], None]) -> Disposer:
|
|
59
|
+
"""Create a synchronous inverse; exceptions become cleanup failures."""
|
|
60
|
+
return cls(action)
|
|
61
|
+
|
|
62
|
+
fallible = new
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def future(cls, action: Callable[[], Awaitable[None]]) -> Disposer:
|
|
66
|
+
"""Create an asynchronous inverse with a synchronously invoked factory."""
|
|
67
|
+
return cls(action)
|
|
68
|
+
|
|
69
|
+
try_future = future
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_handle(cls, handle: Disposable) -> Disposer:
|
|
73
|
+
"""Adopt a nested handle without turning synchronous cleanup asynchronous."""
|
|
74
|
+
return cls(handle.dispose)
|
|
75
|
+
|
|
76
|
+
def call(self) -> Awaitable[None] | None:
|
|
77
|
+
"""Consume the inverse and return only its unfinished asynchronous portion."""
|
|
78
|
+
action, self._action = self._action, None
|
|
79
|
+
if action is None:
|
|
80
|
+
return None
|
|
81
|
+
result = action()
|
|
82
|
+
if isinstance(result, Disposal) and result.is_ready():
|
|
83
|
+
result._take_ready()
|
|
84
|
+
return None
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def run_sequence(runtime: Runtime, disposers: list[Disposer]) -> Disposal:
|
|
89
|
+
"""Run inverses newest first, continuing after failures and reporting the first.
|
|
90
|
+
|
|
91
|
+
Synchronous callbacks run before this function returns. From the first awaitable
|
|
92
|
+
onward, the runtime owns a task which runs the remainder strictly in sequence.
|
|
93
|
+
"""
|
|
94
|
+
rest = list(disposers)
|
|
95
|
+
first_error: BaseException | None = None
|
|
96
|
+
while rest:
|
|
97
|
+
try:
|
|
98
|
+
pending = rest.pop().call()
|
|
99
|
+
except (Exception, asyncio.CancelledError) as error:
|
|
100
|
+
if first_error is None:
|
|
101
|
+
first_error = error
|
|
102
|
+
continue
|
|
103
|
+
if pending is not None:
|
|
104
|
+
return Disposal(runtime.spawn(_finish_sequence(pending, rest, first_error)))
|
|
105
|
+
return Disposal(
|
|
106
|
+
Completion.ready(None) if first_error is None else Completion.failed(first_error)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def _finish_sequence(
|
|
111
|
+
pending: Awaitable[None], rest: list[Disposer], first_error: BaseException | None
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Finish the first pending inverse, then run all remaining inverses in order."""
|
|
114
|
+
try:
|
|
115
|
+
await pending
|
|
116
|
+
except (Exception, asyncio.CancelledError) as error:
|
|
117
|
+
if first_error is None:
|
|
118
|
+
first_error = error
|
|
119
|
+
while rest:
|
|
120
|
+
try:
|
|
121
|
+
following = rest.pop().call()
|
|
122
|
+
if following is not None:
|
|
123
|
+
await following
|
|
124
|
+
except (Exception, asyncio.CancelledError) as error:
|
|
125
|
+
if first_error is None:
|
|
126
|
+
first_error = error
|
|
127
|
+
if first_error is not None:
|
|
128
|
+
raise first_error
|