Skip to content

Commit 8465866

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

File tree

6 files changed

+263
-228
lines changed

6 files changed

+263
-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: 128 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,104 @@
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, Coroutine, 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+
Protocol,
15+
Self,
16+
overload,
17+
runtime_checkable,
18+
)
1019

1120
from injection import Module
1221
from injection.loaders import ProfileLoader, PythonModuleLoader
1322

1423
__all__ = ("AsyncEntrypoint", "Entrypoint", "entrypointmaker")
1524

25+
type Entrypoint[**P, T] = EntrypointBuilder[P, Any, T]
26+
type AsyncEntrypoint[**P, T] = Entrypoint[P, Awaitable[T]]
1627

17-
type AsyncEntrypoint[**P, T] = Entrypoint[P, Coroutine[Any, Any, T]]
1828
type EntrypointSetupMethod[**P, **EPP, T1, T2] = Callable[
1929
Concatenate[Entrypoint[EPP, T1], P],
2030
Entrypoint[EPP, T2],
2131
]
2232

2333

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

27104
@overload
@@ -42,18 +119,21 @@ def __call__(
42119
autocall: bool = ...,
43120
) -> Callable[[Callable[P, T1]], Callable[P, T2]]: ...
44121

122+
@abstractmethod
45123
def __call__(
46124
self,
47125
wrapped: Callable[P, T1] | None = ...,
48126
/,
49127
*,
50128
autocall: bool = ...,
51-
) -> Any: ...
129+
) -> Any:
130+
raise NotImplementedError
52131

53132

54133
# SMP = Setup Method Parameters
55134
# EPP = EntryPoint Parameters
56135

136+
57137
if TYPE_CHECKING: # pragma: no cover
58138

59139
@overload
@@ -85,114 +165,74 @@ def entrypointmaker[**SMP, **EPP, T1, T2](
85165
def decorator(
86166
wp: EntrypointSetupMethod[SMP, EPP, T1, T2],
87167
) -> _EntrypointDecorator[EPP, T1, T2]:
88-
return Entrypoint._make_decorator(wp, profile_loader)
168+
pl = (profile_loader or ProfileLoader()).init()
169+
setup_method = pl.module.make_injected_function(wp)
170+
return setup_method(EntrypointBuilder(pl)) # type: ignore[call-arg]
89171

90172
return decorator(wrapped) if wrapped else decorator
91173

92174

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

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

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

106197
def async_to_sync[_T](
107-
self: AsyncEntrypoint[P, _T],
198+
self: EntrypointBuilder[P, T1, Awaitable[_T]],
108199
run: Callable[[Coroutine[Any, Any, _T]], _T] = asyncio.run,
109200
/,
110-
) -> Entrypoint[P, _T]:
111-
function = self.function
201+
) -> EntrypointBuilder[P, T1, _T]:
202+
return self._add_rule(_AsyncToSyncRule(run)) # type: ignore[arg-type]
112203

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(
204+
def decorate[_T](
120205
self,
121-
decorator: Callable[[Callable[P, T]], Callable[P, T]],
206+
decorator: Callable[[Callable[P, T2]], Callable[P, _T]],
122207
/,
123-
) -> Self:
124-
return self.__recreate(decorator(self.function))
208+
) -> EntrypointBuilder[P, T1, _T]:
209+
return self._add_rule(_DecorateRule(decorator))
125210

126211
def inject(self) -> Self:
127-
return self.decorate(self.__module.make_injected_function)
212+
self._add_rule(_InjectRule(self.profile_loader.module))
213+
return self
128214

129215
def load_modules(
130216
self,
131-
/,
132217
loader: PythonModuleLoader,
133218
*packages: PythonModule | str,
134219
) -> Self:
135-
return self.setup(lambda: loader.load(*packages))
220+
self._add_rule(_LoadModulesRule(loader, packages))
221+
return self
136222

137223
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()
224+
self._add_rule(_LoadProfileRule(self.profile_loader, name))
225+
return self
149226

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
227+
def _add_rule[_T](
228+
self,
229+
rule: Rule[P, T2, _T],
230+
) -> EntrypointBuilder[P, T1, _T]:
231+
self.__rules.append(rule)
232+
return self # type: ignore[return-value]
195233

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

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

0 commit comments

Comments
 (0)