Coverage for greyhorse / river / operator.py: 100%
30 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-18 11:33 +0300
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-18 11:33 +0300
1"""Operator decorator for marking methods as orchestration primitives.
3The ``@operator`` decorator turns a class method into a lazy invocation builder.
4Calling the decorated method returns an ``OperatorInvocation`` instead of executing
5the method body directly. The invocation can then be configured with ``bind()`` /
6``bind_resource()``, and materialized into a lazy ``Resource`` via ``prepare(resolver)``.
8Note that ``prepare()`` itself is lazy — it builds a ``Resource[IO, T]`` program
9without executing anything. The method body runs only when the Resource is consumed
10via ``.run()`` or ``with .open() as value:``.
12Schematic usage::
14 ops = DbOps()
15 inv = ops.publish_pool() # OperatorInvocation (no execution)
16 program = inv.prepare(resolver) # Resource[IO, T] (still no execution)
17 result = program.run() # NOW the method body executes
18"""
20from __future__ import annotations
22from collections.abc import Callable
23from types import MethodType
24from typing import Any
25from weakref import WeakKeyDictionary
27from greyhorse.factory import into_factory
29from .invocation import OperatorInvocation
32def operator(method: Callable) -> _OperatorDescriptor:
33 """Mark a method as an operator — a lazy orchestration primitive.
35 The decorated method becomes a descriptor. Accessing it on an instance
36 returns a callable that produces ``OperatorInvocation`` objects.
38 Uses ``MethodType`` for self-binding and ``WeakKeyDictionary`` to cache
39 one ``Factory`` per owner instance (no leaks on GC).
41 Args:
42 method: The unbound method to decorate.
44 Returns:
45 A descriptor that produces ``OperatorInvocation`` builders on access.
47 Examples:
48 Schematic (requires a Component with fragments for full execution)::
50 class MyOps:
51 @operator
52 def compute(self, x: int) -> int:
53 return x * 2
55 ops = MyOps()
56 inv = ops.compute(x=5) # OperatorInvocation
57 program = inv.prepare(comp) # Resource[IO, int] — lazy
58 result = program.run() # executes the method body
59 """
60 return _OperatorDescriptor(method)
63class _OperatorDescriptor:
64 """Descriptor that binds ``@operator``-decorated methods to owner instances."""
66 __slots__ = ('_factory_cache', '_method')
68 def __init__(self, method: Callable) -> None:
69 self._method = method
70 self._factory_cache: WeakKeyDictionary = WeakKeyDictionary()
72 def __set_name__(self, owner: type, name: str) -> None:
73 pass
75 def __get__(self, owner_self: Any, owner_cls: type | None = None) -> Any:
76 if owner_self is None:
77 return self
79 try:
80 if owner_self not in self._factory_cache:
81 bound = MethodType(self._method, owner_self)
82 self._factory_cache[owner_self] = into_factory(bound)
83 factory = self._factory_cache[owner_self]
84 except TypeError:
85 bound = MethodType(self._method, owner_self)
86 factory = into_factory(bound)
88 def invocation_builder(*args: Any, **kwargs: Any) -> OperatorInvocation:
89 return OperatorInvocation(factory, *args, **kwargs)
91 return invocation_builder