matrix-python 1.3.3a0__tar.gz → 1.4.1a0__tar.gz
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.
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/PKG-INFO +1 -1
- matrix_python-1.4.1a0/matrix/_version.py +24 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/bot.py +131 -123
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/extension.py +16 -3
- matrix_python-1.4.1a0/matrix/protocols.py +9 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/registry.py +77 -13
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix_python.egg-info/PKG-INFO +1 -1
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix_python.egg-info/SOURCES.txt +1 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/tests/test_bot.py +38 -25
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/tests/test_extension.py +92 -6
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/tests/test_registry.py +4 -2
- matrix_python-1.3.3a0/matrix/_version.py +0 -34
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/.github/dependabot.yml +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/.github/workflows/CODEOWNERS +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/.github/workflows/codeql.yml +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/.github/workflows/publish.yml +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/.github/workflows/scorecard.yml +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/.github/workflows/tests.yml +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/.gitignore +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/CODE_OF_CONDUCT.md +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/CONTRIBUTING.md +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/LICENSE +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/README.md +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/examples/README.md +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/examples/checks.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/examples/config.yaml +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/examples/cooldown.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/examples/error_handling.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/examples/extension.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/examples/ping.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/examples/reaction.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/examples/scheduler.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/__init__.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/checks.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/command.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/config.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/content.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/context.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/errors.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/group.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/help/__init__.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/help/help_command.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/help/pagination.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/message.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/room.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/scheduler.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix/types.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix_python.egg-info/dependency_links.txt +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix_python.egg-info/requires.txt +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/matrix_python.egg-info/top_level.txt +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/mypy.ini +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/pyproject.toml +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/setup.cfg +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/tests/config_fixture.yaml +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/tests/config_fixture_token.yaml +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/tests/help/test_default_help_command.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/tests/help/test_help_command.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/tests/help/test_pagination.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/tests/test_command.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/tests/test_config.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/tests/test_context.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/tests/test_group.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/tests/test_message.py +0 -0
- {matrix_python-1.3.3a0 → matrix_python-1.4.1a0}/tests/test_room.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matrix-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.1a0
|
|
4
4
|
Summary: An easy-to-use Matrix bot framework designed for quick development and minimal setup
|
|
5
5
|
Author: Simon Roy, Chris Dedman Rollet
|
|
6
6
|
Maintainer-email: Code Society Lab <admin@codesociety.xyz>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '1.4.1a0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 4, 1, 'a0')
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = 'gcd71354ad'
|
|
@@ -3,7 +3,7 @@ import inspect
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
5
|
|
|
6
|
-
from typing import Union, Optional
|
|
6
|
+
from typing import Union, Optional, Any
|
|
7
7
|
|
|
8
8
|
from nio import AsyncClient, Event, MatrixRoom
|
|
9
9
|
|
|
@@ -49,9 +49,27 @@ class Bot(Registry):
|
|
|
49
49
|
self.help: HelpCommand = help or DefaultHelpCommand(prefix=self.prefix)
|
|
50
50
|
self.register_command(self.help)
|
|
51
51
|
|
|
52
|
-
self.client.add_event_callback(self.
|
|
52
|
+
self.client.add_event_callback(self._on_matrix_event, Event)
|
|
53
53
|
self._auto_register_events()
|
|
54
54
|
|
|
55
|
+
def _auto_register_events(self) -> None:
|
|
56
|
+
for attr in dir(self):
|
|
57
|
+
if not attr.startswith("on_"):
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
coro = getattr(self, attr, None)
|
|
61
|
+
if not inspect.iscoroutinefunction(coro):
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
if attr in self.LIFECYCLE_EVENTS:
|
|
66
|
+
self.hook(coro)
|
|
67
|
+
|
|
68
|
+
if attr in self.EVENT_MAP:
|
|
69
|
+
self.event(coro)
|
|
70
|
+
except ValueError:
|
|
71
|
+
continue
|
|
72
|
+
|
|
55
73
|
def get_room(self, room_id: str) -> Room:
|
|
56
74
|
"""Retrieve a Room instance based on the room_id."""
|
|
57
75
|
matrix_room = self.client.rooms[room_id]
|
|
@@ -72,6 +90,9 @@ class Bot(Registry):
|
|
|
72
90
|
for event_type, handlers in extension._event_handlers.items():
|
|
73
91
|
self._event_handlers[event_type].extend(handlers)
|
|
74
92
|
|
|
93
|
+
for hook_name, handlers in extension._hook_handlers.items():
|
|
94
|
+
self._hook_handlers[hook_name].extend(handlers)
|
|
95
|
+
|
|
75
96
|
self._checks.extend(extension._checks)
|
|
76
97
|
self._error_handlers.update(extension._error_handlers)
|
|
77
98
|
self._command_error_handlers.update(extension._command_error_handlers)
|
|
@@ -84,7 +105,7 @@ class Bot(Registry):
|
|
|
84
105
|
)
|
|
85
106
|
|
|
86
107
|
self.extensions[extension.name] = extension
|
|
87
|
-
extension.load()
|
|
108
|
+
extension.load(self)
|
|
88
109
|
self.log.debug("loaded extension '%s'", extension.name)
|
|
89
110
|
|
|
90
111
|
def unload_extension(self, ext_name: str) -> None:
|
|
@@ -118,133 +139,74 @@ class Bot(Registry):
|
|
|
118
139
|
extension.unload()
|
|
119
140
|
self.log.debug("unloaded extension '%s'", ext_name)
|
|
120
141
|
|
|
121
|
-
|
|
122
|
-
for attr in dir(self):
|
|
123
|
-
if not attr.startswith("on_"):
|
|
124
|
-
continue
|
|
125
|
-
coro = getattr(self, attr, None)
|
|
126
|
-
if inspect.iscoroutinefunction(coro):
|
|
127
|
-
try:
|
|
128
|
-
self.event(coro)
|
|
129
|
-
except ValueError: # ignore unknown name
|
|
130
|
-
continue
|
|
131
|
-
|
|
132
|
-
async def _on_event(self, room: MatrixRoom, event: Event) -> None:
|
|
133
|
-
# ignore bot events
|
|
134
|
-
if event.sender == self.client.user:
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
# ignore events that happened before the bot started
|
|
138
|
-
if self.start_at and self.start_at > (event.server_timestamp / 1000):
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
try:
|
|
142
|
-
await self._dispatch(room, event)
|
|
143
|
-
except Exception as error:
|
|
144
|
-
await self.on_error(error)
|
|
145
|
-
|
|
146
|
-
async def _dispatch(self, room: MatrixRoom, event: Event) -> None:
|
|
147
|
-
"""Internal type-based fan-out plus optional command handling."""
|
|
148
|
-
for event_type, funcs in self._event_handlers.items():
|
|
149
|
-
if isinstance(event, event_type):
|
|
150
|
-
for func in funcs:
|
|
151
|
-
await func(room, event)
|
|
152
|
-
|
|
153
|
-
async def _process_commands(self, room: MatrixRoom, event: Event) -> None:
|
|
154
|
-
"""Parse and execute commands"""
|
|
155
|
-
ctx = await self._build_context(room, event)
|
|
156
|
-
|
|
157
|
-
if ctx.command:
|
|
158
|
-
for check in self._checks:
|
|
159
|
-
if not await check(ctx):
|
|
160
|
-
raise CheckError(ctx.command, check)
|
|
161
|
-
|
|
162
|
-
await ctx.command(ctx)
|
|
163
|
-
|
|
164
|
-
async def _build_context(self, matrix_room: MatrixRoom, event: Event) -> Context:
|
|
165
|
-
room = self.get_room(matrix_room.room_id)
|
|
166
|
-
ctx = Context(bot=self, room=room, event=event)
|
|
167
|
-
prefix: str | None = None
|
|
168
|
-
|
|
169
|
-
if self.prefix is not None and ctx.body.startswith(self.prefix):
|
|
170
|
-
prefix = self.prefix
|
|
171
|
-
else:
|
|
172
|
-
prefix = next(
|
|
173
|
-
(
|
|
174
|
-
cmd.prefix
|
|
175
|
-
for cmd in self._commands.values()
|
|
176
|
-
if cmd.prefix is not None and ctx.body.startswith(cmd.prefix)
|
|
177
|
-
),
|
|
178
|
-
self.config.prefix,
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
if prefix is None or not ctx.body.startswith(prefix):
|
|
182
|
-
return ctx
|
|
183
|
-
|
|
184
|
-
if parts := ctx.body[len(prefix) :].split():
|
|
185
|
-
cmd_name = parts[0]
|
|
186
|
-
cmd = self._commands.get(cmd_name)
|
|
187
|
-
|
|
188
|
-
if cmd and cmd.prefix and not ctx.body.startswith(cmd.prefix):
|
|
189
|
-
return ctx
|
|
190
|
-
|
|
191
|
-
if not cmd:
|
|
192
|
-
raise CommandNotFoundError(cmd_name)
|
|
193
|
-
|
|
194
|
-
ctx.command = cmd
|
|
195
|
-
|
|
196
|
-
return ctx
|
|
197
|
-
|
|
198
|
-
async def on_message(self, room: MatrixRoom, event: Event) -> None:
|
|
199
|
-
"""
|
|
200
|
-
Invoked when a message event is received.
|
|
201
|
-
|
|
202
|
-
This method is automatically called when a :class:`nio.RoomMessageText`
|
|
203
|
-
event is detected. It is primarily responsible for detecting and
|
|
204
|
-
processing commands that match the bot's defined prefix.
|
|
205
|
-
|
|
206
|
-
:param ctx: The context object containing information about the Matrix
|
|
207
|
-
room and the message event.
|
|
208
|
-
:type ctx: Context
|
|
209
|
-
"""
|
|
210
|
-
await self._process_commands(room, event)
|
|
142
|
+
# LIFECYCLE
|
|
211
143
|
|
|
212
144
|
async def on_ready(self) -> None:
|
|
213
|
-
"""
|
|
214
|
-
|
|
145
|
+
"""Override this in a subclass."""
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
async def _on_ready(self) -> None:
|
|
149
|
+
"""Internal hook — always fires, calls public override then extension handlers."""
|
|
150
|
+
await self.on_ready()
|
|
151
|
+
await self._dispatch("on_ready")
|
|
215
152
|
|
|
216
153
|
async def on_error(self, error: Exception) -> None:
|
|
217
|
-
"""
|
|
218
|
-
|
|
219
|
-
a generic error callback, or logging the exception.
|
|
154
|
+
"""Override this in a subclass."""
|
|
155
|
+
self.log.exception("Unhandled error: '%s'", error)
|
|
220
156
|
|
|
221
|
-
|
|
222
|
-
:type error: Exceptipon
|
|
223
|
-
"""
|
|
157
|
+
async def _on_error(self, error: Exception) -> None:
|
|
224
158
|
if handler := self._error_handlers.get(type(error)):
|
|
225
159
|
await handler(error)
|
|
226
160
|
return
|
|
227
161
|
|
|
228
|
-
if self.
|
|
229
|
-
await self.
|
|
162
|
+
if self._fallback_error_handler:
|
|
163
|
+
await self._fallback_error_handler(error)
|
|
230
164
|
return
|
|
165
|
+
|
|
166
|
+
await self._dispatch("on_error", error)
|
|
167
|
+
|
|
168
|
+
async def on_command(self, _ctx: Context) -> None:
|
|
169
|
+
"""Override this in a subclass."""
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
async def _on_command(self, ctx: Context) -> None:
|
|
173
|
+
await self._dispatch("on_command", ctx)
|
|
174
|
+
|
|
175
|
+
async def on_command_error(self, _ctx: Context, error: Exception) -> None:
|
|
176
|
+
"""Override this in a subclass."""
|
|
231
177
|
self.log.exception("Unhandled error: '%s'", error)
|
|
232
178
|
|
|
233
|
-
async def
|
|
179
|
+
async def _on_command_error(self, ctx: Context, error: Exception) -> None:
|
|
234
180
|
"""
|
|
235
181
|
Handles errors raised during command invocation.
|
|
236
182
|
|
|
237
183
|
This method is called automatically when a command error occurs.
|
|
238
184
|
If a specific error handler is registered for the type of the
|
|
239
185
|
exception, it will be invoked with the current context and error.
|
|
240
|
-
|
|
241
|
-
:param ctx: The context in which the command was invoked.
|
|
242
|
-
:type ctx: Context
|
|
243
|
-
:param error: The exception that was raised during command execution.
|
|
244
|
-
:type error: Exception
|
|
245
186
|
"""
|
|
246
187
|
if handler := self._command_error_handlers.get(type(error)):
|
|
247
188
|
await handler(ctx, error)
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
await self._dispatch("on_command_error", ctx, error)
|
|
192
|
+
|
|
193
|
+
# ENTRYPOINT
|
|
194
|
+
|
|
195
|
+
def start(self) -> None:
|
|
196
|
+
"""
|
|
197
|
+
Synchronous entry point for running the bot.
|
|
198
|
+
|
|
199
|
+
This is a convenience wrapper that allows running the bot like a
|
|
200
|
+
script using a blocking call. It internally calls :meth:`run` within
|
|
201
|
+
:func:`asyncio.run`, and ensures the client is closed gracefully
|
|
202
|
+
on interruption.
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
asyncio.run(self.run())
|
|
206
|
+
except KeyboardInterrupt:
|
|
207
|
+
self.log.info("bot interrupted by user")
|
|
208
|
+
finally:
|
|
209
|
+
asyncio.run(self.client.close())
|
|
248
210
|
|
|
249
211
|
async def run(self) -> None:
|
|
250
212
|
"""
|
|
@@ -268,21 +230,67 @@ class Bot(Registry):
|
|
|
268
230
|
|
|
269
231
|
self.scheduler.start()
|
|
270
232
|
|
|
271
|
-
await self.
|
|
233
|
+
await self._on_ready()
|
|
272
234
|
await self.client.sync_forever(timeout=30_000)
|
|
273
235
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
236
|
+
# MATRIX EVENTS
|
|
237
|
+
|
|
238
|
+
async def on_message(self, room: MatrixRoom, event: Event) -> None:
|
|
239
|
+
await self._process_commands(room, event)
|
|
240
|
+
|
|
241
|
+
async def _on_matrix_event(self, room: MatrixRoom, event: Event) -> None:
|
|
242
|
+
# ignore bot events
|
|
243
|
+
if event.sender == self.client.user:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
# ignore events that happened before the bot started
|
|
247
|
+
if self.start_at and self.start_at > (event.server_timestamp / 1000):
|
|
248
|
+
return
|
|
277
249
|
|
|
278
|
-
This is a convenience wrapper that allows running the bot like a
|
|
279
|
-
script using a blocking call. It internally calls :meth:`run` within
|
|
280
|
-
:func:`asyncio.run`, and ensures the client is closed gracefully
|
|
281
|
-
on interruption.
|
|
282
|
-
"""
|
|
283
250
|
try:
|
|
284
|
-
|
|
285
|
-
except
|
|
286
|
-
self.
|
|
287
|
-
|
|
288
|
-
|
|
251
|
+
await self._dispatch_matrix_event(room, event)
|
|
252
|
+
except Exception as error:
|
|
253
|
+
await self._on_error(error)
|
|
254
|
+
|
|
255
|
+
async def _dispatch(self, event_name: str, *args: Any, **kwargs: Any) -> None:
|
|
256
|
+
"""Fire all listeners registered for a named lifecycle event."""
|
|
257
|
+
for handler in self._hook_handlers.get(event_name, []):
|
|
258
|
+
await handler(*args, **kwargs)
|
|
259
|
+
|
|
260
|
+
async def _dispatch_matrix_event(self, room: MatrixRoom, event: Event) -> None:
|
|
261
|
+
"""Fire all listeners registered for a named matrix event."""
|
|
262
|
+
for event_type, funcs in self._event_handlers.items():
|
|
263
|
+
if isinstance(event, event_type):
|
|
264
|
+
for func in funcs:
|
|
265
|
+
await func(room, event)
|
|
266
|
+
|
|
267
|
+
async def _process_commands(self, room: MatrixRoom, event: Event) -> None:
|
|
268
|
+
"""Parse and execute commands"""
|
|
269
|
+
ctx = await self._build_context(room, event)
|
|
270
|
+
|
|
271
|
+
if ctx.command:
|
|
272
|
+
for check in self._checks:
|
|
273
|
+
if not await check(ctx):
|
|
274
|
+
raise CheckError(ctx.command, check)
|
|
275
|
+
|
|
276
|
+
await self._on_command(ctx)
|
|
277
|
+
await ctx.command(ctx)
|
|
278
|
+
|
|
279
|
+
async def _build_context(self, matrix_room: MatrixRoom, event: Event) -> Context:
|
|
280
|
+
room = self.get_room(matrix_room.room_id)
|
|
281
|
+
ctx = Context(bot=self, room=room, event=event)
|
|
282
|
+
prefix = self.prefix or self.config.prefix
|
|
283
|
+
|
|
284
|
+
if not ctx.body.startswith(prefix):
|
|
285
|
+
return ctx
|
|
286
|
+
|
|
287
|
+
if parts := ctx.body[len(prefix) :].split():
|
|
288
|
+
cmd_name = parts[0]
|
|
289
|
+
cmd = self._commands.get(cmd_name)
|
|
290
|
+
|
|
291
|
+
if not cmd:
|
|
292
|
+
raise CommandNotFoundError(cmd_name)
|
|
293
|
+
|
|
294
|
+
ctx.command = cmd
|
|
295
|
+
|
|
296
|
+
return ctx
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Callable, Optional
|
|
3
4
|
|
|
4
|
-
from
|
|
5
|
+
from matrix.protocols import BotLike
|
|
5
6
|
from matrix.registry import Registry
|
|
7
|
+
from matrix.room import Room
|
|
6
8
|
|
|
7
9
|
logger = logging.getLogger(__name__)
|
|
8
10
|
|
|
@@ -10,10 +12,19 @@ logger = logging.getLogger(__name__)
|
|
|
10
12
|
class Extension(Registry):
|
|
11
13
|
def __init__(self, name: str, prefix: Optional[str] = None) -> None:
|
|
12
14
|
super().__init__(name, prefix=prefix)
|
|
15
|
+
|
|
16
|
+
self.bot: Optional[BotLike] = None
|
|
13
17
|
self._on_load: Optional[Callable] = None
|
|
14
18
|
self._on_unload: Optional[Callable] = None
|
|
15
19
|
|
|
16
|
-
def
|
|
20
|
+
def get_room(self, room_id: str) -> Room:
|
|
21
|
+
if self.bot is None:
|
|
22
|
+
raise RuntimeError("Extension is not loaded")
|
|
23
|
+
return self.bot.get_room(room_id)
|
|
24
|
+
|
|
25
|
+
def load(self, bot: BotLike) -> None:
|
|
26
|
+
self.bot = bot
|
|
27
|
+
|
|
17
28
|
if self._on_load:
|
|
18
29
|
self._on_load()
|
|
19
30
|
|
|
@@ -35,6 +46,8 @@ class Extension(Registry):
|
|
|
35
46
|
return func
|
|
36
47
|
|
|
37
48
|
def unload(self) -> None:
|
|
49
|
+
self.bot = None
|
|
50
|
+
|
|
38
51
|
if self._on_unload:
|
|
39
52
|
self._on_unload()
|
|
40
53
|
|
|
@@ -47,6 +47,13 @@ class Registry:
|
|
|
47
47
|
"on_member_change": RoomMemberEvent,
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
LIFECYCLE_EVENTS: set[str] = {
|
|
51
|
+
"on_ready",
|
|
52
|
+
"on_error",
|
|
53
|
+
"on_command",
|
|
54
|
+
"on_command_error",
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
def __init__(self, name: str, prefix: Optional[str] = None):
|
|
51
58
|
self.name = name
|
|
52
59
|
self.prefix = prefix
|
|
@@ -57,7 +64,8 @@ class Registry:
|
|
|
57
64
|
self._scheduler: Scheduler = Scheduler()
|
|
58
65
|
|
|
59
66
|
self._event_handlers: Dict[Type[Event], List[Callback]] = defaultdict(list)
|
|
60
|
-
self.
|
|
67
|
+
self._hook_handlers: Dict[str, List[Callback]] = defaultdict(list)
|
|
68
|
+
self._fallback_error_handler: Optional[ErrorCallback] = None
|
|
61
69
|
self._error_handlers: Dict[type[Exception], ErrorCallback] = {}
|
|
62
70
|
self._command_error_handlers: Dict[type[Exception], CommandErrorCallback] = {}
|
|
63
71
|
|
|
@@ -208,17 +216,15 @@ class Registry:
|
|
|
208
216
|
if not inspect.iscoroutinefunction(f):
|
|
209
217
|
raise TypeError("Event handlers must be coroutines")
|
|
210
218
|
|
|
211
|
-
if event_spec
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if event_type is None:
|
|
221
|
-
raise ValueError(f"Unknown event name: {f.__name__}")
|
|
219
|
+
key = event_spec if isinstance(event_spec, str) else f.__name__
|
|
220
|
+
event_type: type[Event] | None = (
|
|
221
|
+
event_spec
|
|
222
|
+
if event_spec and not isinstance(event_spec, str)
|
|
223
|
+
else self.EVENT_MAP.get(key)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if event_type is None:
|
|
227
|
+
raise ValueError(f"Unknown event: {key!r}")
|
|
222
228
|
|
|
223
229
|
return self.register_event(event_type, f)
|
|
224
230
|
|
|
@@ -238,6 +244,64 @@ class Registry:
|
|
|
238
244
|
)
|
|
239
245
|
return callback
|
|
240
246
|
|
|
247
|
+
def hook(
|
|
248
|
+
self, func: Optional[Callback] = None, *, event_name: Optional[str] = None
|
|
249
|
+
) -> Union[Callback, Callable[[Callback], Callback]]:
|
|
250
|
+
"""Decorator to register a coroutine as a lifecycle event hook.
|
|
251
|
+
|
|
252
|
+
Lifecycle events include things like ``on_ready``, ``on_command``,
|
|
253
|
+
and ``on_error``. If the event name is not provided, it is inferred
|
|
254
|
+
from the function name. Multiple handlers for the same lifecycle
|
|
255
|
+
event are supported and called in registration order.
|
|
256
|
+
|
|
257
|
+
## Example
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
@bot.hook
|
|
261
|
+
async def on_ready():
|
|
262
|
+
print("Bot is ready!")
|
|
263
|
+
|
|
264
|
+
@bot.hook(event_name="on_command")
|
|
265
|
+
async def log_command(ctx):
|
|
266
|
+
print(f"Command invoked: {ctx.command}")
|
|
267
|
+
```
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
def wrapper(f: Callback) -> Callback:
|
|
271
|
+
if not inspect.iscoroutinefunction(f):
|
|
272
|
+
raise TypeError("Lifecycle hooks must be coroutines")
|
|
273
|
+
|
|
274
|
+
name = event_name or f.__name__
|
|
275
|
+
if name not in self.LIFECYCLE_EVENTS:
|
|
276
|
+
raise ValueError(f"Unknown lifecycle event: {name}")
|
|
277
|
+
|
|
278
|
+
return self.register_hook(name, f)
|
|
279
|
+
|
|
280
|
+
if func is None:
|
|
281
|
+
return wrapper
|
|
282
|
+
return wrapper(func)
|
|
283
|
+
|
|
284
|
+
def register_hook(self, event_name: str, callback: Callback) -> Callback:
|
|
285
|
+
"""Register a lifecycle event hook directly for a given event name.
|
|
286
|
+
|
|
287
|
+
Prefer the :meth:`hook` decorator for typical use. This method
|
|
288
|
+
is useful when loading lifecycle hooks from an extension.
|
|
289
|
+
"""
|
|
290
|
+
if not inspect.iscoroutinefunction(callback):
|
|
291
|
+
raise TypeError("Lifecycle hooks must be coroutines")
|
|
292
|
+
|
|
293
|
+
if event_name not in self.LIFECYCLE_EVENTS:
|
|
294
|
+
raise ValueError(f"Unknown lifecycle event: {event_name}")
|
|
295
|
+
|
|
296
|
+
self._hook_handlers[event_name].append(callback)
|
|
297
|
+
logger.debug(
|
|
298
|
+
"registered lifecycle hook '%s' for event '%s' on %s",
|
|
299
|
+
callback.__name__,
|
|
300
|
+
event_name,
|
|
301
|
+
type(self).__name__,
|
|
302
|
+
)
|
|
303
|
+
return callback
|
|
304
|
+
|
|
241
305
|
def check(self, func: Callback) -> Callback:
|
|
242
306
|
"""Register a global check that must pass before any command is invoked.
|
|
243
307
|
|
|
@@ -321,7 +385,7 @@ class Registry:
|
|
|
321
385
|
if exception:
|
|
322
386
|
self._error_handlers[exception] = func
|
|
323
387
|
else:
|
|
324
|
-
self.
|
|
388
|
+
self._fallback_error_handler = func
|
|
325
389
|
logger.debug(
|
|
326
390
|
"registered error handler '%s' on %s",
|
|
327
391
|
func.__name__,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: matrix-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.1a0
|
|
4
4
|
Summary: An easy-to-use Matrix bot framework designed for quick development and minimal setup
|
|
5
5
|
Author: Simon Roy, Chris Dedman Rollet
|
|
6
6
|
Maintainer-email: Code Society Lab <admin@codesociety.xyz>
|
|
@@ -64,16 +64,15 @@ def test_bot_init_with_invalid_config_file():
|
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
def test_auto_register_events_registers_known_events(bot):
|
|
67
|
-
|
|
68
|
-
async def on_message_known(room, event):
|
|
67
|
+
async def on_message(room, event):
|
|
69
68
|
pass
|
|
70
69
|
|
|
71
|
-
setattr(bot, "
|
|
70
|
+
setattr(bot, "on_message", on_message)
|
|
72
71
|
|
|
73
|
-
with patch.object(bot, "event", wraps=bot.event) as
|
|
72
|
+
with patch.object(bot, "event", wraps=bot.event) as mock_event:
|
|
74
73
|
bot._auto_register_events()
|
|
75
74
|
|
|
76
|
-
|
|
75
|
+
mock_event.assert_any_call(on_message)
|
|
77
76
|
|
|
78
77
|
|
|
79
78
|
@pytest.mark.asyncio
|
|
@@ -100,7 +99,7 @@ async def test_dispatch_calls_all_handlers(bot):
|
|
|
100
99
|
)
|
|
101
100
|
room = MatrixRoom("!roomid:matrix.org", "room_alias")
|
|
102
101
|
|
|
103
|
-
await bot.
|
|
102
|
+
await bot._dispatch_matrix_event(room, event)
|
|
104
103
|
assert "h1" in called
|
|
105
104
|
assert "h2" in called
|
|
106
105
|
|
|
@@ -114,26 +113,27 @@ async def test_on_event_ignores_self_events(bot):
|
|
|
114
113
|
event.sender = "@grace:matrix.org"
|
|
115
114
|
event.server_timestamp = 123456789
|
|
116
115
|
|
|
117
|
-
with patch.object(
|
|
118
|
-
|
|
116
|
+
with patch.object(
|
|
117
|
+
bot, "_dispatch_matrix_event", new_callable=AsyncMock
|
|
118
|
+
) as dispatch:
|
|
119
|
+
await bot._on_matrix_event(MatrixRoom("!room:matrix.org", "alias"), event)
|
|
119
120
|
dispatch.assert_not_called()
|
|
120
121
|
|
|
121
122
|
|
|
122
123
|
@pytest.mark.asyncio
|
|
123
124
|
async def test_on_event_ignores_old_events(bot, room, event):
|
|
124
|
-
# Set start_at after event time
|
|
125
125
|
bot.client.user = "@somebot:matrix.org"
|
|
126
126
|
bot.start_at = event.server_timestamp / 1000 + 10
|
|
127
127
|
|
|
128
|
-
bot.
|
|
129
|
-
await bot.
|
|
128
|
+
bot._dispatch_matrix_event = AsyncMock()
|
|
129
|
+
await bot._on_matrix_event(room, event)
|
|
130
130
|
|
|
131
|
-
bot.
|
|
131
|
+
bot._dispatch_matrix_event.assert_not_called()
|
|
132
132
|
|
|
133
133
|
|
|
134
134
|
@pytest.mark.asyncio
|
|
135
135
|
async def test_on_event_calls_error_handler(bot):
|
|
136
|
-
bot.
|
|
136
|
+
bot._dispatch_matrix_event = AsyncMock(side_effect=Exception("boom"))
|
|
137
137
|
|
|
138
138
|
custom_error_handler = AsyncMock()
|
|
139
139
|
bot.error()(custom_error_handler)
|
|
@@ -144,7 +144,7 @@ async def test_on_event_calls_error_handler(bot):
|
|
|
144
144
|
bot.start_at = 0
|
|
145
145
|
bot.client.user = "@grace:matrix.org"
|
|
146
146
|
|
|
147
|
-
await bot.
|
|
147
|
+
await bot._on_matrix_event(MatrixRoom("!roomid", "alias"), event)
|
|
148
148
|
custom_error_handler.assert_awaited_once()
|
|
149
149
|
|
|
150
150
|
|
|
@@ -156,13 +156,28 @@ async def test_on_message_calls_process_commands(bot, room, event):
|
|
|
156
156
|
|
|
157
157
|
|
|
158
158
|
@pytest.mark.asyncio
|
|
159
|
-
async def
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
async def test_on_ready_dispatches(bot):
|
|
160
|
+
with patch.object(bot, "_dispatch", new_callable=AsyncMock) as mock_dispatch:
|
|
161
|
+
await bot._on_ready()
|
|
162
|
+
mock_dispatch.assert_awaited_once_with("on_ready")
|
|
162
163
|
|
|
163
164
|
|
|
164
165
|
@pytest.mark.asyncio
|
|
165
|
-
async def
|
|
166
|
+
async def test_on_error_calls_specific_handler(bot):
|
|
167
|
+
called = False
|
|
168
|
+
|
|
169
|
+
@bot.error(ValueError)
|
|
170
|
+
async def custom_error_handler(e):
|
|
171
|
+
nonlocal called
|
|
172
|
+
called = True
|
|
173
|
+
|
|
174
|
+
await bot._on_error(ValueError("test error"))
|
|
175
|
+
|
|
176
|
+
assert called, "Specific error handler was not called"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@pytest.mark.asyncio
|
|
180
|
+
async def test_on_error_calls_fallback_handler(bot):
|
|
166
181
|
called = False
|
|
167
182
|
|
|
168
183
|
@bot.error()
|
|
@@ -170,15 +185,14 @@ async def test_on_error_calls_custom_handler(bot):
|
|
|
170
185
|
nonlocal called
|
|
171
186
|
called = True
|
|
172
187
|
|
|
173
|
-
|
|
174
|
-
await bot.on_error(error)
|
|
188
|
+
await bot._fallback_error_handler(Exception("test error"))
|
|
189
|
+
await bot.on_error(Exception("test error"))
|
|
175
190
|
|
|
176
|
-
assert called, "
|
|
191
|
+
assert called, "Fallback error handler was not called"
|
|
177
192
|
|
|
178
193
|
|
|
179
194
|
@pytest.mark.asyncio
|
|
180
195
|
async def test_on_error_logs_when_no_handler(bot):
|
|
181
|
-
bot._on_error = None
|
|
182
196
|
error = Exception("test")
|
|
183
197
|
|
|
184
198
|
await bot.on_error(error)
|
|
@@ -197,7 +211,6 @@ async def test_process_commands_executes_command(bot, event):
|
|
|
197
211
|
event.body = "!greet"
|
|
198
212
|
room = MatrixRoom("!roomid:matrix.org", "alias")
|
|
199
213
|
|
|
200
|
-
# Patch _build_context to return context with command assigned
|
|
201
214
|
with patch.object(
|
|
202
215
|
bot, "_build_context", new_callable=AsyncMock
|
|
203
216
|
) as mock_build_context:
|
|
@@ -383,12 +396,12 @@ async def test_run_uses_token():
|
|
|
383
396
|
async def test_run_with_username_and_password(bot):
|
|
384
397
|
bot.client.login = AsyncMock(return_value="login_resp")
|
|
385
398
|
bot.client.sync_forever = AsyncMock()
|
|
386
|
-
bot.
|
|
399
|
+
bot._on_ready = AsyncMock()
|
|
387
400
|
|
|
388
401
|
await bot.run()
|
|
389
402
|
|
|
390
403
|
bot.client.login.assert_awaited_once_with("grace1234")
|
|
391
|
-
bot.
|
|
404
|
+
bot._on_ready.assert_awaited_once()
|
|
392
405
|
bot.client.sync_forever.assert_awaited_once()
|
|
393
406
|
|
|
394
407
|
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
|
+
from unittest.mock import MagicMock
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
3
6
|
from matrix.extension import Extension
|
|
7
|
+
from matrix.room import Room
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MockBot:
|
|
11
|
+
prefix: str = "!"
|
|
12
|
+
|
|
13
|
+
def __init__(self, room: Optional[Room] = None) -> None:
|
|
14
|
+
self.get_room = MagicMock(return_value=room or MagicMock(spec=Room))
|
|
4
15
|
|
|
5
16
|
|
|
6
17
|
@pytest.fixture
|
|
@@ -8,6 +19,14 @@ def extension() -> Extension:
|
|
|
8
19
|
return Extension(name="test_ext", prefix="!")
|
|
9
20
|
|
|
10
21
|
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def bot() -> MockBot:
|
|
24
|
+
return MockBot()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# INIT
|
|
28
|
+
|
|
29
|
+
|
|
11
30
|
def test_init_with_name_and_prefix__expect_attributes_set():
|
|
12
31
|
ext = Extension(name="math", prefix="!")
|
|
13
32
|
|
|
@@ -21,6 +40,10 @@ def test_init_with_name_only__expect_prefix_is_none():
|
|
|
21
40
|
assert ext.prefix is None
|
|
22
41
|
|
|
23
42
|
|
|
43
|
+
def test_init__expect_bot_is_none(extension: Extension):
|
|
44
|
+
assert extension.bot is None
|
|
45
|
+
|
|
46
|
+
|
|
24
47
|
def test_init__expect_on_load_is_none(extension: Extension):
|
|
25
48
|
assert extension._on_load is None
|
|
26
49
|
|
|
@@ -45,6 +68,9 @@ def test_init__expect_empty_checks(extension: Extension):
|
|
|
45
68
|
assert extension._checks == []
|
|
46
69
|
|
|
47
70
|
|
|
71
|
+
# ON LOAD
|
|
72
|
+
|
|
73
|
+
|
|
48
74
|
def test_on_load_with_sync_function__expect_handler_registered(extension: Extension):
|
|
49
75
|
@extension.on_load
|
|
50
76
|
def setup():
|
|
@@ -86,20 +112,34 @@ def test_on_load_overwrites_previous_handler__expect_latest_handler(
|
|
|
86
112
|
assert extension._on_load is second
|
|
87
113
|
|
|
88
114
|
|
|
89
|
-
|
|
115
|
+
# LOAD
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_load__expect_bot_set(extension: Extension, bot: MockBot):
|
|
119
|
+
extension.load(bot)
|
|
120
|
+
|
|
121
|
+
assert extension.bot is bot
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_load_with_registered_handler__expect_handler_called(
|
|
125
|
+
extension: Extension, bot: MockBot
|
|
126
|
+
):
|
|
90
127
|
called = []
|
|
91
128
|
|
|
92
129
|
@extension.on_load
|
|
93
130
|
def setup():
|
|
94
131
|
called.append(True)
|
|
95
132
|
|
|
96
|
-
extension.load()
|
|
133
|
+
extension.load(bot)
|
|
97
134
|
|
|
98
135
|
assert called == [True]
|
|
99
136
|
|
|
100
137
|
|
|
101
|
-
def test_load_with_no_handler__expect_no_error(extension: Extension):
|
|
102
|
-
extension.load()
|
|
138
|
+
def test_load_with_no_handler__expect_no_error(extension: Extension, bot: MockBot):
|
|
139
|
+
extension.load(bot)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ON UNLOAD
|
|
103
143
|
|
|
104
144
|
|
|
105
145
|
def test_on_unload_with_sync_function__expect_handler_registered(extension: Extension):
|
|
@@ -143,17 +183,63 @@ def test_on_unload_overwrites_previous_handler__expect_latest_handler(
|
|
|
143
183
|
assert extension._on_unload is second
|
|
144
184
|
|
|
145
185
|
|
|
146
|
-
|
|
186
|
+
# UNLOAD
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_unload__expect_bot_cleared(extension: Extension, bot: MockBot):
|
|
190
|
+
extension.load(bot)
|
|
191
|
+
extension.unload()
|
|
192
|
+
|
|
193
|
+
assert extension.bot is None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_unload_with_registered_handler__expect_handler_called(
|
|
197
|
+
extension: Extension, bot: MockBot
|
|
198
|
+
):
|
|
147
199
|
called = []
|
|
148
200
|
|
|
149
201
|
@extension.on_unload
|
|
150
202
|
def teardown():
|
|
151
203
|
called.append(True)
|
|
152
204
|
|
|
205
|
+
extension.load(bot)
|
|
153
206
|
extension.unload()
|
|
154
207
|
|
|
155
208
|
assert called == [True]
|
|
156
209
|
|
|
157
210
|
|
|
158
|
-
def test_unload_with_no_handler__expect_no_error(extension: Extension):
|
|
211
|
+
def test_unload_with_no_handler__expect_no_error(extension: Extension, bot: MockBot):
|
|
212
|
+
extension.load(bot)
|
|
159
213
|
extension.unload()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# GET ROOM
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_get_room_before_load__expect_runtime_error(extension: Extension):
|
|
220
|
+
with pytest.raises(RuntimeError, match="Extension is not loaded"):
|
|
221
|
+
extension.get_room("!room:example.com")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_get_room_after_load__expect_delegates_to_bot(
|
|
225
|
+
extension: Extension, bot: MockBot
|
|
226
|
+
):
|
|
227
|
+
room_id = "!room:example.com"
|
|
228
|
+
expected_room = MagicMock(spec=Room)
|
|
229
|
+
bot.get_room.return_value = expected_room
|
|
230
|
+
|
|
231
|
+
extension.load(bot)
|
|
232
|
+
result = extension.get_room(room_id)
|
|
233
|
+
|
|
234
|
+
bot.get_room.assert_called_once_with(room_id)
|
|
235
|
+
assert result is expected_room
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_get_room_after_unload__expect_runtime_error(
|
|
239
|
+
extension: Extension, bot: MockBot
|
|
240
|
+
):
|
|
241
|
+
extension.load(bot)
|
|
242
|
+
extension.unload()
|
|
243
|
+
|
|
244
|
+
with pytest.raises(RuntimeError, match="Extension is not loaded"):
|
|
245
|
+
extension.get_room("!room:example.com")
|
|
@@ -297,12 +297,14 @@ def test_register_error_handler_with_exception_type__expect_handler_in_dict(
|
|
|
297
297
|
assert registry._error_handlers[ValueError] is on_value_error
|
|
298
298
|
|
|
299
299
|
|
|
300
|
-
def
|
|
300
|
+
def test_register_generic_error_handler__expect_fallback_error_handler_set(
|
|
301
|
+
registry: Registry,
|
|
302
|
+
):
|
|
301
303
|
@registry.error()
|
|
302
304
|
async def on_any_error(error):
|
|
303
305
|
pass
|
|
304
306
|
|
|
305
|
-
assert registry.
|
|
307
|
+
assert registry._fallback_error_handler is on_any_error
|
|
306
308
|
|
|
307
309
|
|
|
308
310
|
def test_register_error_handler_with_non_coroutine__expect_type_error(
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# file generated by setuptools-scm
|
|
2
|
-
# don't change, don't track in version control
|
|
3
|
-
|
|
4
|
-
__all__ = [
|
|
5
|
-
"__version__",
|
|
6
|
-
"__version_tuple__",
|
|
7
|
-
"version",
|
|
8
|
-
"version_tuple",
|
|
9
|
-
"__commit_id__",
|
|
10
|
-
"commit_id",
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
TYPE_CHECKING = False
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from typing import Tuple
|
|
16
|
-
from typing import Union
|
|
17
|
-
|
|
18
|
-
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
-
COMMIT_ID = Union[str, None]
|
|
20
|
-
else:
|
|
21
|
-
VERSION_TUPLE = object
|
|
22
|
-
COMMIT_ID = object
|
|
23
|
-
|
|
24
|
-
version: str
|
|
25
|
-
__version__: str
|
|
26
|
-
__version_tuple__: VERSION_TUPLE
|
|
27
|
-
version_tuple: VERSION_TUPLE
|
|
28
|
-
commit_id: COMMIT_ID
|
|
29
|
-
__commit_id__: COMMIT_ID
|
|
30
|
-
|
|
31
|
-
__version__ = version = '1.3.3a0'
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 3, 3, 'a0')
|
|
33
|
-
|
|
34
|
-
__commit_id__ = commit_id = 'g7d083d049'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|