telegrinder 0.3.2__py3-none-any.whl → 0.3.3.post1__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.

Potentially problematic release.


This version of telegrinder might be problematic. Click here for more details.

Files changed (44) hide show
  1. telegrinder/bot/cute_types/__init__.py +6 -6
  2. telegrinder/bot/cute_types/callback_query.py +5 -2
  3. telegrinder/bot/cute_types/inline_query.py +3 -13
  4. telegrinder/bot/cute_types/message.py +9 -3
  5. telegrinder/bot/cute_types/update.py +45 -11
  6. telegrinder/bot/cute_types/utils.py +5 -5
  7. telegrinder/bot/dispatch/context.py +6 -0
  8. telegrinder/bot/dispatch/handler/func.py +10 -3
  9. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +2 -2
  10. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +2 -2
  11. telegrinder/bot/dispatch/waiter_machine/machine.py +7 -8
  12. telegrinder/bot/dispatch/waiter_machine/short_state.py +4 -1
  13. telegrinder/bot/rules/abc.py +2 -27
  14. telegrinder/bot/rules/adapter/abc.py +1 -1
  15. telegrinder/bot/rules/adapter/event.py +22 -31
  16. telegrinder/bot/rules/adapter/node.py +1 -1
  17. telegrinder/bot/rules/adapter/raw_update.py +1 -1
  18. telegrinder/bot/rules/callback_data.py +1 -2
  19. telegrinder/bot/rules/chat_join.py +1 -3
  20. telegrinder/bot/rules/inline.py +1 -1
  21. telegrinder/bot/rules/is_from.py +19 -19
  22. telegrinder/bot/rules/message.py +2 -2
  23. telegrinder/model.py +51 -0
  24. telegrinder/msgspec_utils.py +2 -2
  25. telegrinder/node/__init__.py +24 -20
  26. telegrinder/node/attachment.py +5 -5
  27. telegrinder/node/base.py +30 -7
  28. telegrinder/node/callback_query.py +41 -2
  29. telegrinder/node/composer.py +6 -4
  30. telegrinder/node/event.py +3 -9
  31. telegrinder/node/text.py +22 -2
  32. telegrinder/tools/formatting/html.py +25 -25
  33. telegrinder/tools/i18n/__init__.py +5 -5
  34. telegrinder/tools/i18n/abc.py +2 -2
  35. telegrinder/tools/keyboard.py +5 -5
  36. telegrinder/tools/loop_wrapper/loop_wrapper.py +17 -9
  37. telegrinder/tools/magic.py +11 -11
  38. telegrinder/types/__init__.py +254 -254
  39. telegrinder/types/enums.py +29 -29
  40. telegrinder/types/objects.py +3723 -1703
  41. {telegrinder-0.3.2.dist-info → telegrinder-0.3.3.post1.dist-info}/METADATA +1 -1
  42. {telegrinder-0.3.2.dist-info → telegrinder-0.3.3.post1.dist-info}/RECORD +44 -44
  43. {telegrinder-0.3.2.dist-info → telegrinder-0.3.3.post1.dist-info}/LICENSE +0 -0
  44. {telegrinder-0.3.2.dist-info → telegrinder-0.3.3.post1.dist-info}/WHEEL +0 -0
telegrinder/model.py CHANGED
@@ -60,6 +60,57 @@ def get_params(params: dict[str, typing.Any]) -> dict[str, typing.Any]:
60
60
  return validated_params
61
61
 
62
62
 
63
+ class From(typing.Generic[T]):
64
+ def __new__(cls, _: T, /) -> typing.Any: ...
65
+
66
+
67
+ if typing.TYPE_CHECKING:
68
+
69
+ @typing.overload
70
+ def field(*, name: str | None = ...) -> typing.Any: ...
71
+
72
+ @typing.overload
73
+ def field(*, default: typing.Any, name: str | None = ...) -> typing.Any: ...
74
+
75
+ @typing.overload
76
+ def field(
77
+ *,
78
+ converter: typing.Callable[[typing.Any], typing.Any],
79
+ name: str | None = ...,
80
+ ) -> typing.Any: ...
81
+
82
+ @typing.overload
83
+ def field(
84
+ *,
85
+ default: typing.Any,
86
+ converter: typing.Callable[[typing.Any], typing.Any],
87
+ name: str | None = ...,
88
+ ) -> typing.Any: ...
89
+
90
+ @typing.overload
91
+ def field(
92
+ *,
93
+ default_factory: typing.Callable[[], typing.Any],
94
+ converter: typing.Callable[[typing.Any], typing.Any],
95
+ name: str | None = None,
96
+ ) -> typing.Any: ...
97
+
98
+ def field(
99
+ *,
100
+ default=...,
101
+ default_factory=...,
102
+ name=...,
103
+ converter=...,
104
+ ) -> typing.Any: ...
105
+ else:
106
+ from msgspec import field as _field
107
+
108
+ def field(**kwargs):
109
+ kwargs.pop("converter", None)
110
+ return _field(**kwargs)
111
+
112
+
113
+ @typing.dataclass_transform(field_specifiers=(field,))
63
114
  class Model(msgspec.Struct, **MODEL_CONFIG):
64
115
  @classmethod
65
116
  def from_data(cls, data: dict[str, typing.Any]) -> typing.Self:
@@ -235,7 +235,7 @@ class Decoder:
235
235
  )
236
236
  yield dec_obj
237
237
 
238
- def add_dec_hook(self, t: T): # type: ignore
238
+ def add_dec_hook(self, t: type[T]): # type: ignore
239
239
  def decorator(func: DecHook[T]) -> DecHook[T]:
240
240
  return self.dec_hooks.setdefault(get_origin(t), func) # type: ignore
241
241
 
@@ -346,7 +346,7 @@ class Encoder:
346
346
  enc_obj = msgspec.json.Encoder(enc_hook=self.enc_hook)
347
347
  yield enc_obj
348
348
 
349
- def add_dec_hook(self, t: type[T]):
349
+ def add_enc_hook(self, t: type[T]):
350
350
  def decorator(func: EncHook[T]) -> EncHook[T]:
351
351
  encode_hook = self.enc_hooks.setdefault(get_origin(t), func)
352
352
  return func if encode_hook is not func else encode_hook
@@ -1,23 +1,24 @@
1
- from .attachment import Attachment, Audio, Photo, Video
2
- from .base import ComposeError, DataNode, Node, ScalarNode, is_node
3
- from .callback_query import CallbackQueryNode
4
- from .command import CommandInfo
5
- from .composer import Composition, NodeCollection, NodeSession, compose_node, compose_nodes
6
- from .container import ContainerNode
7
- from .event import EventNode
8
- from .me import Me
9
- from .message import MessageNode
10
- from .polymorphic import Polymorphic, impl
11
- from .rule import RuleChain
12
- from .scope import GLOBAL, PER_CALL, PER_EVENT, NodeScope, global_node, per_call, per_event
13
- from .source import ChatSource, Source, UserSource
14
- from .text import Text, TextInteger
15
- from .tools import generate_node
16
- from .update import UpdateNode
17
-
18
- __all__ = (
1
+ from .attachment import Attachment, Audio, Photo, Video
2
+ from .base import ComposeError, DataNode, Name, Node, ScalarNode, is_node
3
+ from .callback_query import CallbackQueryData, CallbackQueryNode, Field
4
+ from .command import CommandInfo
5
+ from .composer import Composition, NodeCollection, NodeSession, compose_node, compose_nodes
6
+ from .container import ContainerNode
7
+ from .event import EventNode
8
+ from .me import Me
9
+ from .message import MessageNode
10
+ from .polymorphic import Polymorphic, impl
11
+ from .rule import RuleChain
12
+ from .scope import GLOBAL, PER_CALL, PER_EVENT, NodeScope, global_node, per_call, per_event
13
+ from .source import ChatSource, Source, UserSource
14
+ from .text import Text, TextInteger, TextLiteral
15
+ from .tools import generate_node
16
+ from .update import UpdateNode
17
+
18
+ __all__ = (
19
19
  "Attachment",
20
20
  "Audio",
21
+ "CallbackQueryData",
21
22
  "CallbackQueryNode",
22
23
  "ChatSource",
23
24
  "CommandInfo",
@@ -26,9 +27,11 @@ __all__ = (
26
27
  "ContainerNode",
27
28
  "DataNode",
28
29
  "EventNode",
30
+ "Field",
29
31
  "GLOBAL",
30
32
  "Me",
31
33
  "MessageNode",
34
+ "Name",
32
35
  "Node",
33
36
  "NodeCollection",
34
37
  "NodeScope",
@@ -42,6 +45,7 @@ __all__ = (
42
45
  "Source",
43
46
  "Text",
44
47
  "TextInteger",
48
+ "TextLiteral",
45
49
  "UpdateNode",
46
50
  "UserSource",
47
51
  "Video",
@@ -52,5 +56,5 @@ __all__ = (
52
56
  "impl",
53
57
  "is_node",
54
58
  "per_call",
55
- "per_event",
56
- )
59
+ "per_event",
60
+ )
@@ -83,10 +83,10 @@ class Poll(ScalarNode, telegrinder.types.Poll):
83
83
 
84
84
 
85
85
  __all__ = (
86
- "Attachment",
87
- "Audio",
88
- "Document",
89
- "Photo",
90
- "Poll",
86
+ "Attachment",
87
+ "Audio",
88
+ "Document",
89
+ "Photo",
90
+ "Poll",
91
91
  "Video",
92
92
  )
telegrinder/node/base.py CHANGED
@@ -1,14 +1,15 @@
1
1
  import abc
2
2
  import inspect
3
- import typing
4
3
  from types import AsyncGeneratorType
5
4
 
5
+ import typing_extensions as typing
6
+
6
7
  from telegrinder.node.scope import NodeScope
7
8
  from telegrinder.tools.magic import cache_magic_value, get_annotations
8
9
 
9
- ComposeResult: typing.TypeAlias = (
10
- typing.Awaitable[typing.Any] | typing.AsyncGenerator[typing.Any, None] | typing.Any
11
- )
10
+ T = typing.TypeVar("T", default=typing.Any)
11
+
12
+ ComposeResult: typing.TypeAlias = T | typing.Awaitable[T] | typing.AsyncGenerator[T, None]
12
13
 
13
14
 
14
15
  def is_node(maybe_node: typing.Any) -> typing.TypeGuard[type["Node"]]:
@@ -77,20 +78,35 @@ class Node(abc.ABC):
77
78
  return is_generator(cls.compose)
78
79
 
79
80
 
81
+ @typing.dataclass_transform(kw_only_default=True)
82
+ class FactoryNode(Node, abc.ABC):
83
+ node = "factory"
84
+
85
+ @classmethod
86
+ @abc.abstractmethod
87
+ def compose(cls, *args, **kwargs) -> ComposeResult:
88
+ pass
89
+
90
+ def __new__(cls, **context: typing.Any) -> typing.Self:
91
+ namespace = dict(**cls.__dict__)
92
+ namespace.pop("__new__", None)
93
+ return type(cls.__name__, (cls,), context | namespace) # type: ignore
94
+
95
+
96
+ @typing.dataclass_transform()
80
97
  class DataNode(Node, abc.ABC):
81
98
  node = "data"
82
99
 
83
- @typing.dataclass_transform()
84
100
  @classmethod
85
101
  @abc.abstractmethod
86
- async def compose(cls, *args, **kwargs) -> ComposeResult:
102
+ def compose(cls, *args, **kwargs) -> ComposeResult[typing.Self]:
87
103
  pass
88
104
 
89
105
 
90
106
  class ScalarNodeProto(Node, abc.ABC):
91
107
  @classmethod
92
108
  @abc.abstractmethod
93
- async def compose(cls, *args, **kwargs) -> ComposeResult:
109
+ def compose(cls, *args, **kwargs) -> ComposeResult:
94
110
  pass
95
111
 
96
112
 
@@ -131,9 +147,16 @@ else:
131
147
  pass
132
148
 
133
149
 
150
+ class Name(ScalarNode, str):
151
+ @classmethod
152
+ def compose(cls) -> str: ...
153
+
154
+
134
155
  __all__ = (
135
156
  "ComposeError",
157
+ "FactoryNode",
136
158
  "DataNode",
159
+ "Name",
137
160
  "Node",
138
161
  "SCALAR_NODE",
139
162
  "ScalarNode",
@@ -1,7 +1,14 @@
1
+ import typing
2
+
3
+ from fntypes.result import Error, Ok
4
+
1
5
  from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
2
- from telegrinder.node.base import ComposeError, ScalarNode
6
+ from telegrinder.msgspec_utils import msgspec_convert
7
+ from telegrinder.node.base import ComposeError, FactoryNode, Name, ScalarNode
3
8
  from telegrinder.node.update import UpdateNode
4
9
 
10
+ FieldType = typing.TypeVar("FieldType")
11
+
5
12
 
6
13
  class CallbackQueryNode(ScalarNode, CallbackQueryCute):
7
14
  @classmethod
@@ -11,4 +18,36 @@ class CallbackQueryNode(ScalarNode, CallbackQueryCute):
11
18
  return update.callback_query.unwrap()
12
19
 
13
20
 
14
- __all__ = ("CallbackQueryNode",)
21
+ class CallbackQueryData(ScalarNode, dict[str, typing.Any]):
22
+ @classmethod
23
+ def compose(cls, callback_query: CallbackQueryNode) -> dict[str, typing.Any]:
24
+ return callback_query.decode_callback_data().expect(
25
+ ComposeError("Cannot complete decode callback query data.")
26
+ )
27
+
28
+
29
+ class _Field(FactoryNode):
30
+ field_type: type[typing.Any]
31
+
32
+ def __class_getitem__(cls, field_type: type[typing.Any], /) -> typing.Self:
33
+ return cls(field_type=field_type)
34
+
35
+ @classmethod
36
+ def compose(cls, callback_query_data: CallbackQueryData, data_name: Name) -> typing.Any:
37
+ if data := callback_query_data.get(data_name):
38
+ match msgspec_convert(data, cls.field_type):
39
+ case Ok(value):
40
+ return value
41
+ case Error(err):
42
+ raise ComposeError(err)
43
+
44
+ raise ComposeError(f"Cannot find callback data with name {data_name!r}.")
45
+
46
+
47
+ if typing.TYPE_CHECKING:
48
+ Field = typing.Annotated[FieldType, ...]
49
+ else:
50
+ Field = _Field
51
+
52
+
53
+ __all__ = ("CallbackQueryData", "CallbackQueryNode", "Field")
@@ -11,6 +11,7 @@ from telegrinder.bot.dispatch.context import Context
11
11
  from telegrinder.modules import logger
12
12
  from telegrinder.node.base import (
13
13
  ComposeError,
14
+ Name,
14
15
  Node,
15
16
  NodeScope,
16
17
  get_node_calc_lst,
@@ -53,13 +54,14 @@ async def compose_nodes(
53
54
  parent_nodes: dict[type[Node], NodeSession] = {}
54
55
  event_nodes: dict[type[Node], NodeSession] = ctx.get_or_set(CONTEXT_STORE_NODES_KEY, {})
55
56
  # TODO: optimize flattened list calculation via caching key = tuple of node types
56
- calculation_nodes: dict[type[Node], tuple[type[Node], ...]] = {
57
- node_t: tuple(get_node_calc_lst(node_t)) for node_t in nodes.values()
57
+ calculation_nodes: dict[tuple[str, type[Node]], tuple[type[Node], ...]] = {
58
+ (node_name, node_t): tuple(get_node_calc_lst(node_t)) for node_name, node_t in nodes.items()
58
59
  }
59
60
 
60
- for parent_node, linked_nodes in calculation_nodes.items():
61
+ for (parent_node_name, parent_node_t), linked_nodes in calculation_nodes.items():
61
62
  local_nodes = {}
62
63
  subnodes = {}
64
+ data[Name] = parent_node_name
63
65
 
64
66
  for node_t in linked_nodes:
65
67
  scope = getattr(node_t, "scope", None)
@@ -88,7 +90,7 @@ async def compose_nodes(
88
90
  elif scope is NodeScope.GLOBAL:
89
91
  setattr(node_t, GLOBAL_VALUE_KEY, local_nodes[node_t])
90
92
 
91
- parent_nodes[parent_node] = local_nodes[parent_node]
93
+ parent_nodes[parent_node_t] = local_nodes[parent_node_t]
92
94
 
93
95
  node_sessions = {k: parent_nodes[t] for k, t in nodes.items()}
94
96
  return Ok(NodeCollection(node_sessions))
telegrinder/node/event.py CHANGED
@@ -7,7 +7,7 @@ from telegrinder.api.api import API
7
7
  from telegrinder.bot.cute_types import BaseCute
8
8
  from telegrinder.bot.dispatch.context import Context
9
9
  from telegrinder.msgspec_utils import DataclassInstance, decoder
10
- from telegrinder.node.base import ComposeError, Node
10
+ from telegrinder.node.base import ComposeError, FactoryNode
11
11
  from telegrinder.node.update import UpdateNode
12
12
 
13
13
  if typing.TYPE_CHECKING:
@@ -18,17 +18,11 @@ if typing.TYPE_CHECKING:
18
18
  EVENT_NODE_KEY = "_event_node"
19
19
 
20
20
 
21
- class _EventNode(Node):
21
+ class _EventNode(FactoryNode):
22
22
  dataclass: type["DataclassType"]
23
23
 
24
- def __new__(cls, dataclass: type["DataclassType"], /) -> type[typing.Self]:
25
- namespace = dict(**cls.__dict__)
26
- namespace.pop("__new__", None)
27
- new_cls = type("EventNode", (cls,), {"dataclass": dataclass, **namespace})
28
- return new_cls # type: ignore
29
-
30
24
  def __class_getitem__(cls, dataclass: type["DataclassType"], /) -> typing.Self:
31
- return cls(dataclass)
25
+ return cls(dataclass=dataclass)
32
26
 
33
27
  @classmethod
34
28
  def compose(cls, raw_update: UpdateNode, ctx: Context, api: API) -> "DataclassType":
telegrinder/node/text.py CHANGED
@@ -1,4 +1,6 @@
1
- from telegrinder.node.base import ComposeError, ScalarNode
1
+ import typing
2
+
3
+ from telegrinder.node.base import ComposeError, FactoryNode, ScalarNode
2
4
  from telegrinder.node.message import MessageNode
3
5
 
4
6
 
@@ -18,4 +20,22 @@ class TextInteger(ScalarNode, int):
18
20
  return int(text)
19
21
 
20
22
 
21
- __all__ = ("Text", "TextInteger")
23
+ if typing.TYPE_CHECKING:
24
+ from typing import Literal as TextLiteral
25
+
26
+ else:
27
+
28
+ class TextLiteral(FactoryNode):
29
+ texts: tuple[str, ...]
30
+
31
+ def __class_getitem__(cls, texts, /):
32
+ return cls(texts=(texts,) if not isinstance(texts, tuple) else texts)
33
+
34
+ @classmethod
35
+ def compose(cls, text: Text) -> str:
36
+ if text in cls.texts:
37
+ return text
38
+ raise ComposeError("Text matching failed.")
39
+
40
+
41
+ __all__ = ("Text", "TextInteger", "TextLiteral")
@@ -279,30 +279,30 @@ def underline(string: str) -> TagFormat:
279
279
 
280
280
 
281
281
  __all__ = (
282
- "FormatString",
283
- "HTMLFormatter",
284
- "SpecialFormat",
285
- "block_quote",
286
- "bold",
287
- "channel_boost_link",
288
- "code_inline",
289
- "escape",
290
- "get_channel_boost_link",
291
- "get_invite_chat_link",
292
- "get_mention_link",
293
- "get_resolve_domain_link",
294
- "get_start_bot_link",
295
- "get_start_group_link",
296
- "invite_chat_link",
297
- "italic",
298
- "link",
299
- "mention",
300
- "pre_code",
301
- "resolve_domain",
302
- "spoiler",
303
- "start_bot_link",
304
- "start_group_link",
305
- "strike",
306
- "tg_emoji",
282
+ "FormatString",
283
+ "HTMLFormatter",
284
+ "SpecialFormat",
285
+ "block_quote",
286
+ "bold",
287
+ "channel_boost_link",
288
+ "code_inline",
289
+ "escape",
290
+ "get_channel_boost_link",
291
+ "get_invite_chat_link",
292
+ "get_mention_link",
293
+ "get_resolve_domain_link",
294
+ "get_start_bot_link",
295
+ "get_start_group_link",
296
+ "invite_chat_link",
297
+ "italic",
298
+ "link",
299
+ "mention",
300
+ "pre_code",
301
+ "resolve_domain",
302
+ "spoiler",
303
+ "start_bot_link",
304
+ "start_group_link",
305
+ "strike",
306
+ "tg_emoji",
307
307
  "underline",
308
308
  )
@@ -3,10 +3,10 @@ from .middleware import ABCTranslatorMiddleware
3
3
  from .simple import SimpleI18n, SimpleTranslator
4
4
 
5
5
  __all__ = (
6
- "ABCI18n",
7
- "ABCTranslator",
8
- "ABCTranslatorMiddleware",
9
- "I18nEnum",
10
- "SimpleI18n",
6
+ "ABCI18n",
7
+ "ABCTranslator",
8
+ "ABCTranslatorMiddleware",
9
+ "I18nEnum",
10
+ "SimpleI18n",
11
11
  "SimpleTranslator",
12
12
  )
@@ -26,7 +26,7 @@ class I18nEnum(enum.Enum):
26
26
 
27
27
 
28
28
  __all__ = (
29
- "ABCI18n",
30
- "ABCTranslator",
29
+ "ABCI18n",
30
+ "ABCTranslator",
31
31
  "I18nEnum",
32
32
  )
@@ -106,7 +106,7 @@ class Keyboard(ABCMarkup[Button], KeyboardModel):
106
106
  return ReplyKeyboardMarkup(**self.dict())
107
107
 
108
108
  def keyboard_remove(self, *, selective: bool = False) -> ReplyKeyboardRemove:
109
- return ReplyKeyboardRemove(remove_keyboard=True, selective=Some(selective))
109
+ return ReplyKeyboardRemove(remove_keyboard=True, selective=selective)
110
110
 
111
111
 
112
112
  class InlineKeyboard(ABCMarkup[InlineButton]):
@@ -124,9 +124,9 @@ class InlineKeyboard(ABCMarkup[InlineButton]):
124
124
 
125
125
 
126
126
  __all__ = (
127
- "ABCMarkup",
128
- "InlineKeyboard",
129
- "Keyboard",
130
- "KeyboardModel",
127
+ "ABCMarkup",
128
+ "InlineKeyboard",
129
+ "Keyboard",
130
+ "KeyboardModel",
131
131
  "copy_keyboard",
132
132
  )
@@ -5,16 +5,17 @@ import datetime
5
5
  import typing
6
6
 
7
7
  from telegrinder.modules import logger
8
-
9
- from .abc import ABCLoopWrapper
8
+ from telegrinder.tools.loop_wrapper.abc import ABCLoopWrapper
10
9
 
11
10
  T = typing.TypeVar("T")
12
11
  P = typing.ParamSpec("P")
13
- CoroFunc = typing.TypeVar("CoroFunc", bound="CoroutineFunc")
12
+ CoroFunc = typing.TypeVar("CoroFunc", bound="CoroutineFunc[..., typing.Any]")
14
13
 
15
14
  CoroutineTask: typing.TypeAlias = typing.Coroutine[typing.Any, typing.Any, T]
16
15
  CoroutineFunc: typing.TypeAlias = typing.Callable[P, CoroutineTask[T]]
17
- Task: typing.TypeAlias = typing.Union[CoroutineFunc, CoroutineTask, "DelayedTask"]
16
+ Task: typing.TypeAlias = (
17
+ "CoroutineFunc[P, T] | CoroutineTask[T] | DelayedTask[typing.Callable[P, CoroutineTask[T]]]"
18
+ )
18
19
 
19
20
 
20
21
  def run_tasks(
@@ -90,7 +91,12 @@ class LoopWrapper(ABCLoopWrapper):
90
91
  ) -> None:
91
92
  self.tasks: list[CoroutineTask[typing.Any]] = tasks or []
92
93
  self.lifespan = lifespan or Lifespan()
93
- self._loop = event_loop or asyncio.new_event_loop()
94
+ self._loop = event_loop
95
+
96
+ @property
97
+ def loop(self) -> asyncio.AbstractEventLoop:
98
+ assert self._loop is not None
99
+ return self._loop
94
100
 
95
101
  def __repr__(self) -> str:
96
102
  return "<{}: loop={!r} with tasks={!r}, lifespan={!r}>".format(
@@ -104,11 +110,13 @@ class LoopWrapper(ABCLoopWrapper):
104
110
  if not self.tasks:
105
111
  logger.warning("You run loop with 0 tasks!")
106
112
 
113
+ self._loop = asyncio.new_event_loop() if self._loop is None else self._loop
107
114
  self.lifespan.start(self._loop)
115
+
108
116
  while self.tasks:
109
117
  self._loop.create_task(self.tasks.pop(0))
110
- tasks = asyncio.all_tasks(self._loop)
111
118
 
119
+ tasks = asyncio.all_tasks(self._loop)
112
120
  try:
113
121
  while tasks:
114
122
  tasks_results, _ = self._loop.run_until_complete(
@@ -132,17 +140,17 @@ class LoopWrapper(ABCLoopWrapper):
132
140
  def add_task(self, task: Task) -> None:
133
141
  task = to_coroutine_task(task)
134
142
 
135
- if self._loop and self._loop.is_running():
143
+ if self._loop is not None and self._loop.is_running():
136
144
  self._loop.create_task(task)
137
145
  else:
138
146
  self.tasks.append(task)
139
147
 
140
148
  def complete_tasks(self, tasks: set[asyncio.Task[typing.Any]]) -> None:
141
- tasks = tasks | asyncio.all_tasks(self._loop)
149
+ tasks = tasks | asyncio.all_tasks(self.loop)
142
150
  task_to_cancel = asyncio.gather(*tasks, return_exceptions=True)
143
151
  task_to_cancel.cancel()
144
152
  with contextlib.suppress(asyncio.CancelledError):
145
- self._loop.run_until_complete(task_to_cancel)
153
+ self.loop.run_until_complete(task_to_cancel)
146
154
 
147
155
  @typing.overload
148
156
  def timer(self, *, seconds: datetime.timedelta) -> typing.Callable[..., typing.Any]: ...
@@ -153,16 +153,16 @@ def get_impls(cls: type["Polymorphic"]) -> list[typing.Callable[..., typing.Any]
153
153
 
154
154
 
155
155
  __all__ = (
156
- "TRANSLATIONS_KEY",
157
- "cache_magic_value",
158
- "cache_translation",
159
- "get_annotations",
160
- "get_cached_translation",
161
- "get_default_args",
162
- "get_default_args",
163
- "get_impls",
164
- "impl",
165
- "magic_bundle",
166
- "resolve_arg_names",
156
+ "TRANSLATIONS_KEY",
157
+ "cache_magic_value",
158
+ "cache_translation",
159
+ "get_annotations",
160
+ "get_cached_translation",
161
+ "get_default_args",
162
+ "get_default_args",
163
+ "get_impls",
164
+ "impl",
165
+ "magic_bundle",
166
+ "resolve_arg_names",
167
167
  "to_str",
168
168
  )