Skip to content

Commit 719efd3

Browse files
author
remimd
committed
refactor: entrypoint.py
1 parent 8792bb3 commit 719efd3

File tree

6 files changed

+264
-228
lines changed

6 files changed

+264
-228
lines changed

injection/_core/asfunction.py

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from collections.abc import Callable
2-
from functools import wraps
1+
from collections.abc import Awaitable, Callable
2+
from functools import update_wrapper
33
from inspect import iscoroutinefunction
44
from typing import Any, Protocol
55

@@ -21,33 +21,45 @@ def asfunction[**P, T](
2121
module: Module | None = None,
2222
threadsafe: bool | None = None,
2323
) -> Any:
24-
module = module or mod()
25-
2624
def decorator(wp: AsFunctionWrappedType[P, T]) -> Callable[P, T]:
2725
fake_method = wp.__call__.__get__(NotImplemented, wp)
28-
factory: Caller[..., Callable[P, T]] = module.make_injected_function(
29-
wp,
30-
threadsafe=threadsafe,
31-
).__injection_metadata__
26+
factory: Caller[..., Callable[P, T]] = (
27+
(module or mod())
28+
.make_injected_function(
29+
wp,
30+
threadsafe=threadsafe,
31+
)
32+
.__injection_metadata__
33+
)
3234

33-
wrapper: Callable[P, T]
35+
wrapper: Callable[P, T] = (
36+
_wrap_async(factory) # type: ignore[arg-type, assignment]
37+
if iscoroutinefunction(fake_method)
38+
else _wrap_sync(factory)
39+
)
40+
wrapper = update_wrapper(wrapper, fake_method)
3441

35-
if iscoroutinefunction(fake_method):
42+
for attribute in ("__name__", "__qualname__"):
43+
setattr(wrapper, attribute, getattr(wp, attribute))
3644

37-
@wraps(fake_method)
38-
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> Any:
39-
self = await factory.acall()
40-
return await self(*args, **kwargs) # type: ignore[misc]
45+
return wrapper
4146

42-
else:
47+
return decorator(wrapped) if wrapped else decorator
4348

44-
@wraps(fake_method)
45-
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
46-
self = factory.call()
47-
return self(*args, **kwargs)
4849

49-
wrapper.__name__ = wp.__name__
50-
wrapper.__qualname__ = wp.__qualname__
51-
return wrapper
50+
def _wrap_async[**P, T](
51+
factory: Caller[..., Callable[P, Awaitable[T]]],
52+
) -> Callable[P, Awaitable[T]]:
53+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
54+
self = await factory.acall()
55+
return await self(*args, **kwargs)
5256

53-
return decorator(wrapped) if wrapped else decorator
57+
return wrapper
58+
59+
60+
def _wrap_sync[**P, T](factory: Caller[..., Callable[P, T]]) -> Callable[P, T]:
61+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
62+
self = factory.call()
63+
return self(*args, **kwargs)
64+
65+
return wrapper

injection/entrypoint.py

Lines changed: 129 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,105 @@
11
from __future__ import annotations
22

33
import asyncio
4-
from collections.abc import AsyncIterator, Awaitable, Callable, Coroutine, Iterator
5-
from contextlib import asynccontextmanager, contextmanager
4+
from abc import ABC, abstractmethod
5+
from collections.abc import Awaitable, Callable, Iterator, Sequence
6+
from contextlib import contextmanager
67
from dataclasses import dataclass, field
78
from functools import wraps
89
from types import ModuleType as PythonModule
9-
from typing import TYPE_CHECKING, Any, Concatenate, Protocol, Self, final, overload
10+
from typing import (
11+
TYPE_CHECKING,
12+
Any,
13+
Concatenate,
14+
Coroutine,
15+
Protocol,
16+
Self,
17+
overload,
18+
runtime_checkable,
19+
)
1020

1121
from injection import Module
1222
from injection.loaders import ProfileLoader, PythonModuleLoader
1323

1424
__all__ = ("AsyncEntrypoint", "Entrypoint", "entrypointmaker")
1525

26+
type Entrypoint[**P, T] = EntrypointBuilder[P, Any, T]
27+
type AsyncEntrypoint[**P, T] = Entrypoint[P, Awaitable[T]]
1628

17-
type AsyncEntrypoint[**P, T] = Entrypoint[P, Coroutine[Any, Any, T]]
1829
type EntrypointSetupMethod[**P, **EPP, T1, T2] = Callable[
1930
Concatenate[Entrypoint[EPP, T1], P],
2031
Entrypoint[EPP, T2],
2132
]
2233

2334

35+
class Rule[**P, T1, T2](ABC):
36+
__slots__ = ()
37+
38+
@abstractmethod
39+
def apply(self, wrapped: Callable[P, T1]) -> Callable[P, T2]:
40+
raise NotImplementedError
41+
42+
43+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
44+
class _AsyncToSyncRule[**P, T](Rule[P, Awaitable[T], T]):
45+
run: Callable[[Awaitable[T]], T]
46+
47+
def apply(self, wrapped: Callable[P, Awaitable[T]]) -> Callable[P, T]:
48+
@wraps(wrapped)
49+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
50+
return self.run(wrapped(*args, **kwargs))
51+
52+
return wrapper
53+
54+
55+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
56+
class _DecorateRule[**P, T1, T2](Rule[P, T1, T2]):
57+
decorator: Callable[[Callable[P, T1]], Callable[P, T2]]
58+
59+
def apply(self, wrapped: Callable[P, T1]) -> Callable[P, T2]:
60+
return self.decorator(wrapped)
61+
62+
63+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
64+
class _InjectRule[**P, T](Rule[P, T, T]):
65+
module: Module
66+
67+
def apply(self, wrapped: Callable[P, T]) -> Callable[P, T]:
68+
return self.module.make_injected_function(wrapped)
69+
70+
71+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
72+
class _LoadModulesRule[**P, T](Rule[P, T, T]):
73+
loader: PythonModuleLoader
74+
packages: Sequence[PythonModule | str]
75+
76+
def apply(self, wrapped: Callable[P, T]) -> Callable[P, T]:
77+
return self.__decorator()(wrapped)
78+
79+
@contextmanager
80+
def __decorator(self) -> Iterator[None]:
81+
self.loader.load(*self.packages)
82+
yield
83+
84+
85+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
86+
class _LoadProfileRule[**P, T](Rule[P, T, T]):
87+
loader: ProfileLoader
88+
profile_name: str
89+
90+
def apply(self, wrapped: Callable[P, T]) -> Callable[P, T]:
91+
return self.__decorator()(wrapped)
92+
93+
@contextmanager
94+
def __decorator(self) -> Iterator[None]:
95+
with self.loader.load(self.profile_name):
96+
yield
97+
98+
99+
@runtime_checkable
24100
class _EntrypointDecorator[**P, T1, T2](Protocol):
101+
__slots__ = ()
102+
25103
if TYPE_CHECKING: # pragma: no cover
26104

27105
@overload
@@ -42,18 +120,21 @@ def __call__(
42120
autocall: bool = ...,
43121
) -> Callable[[Callable[P, T1]], Callable[P, T2]]: ...
44122

123+
@abstractmethod
45124
def __call__(
46125
self,
47126
wrapped: Callable[P, T1] | None = ...,
48127
/,
49128
*,
50129
autocall: bool = ...,
51-
) -> Any: ...
130+
) -> Any:
131+
raise NotImplementedError
52132

53133

54134
# SMP = Setup Method Parameters
55135
# EPP = EntryPoint Parameters
56136

137+
57138
if TYPE_CHECKING: # pragma: no cover
58139

59140
@overload
@@ -85,114 +166,74 @@ def entrypointmaker[**SMP, **EPP, T1, T2](
85166
def decorator(
86167
wp: EntrypointSetupMethod[SMP, EPP, T1, T2],
87168
) -> _EntrypointDecorator[EPP, T1, T2]:
88-
return Entrypoint._make_decorator(wp, profile_loader)
169+
pl = (profile_loader or ProfileLoader()).init()
170+
setup_method = pl.module.make_injected_function(wp)
171+
return setup_method(EntrypointBuilder(pl)) # type: ignore[call-arg]
89172

90173
return decorator(wrapped) if wrapped else decorator
91174

92175

93-
@final
94176
@dataclass(repr=False, eq=False, frozen=True, slots=True)
95-
class Entrypoint[**P, T]:
96-
function: Callable[P, T]
177+
class EntrypointBuilder[**P, T1, T2](_EntrypointDecorator[P, T1, T2]):
97178
profile_loader: ProfileLoader = field(default_factory=ProfileLoader)
179+
__rules: list[Rule[P, Any, Any]] = field(default_factory=list, init=False)
180+
181+
def __call__(
182+
self,
183+
wrapped: Callable[P, T1] | None = None,
184+
/,
185+
*,
186+
autocall: bool = False,
187+
) -> Any:
188+
def decorator(wp: Callable[P, T1]) -> Callable[P, T2]:
189+
wrapper = self._apply(wp)
190+
191+
if autocall:
192+
wrapper() # type: ignore[call-arg]
98193

99-
def __call__(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
100-
return self.function(*args, **kwargs)
194+
return wrapper
101195

102-
@property
103-
def __module(self) -> Module:
104-
return self.profile_loader.module
196+
return decorator(wrapped) if wrapped else decorator
105197

106198
def async_to_sync[_T](
107-
self: AsyncEntrypoint[P, _T],
199+
self: EntrypointBuilder[P, T1, Awaitable[_T]],
108200
run: Callable[[Coroutine[Any, Any, _T]], _T] = asyncio.run,
109201
/,
110-
) -> Entrypoint[P, _T]:
111-
function = self.function
202+
) -> EntrypointBuilder[P, T1, _T]:
203+
return self._add_rule(_AsyncToSyncRule(run)) # type: ignore[arg-type]
112204

113-
@wraps(function)
114-
def wrapper(*args: P.args, **kwargs: P.kwargs) -> _T:
115-
return run(function(*args, **kwargs))
116-
117-
return self.__recreate(wrapper)
118-
119-
def decorate(
205+
def decorate[_T](
120206
self,
121-
decorator: Callable[[Callable[P, T]], Callable[P, T]],
207+
decorator: Callable[[Callable[P, T2]], Callable[P, _T]],
122208
/,
123-
) -> Self:
124-
return self.__recreate(decorator(self.function))
209+
) -> EntrypointBuilder[P, T1, _T]:
210+
return self._add_rule(_DecorateRule(decorator))
125211

126212
def inject(self) -> Self:
127-
return self.decorate(self.__module.make_injected_function)
213+
self._add_rule(_InjectRule(self.profile_loader.module))
214+
return self
128215

129216
def load_modules(
130217
self,
131-
/,
132218
loader: PythonModuleLoader,
133219
*packages: PythonModule | str,
134220
) -> Self:
135-
return self.setup(lambda: loader.load(*packages))
221+
self._add_rule(_LoadModulesRule(loader, packages))
222+
return self
136223

137224
def load_profile(self, name: str, /) -> Self:
138-
@contextmanager
139-
def decorator(loader: ProfileLoader) -> Iterator[None]:
140-
with loader.load(name):
141-
yield
142-
143-
return self.decorate(decorator(self.profile_loader))
144-
145-
def setup(self, function: Callable[..., Any], /) -> Self:
146-
@contextmanager
147-
def decorator() -> Iterator[Any]:
148-
yield function()
225+
self._add_rule(_LoadProfileRule(self.profile_loader, name))
226+
return self
149227

150-
return self.decorate(decorator())
151-
152-
def async_setup[_T](
153-
self: AsyncEntrypoint[P, _T],
154-
function: Callable[..., Awaitable[Any]],
155-
/,
156-
) -> AsyncEntrypoint[P, _T]:
157-
@asynccontextmanager
158-
async def decorator() -> AsyncIterator[Any]:
159-
yield await function()
160-
161-
return self.decorate(decorator())
162-
163-
def __recreate[**_P, _T](
164-
self: Entrypoint[Any, Any],
165-
function: Callable[_P, _T],
166-
/,
167-
) -> Entrypoint[_P, _T]:
168-
return type(self)(function, self.profile_loader)
169-
170-
@classmethod
171-
def _make_decorator[**_P, _T](
172-
cls,
173-
setup_method: EntrypointSetupMethod[_P, P, T, _T],
174-
/,
175-
profile_loader: ProfileLoader | None = None,
176-
) -> _EntrypointDecorator[P, T, _T]:
177-
profile_loader = profile_loader or ProfileLoader()
178-
setup_method = profile_loader.module.make_injected_function(setup_method)
179-
180-
def entrypoint_decorator(
181-
wrapped: Callable[P, T] | None = None,
182-
/,
183-
*,
184-
autocall: bool = False,
185-
) -> Any:
186-
def decorator(wp: Callable[P, T]) -> Callable[P, _T]:
187-
profile_loader.init()
188-
self = cls(wp, profile_loader)
189-
wrapper = setup_method(self).function # type: ignore[call-arg]
190-
191-
if autocall:
192-
wrapper() # type: ignore[call-arg]
193-
194-
return wrapper
228+
def _add_rule[_T](
229+
self,
230+
rule: Rule[P, T2, _T],
231+
) -> EntrypointBuilder[P, T1, _T]:
232+
self.__rules.append(rule)
233+
return self # type: ignore[return-value]
195234

196-
return decorator(wrapped) if wrapped else decorator
235+
def _apply(self, function: Callable[P, T1], /) -> Callable[P, T2]:
236+
for rule in self.__rules:
237+
function = rule.apply(function)
197238

198-
return entrypoint_decorator
239+
return function # type: ignore[return-value]

0 commit comments

Comments
 (0)