affective-py 0.1.0__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.
affective/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from affective.core.effects import Effect as Effect
2
+ from affective.core.effects import Raise as Raise
3
+ from affective.core.effects import Async as Async
4
+ from affective.core.effects import Affects as Affects
5
+ from affective.core.handlers import catch as catch
6
+ from affective.core.effects import operation as operation
7
+ from affective.core.continuation import \
8
+ RunningContinuation as RunningContinuation
9
+ from affective.core.continuation import Continuation as Continuation
10
+ from affective.core.handlers import handler as handler
11
+ from affective.core.loop import UnhandledEffect as UnhandledEffect
12
+ from affective.core.loop import run as run
13
+ from affective.core.loop import handle as handle
File without changes
@@ -0,0 +1,8 @@
1
+ from collections.abc import Callable, Generator
2
+ from typing import Any, ParamSpec, TypeAlias
3
+
4
+ # bcz mkdocstrings does not understand PEP695
5
+
6
+ RunningContinuation: TypeAlias = Generator[Any, Any, Any]
7
+ _ExpectsInput = ParamSpec("_ExpectsInput")
8
+ Continuation: TypeAlias = Callable[_ExpectsInput, RunningContinuation]
@@ -0,0 +1,55 @@
1
+ from collections.abc import Generator, Callable
2
+ from dataclasses import dataclass
3
+ from functools import wraps
4
+ from typing import (
5
+ Annotated, Any, Awaitable, Sequence,
6
+ Mapping, final, cast, TypeVar
7
+ )
8
+
9
+
10
+ @dataclass
11
+ @final
12
+ class Perform:
13
+ effect_type: Any
14
+ effect_args: Sequence[Any]
15
+ effect_kwargs: Mapping[str, Any]
16
+
17
+
18
+ class Effect:
19
+ ...
20
+
21
+
22
+ class _StaticGeneratorMethod[T]:
23
+ # This is some black magic by mypy & Gemini
24
+ def __get__(self, instance: Any, owner: type | None = None) -> T:
25
+ raise NotImplementedError
26
+
27
+
28
+ def operation[**P, R](f: Callable[P, Affects[R]]) -> _StaticGeneratorMethod[
29
+ Callable[P, Generator[Perform, Any, R]]
30
+ ]:
31
+ @wraps(f)
32
+ def wrapper(
33
+ *args: P.args, **kwargs: P.kwargs
34
+ ) -> Generator[Perform, R | Perform, R]:
35
+ ret = yield Perform(wrapper, args, kwargs)
36
+ while ret.__class__ is Perform:
37
+ ret = yield ret
38
+ return cast(R, ret)
39
+
40
+ return wrapper # type: ignore
41
+
42
+
43
+ class Raise[ExcT: Exception](Effect):
44
+ @operation
45
+ def error[_ExcT: Exception](err: _ExcT) -> Affects[None]: ...
46
+
47
+
48
+ class Async(Effect):
49
+ @operation
50
+ def wait[T](coro: Awaitable[T]) -> Affects[T, Raise[Exception]]: ...
51
+
52
+
53
+ type Affects[ReturnT, Effects = None] = Annotated[
54
+ Generator[Perform, Any, ReturnT], Effects
55
+ ]
@@ -0,0 +1,85 @@
1
+ from collections.abc import Callable, Generator
2
+ from dataclasses import dataclass
3
+ from typing import Any, Concatenate
4
+
5
+ from affective import Raise
6
+ from affective.core.effects import Perform
7
+ from affective.core.continuation import Continuation
8
+
9
+
10
+ @dataclass
11
+ class OperationHandlerCollection:
12
+ handlers: dict[Any, Callable[
13
+ ...,
14
+ Generator[Perform, Any, Any]
15
+ ]]
16
+
17
+ def __add__(
18
+ self, other: Any
19
+ ) -> "OperationHandlerCollection":
20
+ match other:
21
+ case OperationHandler(op, func):
22
+ return OperationHandlerCollection(self.handlers | {op: func})
23
+ case OperationHandlerCollection(handlers):
24
+ return OperationHandlerCollection(handlers | self.handlers)
25
+ case _:
26
+ return NotImplemented
27
+
28
+ def __radd__(self, other: Any) -> "OperationHandlerCollection":
29
+ return self.__add__(other)
30
+
31
+
32
+ @dataclass
33
+ class OperationHandler[**P = ..., R = Any]:
34
+ operation: Any
35
+ func: Callable[
36
+ Concatenate[Continuation[[R]], P],
37
+ Generator[Perform, Any, R]
38
+ ]
39
+
40
+ def __add__(self, other: Any) -> "OperationHandlerCollection":
41
+ match other:
42
+ case OperationHandler(op, func):
43
+ return OperationHandlerCollection(
44
+ {op: func, self.operation: self.func}
45
+ )
46
+ case _:
47
+ return NotImplemented
48
+
49
+
50
+ def handler[**P, R](
51
+ eff_op: Callable[P, Generator[Perform, Any, R]]
52
+ ) -> Callable[
53
+ [
54
+ Callable[
55
+ Concatenate[Continuation[[R]], P],
56
+ Generator[Perform, Any, R]
57
+ ]
58
+ ],
59
+ OperationHandler
60
+ ]:
61
+ def wrapper(
62
+ function: Callable[
63
+ Concatenate[Continuation[[R]], P],
64
+ Generator[Perform, Any, R]
65
+ ]
66
+ ) -> OperationHandler:
67
+ return OperationHandler(eff_op, function)
68
+
69
+ return wrapper
70
+
71
+
72
+ def catch[
73
+ R,
74
+ ThenContT: Continuation[...]
75
+ ](
76
+ on_catch: Continuation[[ThenContT, Exception]]
77
+ ) -> OperationHandler:
78
+ @handler(Raise.error) # type: ignore
79
+ def _handler(
80
+ cont: ThenContT, exc: Exception
81
+ ) -> Generator[Perform, R, R]:
82
+ ret: R = yield from on_catch(cont, exc)
83
+ return ret
84
+
85
+ return _handler
affective/core/loop.py ADDED
@@ -0,0 +1,97 @@
1
+ from typing import Any
2
+
3
+ from affective.core.effects import Async, Raise, Perform
4
+ from affective.core.handlers import (
5
+ OperationHandlerCollection,
6
+ OperationHandler,
7
+ )
8
+ from affective.core.continuation import RunningContinuation
9
+
10
+
11
+ class UnhandledEffect(Exception):
12
+ def __init__(self, effect: Perform):
13
+ super().__init__(
14
+ f"Effect handler for {effect.effect_type.__qualname__} not found"
15
+ )
16
+ self.effect = effect
17
+
18
+
19
+ def handle(
20
+ ctx: OperationHandlerCollection | OperationHandler,
21
+ cont: RunningContinuation,
22
+ effect: Perform | None = None,
23
+ ) -> Any:
24
+ if isinstance(ctx, OperationHandler):
25
+ ctx = OperationHandlerCollection({ctx.operation: ctx.func})
26
+ if effect is None:
27
+ try:
28
+ effect = next(cont)
29
+ except StopIteration as stop:
30
+ return stop.value
31
+ while True:
32
+ if not isinstance(effect, Perform):
33
+ raise TypeError(f"Unknown yield: {effect}")
34
+ if effect.effect_type in ctx.handlers:
35
+ def after(effect_result: Any) -> Any:
36
+ try:
37
+ eff = cont.send(effect_result)
38
+ except StopIteration as stop:
39
+ return stop.value
40
+ return_value = yield from handle(ctx, cont, eff)
41
+ return return_value
42
+
43
+ ret = yield from handle(
44
+ ctx, ctx.handlers[effect.effect_type](
45
+ after, *effect.effect_args, **effect.effect_kwargs
46
+ )
47
+ )
48
+ return ret
49
+ else:
50
+ effect_result = yield effect
51
+ try:
52
+ effect = cont.send(effect_result)
53
+ except StopIteration as stop:
54
+ return stop.value
55
+ return_value = yield from handle(ctx, cont, effect)
56
+ return return_value
57
+
58
+
59
+ def run(cont: RunningContinuation) -> Any:
60
+ try:
61
+ effect = next(cont)
62
+ while True:
63
+ if not isinstance(effect, Perform):
64
+ raise TypeError(f"Unknown yield: {effect}")
65
+ if effect.effect_type == Raise.error:
66
+ raise effect.effect_args[0]
67
+ else:
68
+ raise UnhandledEffect(effect)
69
+ except StopIteration as stop:
70
+ return stop.value
71
+
72
+
73
+ async def arun(cont: RunningContinuation) -> Any:
74
+ try:
75
+ effect = next(cont)
76
+ while True:
77
+ if not isinstance(effect, Perform):
78
+ raise TypeError(f"Unknown yield: {effect}")
79
+ if effect.effect_type == Raise.error:
80
+ raise effect.effect_args[0]
81
+ elif effect.effect_type == Async.wait:
82
+ try:
83
+ result = await effect.effect_args[0]
84
+ except Exception as exc:
85
+ try:
86
+ effect = cont.send(Perform(Raise.error, [exc], {}))
87
+ except StopIteration as stop:
88
+ return stop.value
89
+ else:
90
+ try:
91
+ effect = cont.send(result)
92
+ except StopIteration as stop:
93
+ return stop
94
+ else:
95
+ raise UnhandledEffect(effect)
96
+ except StopIteration as stop:
97
+ return stop.value
@@ -0,0 +1,9 @@
1
+ from mypy.plugin import Plugin
2
+
3
+ from affective.mypy_ext.plugin import AffectivePlugin
4
+
5
+
6
+ def plugin(version: str) -> type[Plugin]:
7
+ if not version.startswith("2."):
8
+ raise ValueError("Cannot run Affective with mypy < 2.0.0")
9
+ return AffectivePlugin
@@ -0,0 +1,19 @@
1
+ from typing import Callable, Optional
2
+
3
+ from mypy.plugin import Plugin, ClassDefContext
4
+ from mypy.nodes import Decorator
5
+
6
+
7
+ def abstractise_effects(ctx: ClassDefContext) -> None:
8
+ for stmt in ctx.cls.defs.body:
9
+ if isinstance(stmt, Decorator):
10
+ stmt.func.abstract_status = 1
11
+
12
+
13
+ class AffectivePlugin(Plugin):
14
+ def get_base_class_hook(self, fullname: str) -> Optional[
15
+ Callable[[ClassDefContext], None]
16
+ ]:
17
+ if fullname == "affective.core.effects.Effect":
18
+ return abstractise_effects
19
+ return None
affective/py.typed ADDED
File without changes
File without changes
affective/std/files.py ADDED
@@ -0,0 +1,48 @@
1
+ from typing import Any
2
+
3
+ from affective import Effect, Affects, Raise, operation, Continuation, handler
4
+
5
+
6
+ class Files(Effect):
7
+ @operation
8
+ def write(path: str, contents: bytes) -> Affects[
9
+ None, Raise[PermissionError]
10
+ ]: ...
11
+
12
+ @operation
13
+ def read(path: str) -> Affects[
14
+ bytes, Raise[PermissionError] | Raise[FileNotFoundError]
15
+ ]: ...
16
+
17
+
18
+ @handler(Files.write)
19
+ def _handle_default_write(
20
+ then: Continuation[[None]],
21
+ path: str,
22
+ contents: bytes
23
+ ) -> Affects[Any]:
24
+ try:
25
+ with open(path, "wb") as f:
26
+ f.write(contents)
27
+ except PermissionError as exc:
28
+ yield from Raise.error(exc)
29
+ ret = yield from then(None)
30
+ return ret
31
+
32
+
33
+ @handler(Files.read)
34
+ def _handle_default_read(
35
+ then: Continuation[[bytes]],
36
+ path: str,
37
+ ) -> Affects[Any]:
38
+ try:
39
+ with open(path, "rb") as f:
40
+ contents = f.read()
41
+ except PermissionError as exc:
42
+ yield from Raise.error(exc)
43
+ else:
44
+ ret = yield from then(contents)
45
+ return ret
46
+
47
+
48
+ default_files_handler = _handle_default_read + _handle_default_write
affective/std/http.py ADDED
@@ -0,0 +1,75 @@
1
+ from dataclasses import dataclass
2
+ from logging import Handler
3
+ from typing import Any
4
+
5
+ try:
6
+ from aiohttp.web_response import Response
7
+ from aiohttp import ClientSession, ClientTimeout
8
+ except ImportError as exc:
9
+ raise ImportError(
10
+ "Cannot use Http effects without [aiohttp] extra"
11
+ ) from exc
12
+
13
+ from affective import Async, Effect, handler, operation, Continuation, Affects
14
+
15
+
16
+ @dataclass
17
+ class HttpResponse:
18
+ status_code: int
19
+ data: bytes
20
+ headers: dict[str, str]
21
+
22
+
23
+ class Http(Effect):
24
+ @operation
25
+ def request(
26
+ method: str,
27
+ url: str,
28
+ params: str | dict[str, Any] | None = None,
29
+ data: Any | None = None,
30
+ headers: dict[str, str] | None = None,
31
+ timeout_s: float | None = None,
32
+ ) -> Affects[HttpResponse]:
33
+ ...
34
+
35
+
36
+ @handler(Http.request)
37
+ def _async_request_handler(
38
+ then: Continuation[[HttpResponse]],
39
+ method: str,
40
+ url: str,
41
+ params: str | dict[str, Any] | None = None,
42
+ data: Any | None = None,
43
+ headers: dict[str, str] | None = None,
44
+ timeout_s: float | None = None,
45
+ ) -> Affects[Any, Async]:
46
+ async def _do() -> HttpResponse:
47
+ async with (
48
+ ClientSession() as session,
49
+ session.request(
50
+ method=method,
51
+ url=url,
52
+ params=params,
53
+ data=data,
54
+ headers=headers,
55
+ timeout=ClientTimeout(timeout_s)
56
+ if timeout_s is not None else None,
57
+ raise_for_status=False,
58
+ ) as response
59
+ ):
60
+ response_data = await response.read()
61
+ return HttpResponse(
62
+ status_code=response.status,
63
+ data=response_data,
64
+ headers={
65
+ header: value
66
+ for header, value in response.headers.items()
67
+ },
68
+ )
69
+
70
+ result = yield from Async.wait(_do())
71
+ ret = yield from then(result)
72
+ return ret
73
+
74
+
75
+ async_http_handler = _async_request_handler
affective/std/stdio.py ADDED
@@ -0,0 +1,34 @@
1
+ import sys
2
+ from typing import Any
3
+
4
+ from affective import operation, handler, Effect, Affects
5
+ from affective.core.continuation import Continuation, RunningContinuation
6
+
7
+
8
+ class Console(Effect):
9
+ @operation
10
+ def read() -> Affects[str]: ...
11
+
12
+ @operation
13
+ def write(text: str) -> Affects[None]: ...
14
+
15
+
16
+ @handler(Console.write)
17
+ def default_stdout_writer(
18
+ then: Continuation[[None]], text: str
19
+ ) -> Affects[Any]:
20
+ sys.stdout.write(text)
21
+ sys.stdout.flush()
22
+ ret = yield from then(None)
23
+ return ret
24
+
25
+
26
+ @handler(Console.read)
27
+ def default_stdin_reader(
28
+ then: Continuation[[str]]
29
+ ) -> Affects[Any]:
30
+ ret = yield from then(sys.stdin.readline().removesuffix("\n"))
31
+ return ret
32
+
33
+
34
+ default_stdio_handler = default_stdout_writer + default_stdin_reader
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: affective-py
3
+ Version: 0.1.0
4
+ Summary: WIP AE in Python
5
+ Requires-Python: >=3.14
6
+ Provides-Extra: aiohttp
7
+ Requires-Dist: aiohttp>=3.13.5; extra == "aiohttp"
@@ -0,0 +1,17 @@
1
+ affective/__init__.py,sha256=N9Qj8r6viO9t9vWGv3qU1PGQ_-3AAmUmY_jLeGd9jZQ,700
2
+ affective/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ affective/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ affective/core/continuation.py,sha256=YhA7xDWOLvA3jyrSVye3D5yA9OMlHo56krI3bpISJO8,321
5
+ affective/core/effects.py,sha256=twF1ow-asfLpmWu57k89Y76fAE8LcJkLrlExFAAXfPA,1358
6
+ affective/core/handlers.py,sha256=re1o3P7hvMOxTSetjbsLspH05KqlVXmQn4xTGoThzyo,2316
7
+ affective/core/loop.py,sha256=m2LUMETFlx7vUHSMBGKUuzjK_qNt2OPBS87WNtyFUns,3337
8
+ affective/mypy_ext/__init__.py,sha256=SEGepM-7TBVSvsXbWOzO4VZD55EJ5d5C4hiycCe60I4,270
9
+ affective/mypy_ext/plugin.py,sha256=bT-cf3BXzu2CCBRAL6HSYdUREt7nSKN4G0Ma7S-EJhM,572
10
+ affective/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ affective/std/files.py,sha256=LyGJ0l00JyK5oAyo47DPf6xP3ghyXYIlInLj1idWI5A,1153
12
+ affective/std/http.py,sha256=bDKQX9Ec7YQ00qn-ArPoJv70Cs0abeBTOjdf2rz5kpA,2094
13
+ affective/std/stdio.py,sha256=OC2lyhRRKp7kIq65PPzpVkFaTX_HzK5yyaG7KE5M6Ms,804
14
+ affective_py-0.1.0.dist-info/METADATA,sha256=S5bTU31VlU2gB6FcbWYPqqyz_IT5evEDPO29GJOzX3I,188
15
+ affective_py-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
16
+ affective_py-0.1.0.dist-info/top_level.txt,sha256=3PaZbbPjBoUDa-uZoYamcf8haaV2WV1Kh_pVS8YcpwY,10
17
+ affective_py-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ affective