matrix-python 1.4.3a0__tar.gz → 1.4.4a0__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.
Files changed (63) hide show
  1. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/PKG-INFO +1 -1
  2. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/_version.py +3 -3
  3. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/bot.py +45 -8
  4. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/errors.py +4 -0
  5. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/extension.py +14 -1
  6. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/protocols.py +1 -1
  7. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix_python.egg-info/PKG-INFO +1 -1
  8. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/tests/test_bot.py +110 -4
  9. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/.github/dependabot.yml +0 -0
  10. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/.github/workflows/CODEOWNERS +0 -0
  11. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/.github/workflows/codeql.yml +0 -0
  12. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/.github/workflows/publish.yml +0 -0
  13. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/.github/workflows/scorecard.yml +0 -0
  14. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/.github/workflows/tests.yml +0 -0
  15. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/.gitignore +0 -0
  16. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/CODE_OF_CONDUCT.md +0 -0
  17. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/CONTRIBUTING.md +0 -0
  18. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/LICENSE +0 -0
  19. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/README.md +0 -0
  20. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/examples/README.md +0 -0
  21. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/examples/checks.py +0 -0
  22. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/examples/config.yaml +0 -0
  23. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/examples/cooldown.py +0 -0
  24. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/examples/error_handling.py +0 -0
  25. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/examples/extension.py +0 -0
  26. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/examples/ping.py +0 -0
  27. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/examples/reaction.py +0 -0
  28. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/examples/scheduler.py +0 -0
  29. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/__init__.py +0 -0
  30. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/checks.py +0 -0
  31. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/command.py +0 -0
  32. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/config.py +0 -0
  33. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/content.py +0 -0
  34. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/context.py +0 -0
  35. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/group.py +0 -0
  36. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/help/__init__.py +0 -0
  37. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/help/help_command.py +0 -0
  38. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/help/pagination.py +0 -0
  39. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/message.py +0 -0
  40. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/registry.py +0 -0
  41. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/room.py +0 -0
  42. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/scheduler.py +0 -0
  43. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix/types.py +0 -0
  44. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix_python.egg-info/SOURCES.txt +0 -0
  45. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix_python.egg-info/dependency_links.txt +0 -0
  46. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix_python.egg-info/requires.txt +0 -0
  47. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/matrix_python.egg-info/top_level.txt +0 -0
  48. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/mypy.ini +0 -0
  49. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/pyproject.toml +0 -0
  50. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/setup.cfg +0 -0
  51. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/tests/config_fixture.yaml +0 -0
  52. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/tests/config_fixture_token.yaml +0 -0
  53. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/tests/help/test_default_help_command.py +0 -0
  54. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/tests/help/test_help_command.py +0 -0
  55. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/tests/help/test_pagination.py +0 -0
  56. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/tests/test_command.py +0 -0
  57. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/tests/test_config.py +0 -0
  58. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/tests/test_context.py +0 -0
  59. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/tests/test_extension.py +0 -0
  60. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/tests/test_group.py +0 -0
  61. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/tests/test_message.py +0 -0
  62. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/tests/test_registry.py +0 -0
  63. {matrix_python-1.4.3a0 → matrix_python-1.4.4a0}/tests/test_room.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrix-python
3
- Version: 1.4.3a0
3
+ Version: 1.4.4a0
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>
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.4.3a0'
22
- __version_tuple__ = version_tuple = (1, 4, 3, 'a0')
21
+ __version__ = version = '1.4.4a0'
22
+ __version_tuple__ = version_tuple = (1, 4, 4, 'a0')
23
23
 
24
- __commit_id__ = commit_id = 'gba1ba5d3c'
24
+ __commit_id__ = commit_id = 'gaaee53bc7'
@@ -3,7 +3,7 @@ import inspect
3
3
  import asyncio
4
4
  import logging
5
5
 
6
- from typing import Union, Optional, Any
6
+ from typing import Optional, Any
7
7
 
8
8
  from nio import AsyncClient, Event, MatrixRoom
9
9
 
@@ -15,7 +15,12 @@ from .extension import Extension
15
15
  from .registry import Registry
16
16
  from .help import HelpCommand, DefaultHelpCommand
17
17
  from .scheduler import Scheduler
18
- from .errors import AlreadyRegisteredError, CommandNotFoundError, CheckError
18
+ from .errors import (
19
+ AlreadyRegisteredError,
20
+ CommandNotFoundError,
21
+ CheckError,
22
+ RoomNotFoundError,
23
+ )
19
24
 
20
25
 
21
26
  class Bot(Registry):
@@ -36,7 +41,9 @@ class Bot(Registry):
36
41
 
37
42
  self._config: Config | None = None
38
43
  self._client: AsyncClient | None = None
44
+ self._synced: asyncio.Event = asyncio.Event()
39
45
  self._help: HelpCommand | None = help_
46
+
40
47
  self.extensions: dict[str, Extension] = {}
41
48
  self.scheduler: Scheduler = Scheduler()
42
49
  self.log: logging.Logger = logging.getLogger(__name__)
@@ -75,10 +82,23 @@ class Bot(Registry):
75
82
  except ValueError:
76
83
  continue
77
84
 
78
- def get_room(self, room_id: str) -> Room:
79
- """Retrieve a Room instance based on the room_id."""
80
- matrix_room = self.client.rooms[room_id]
81
- return Room(matrix_room=matrix_room, client=self.client)
85
+ def get_room(self, room_id: str) -> Room | None:
86
+ """Retrieve a `Room` instance by its Matrix room ID.
87
+
88
+ Returns the `Room` object corresponding to `room_id` if it exists in
89
+ the client's known rooms. Returns `None` if the room cannot be found.
90
+
91
+ ## Example
92
+
93
+ ```python
94
+ room = bot.get_room("!abc123:matrix.org")
95
+ if room:
96
+ print(room.name)
97
+ ```
98
+ """
99
+ if matrix_room := self.client.rooms.get(room_id):
100
+ return Room(matrix_room=matrix_room, client=self.client)
101
+ return None
82
102
 
83
103
  def load_extension(self, extension: Extension) -> None:
84
104
  self.log.debug(f"Loading extension: '{extension.name}'")
@@ -255,10 +275,16 @@ class Bot(Registry):
255
275
  login_resp = await self.client.login(self.config.password)
256
276
  self.log.info("logged in: %s", login_resp)
257
277
 
258
- self.scheduler.start()
278
+ sync_task = asyncio.create_task(self.client.sync_forever(timeout=30_000))
259
279
 
280
+ await self._wait_until_synced()
260
281
  await self._on_ready()
261
- await self.client.sync_forever(timeout=30_000)
282
+
283
+ self.scheduler.start()
284
+ await sync_task
285
+
286
+ async def _wait_until_synced(self) -> None:
287
+ await self._synced.wait()
262
288
 
263
289
  # MATRIX EVENTS
264
290
 
@@ -266,6 +292,9 @@ class Bot(Registry):
266
292
  await self._process_commands(room, event)
267
293
 
268
294
  async def _on_matrix_event(self, matrix_room: MatrixRoom, event: Event) -> None:
295
+ if not self._synced.is_set():
296
+ self._synced.set()
297
+
269
298
  # ignore bot events
270
299
  if event.sender == self.client.user:
271
300
  return
@@ -276,6 +305,10 @@ class Bot(Registry):
276
305
 
277
306
  try:
278
307
  room = self.get_room(matrix_room.room_id)
308
+
309
+ if not room:
310
+ raise RoomNotFoundError(f"Room '{matrix_room.room_id}' not found.")
311
+
279
312
  await self._dispatch_matrix_event(room, event)
280
313
  except Exception as error:
281
314
  await self._on_error(error)
@@ -306,6 +339,10 @@ class Bot(Registry):
306
339
 
307
340
  async def _build_context(self, matrix_room: Room, event: Event) -> Context:
308
341
  room = self.get_room(matrix_room.room_id)
342
+
343
+ if not room:
344
+ raise RoomNotFoundError(f"Room '{matrix_room.room_id}' not found.")
345
+
309
346
  ctx = Context(bot=self, room=room, event=event)
310
347
  prefix = self.prefix or self.config.prefix
311
348
 
@@ -13,6 +13,10 @@ class MatrixError(Exception):
13
13
  pass
14
14
 
15
15
 
16
+ class RoomNotFoundError(MatrixError):
17
+ pass
18
+
19
+
16
20
  class RegistryError(MatrixError):
17
21
  pass
18
22
 
@@ -17,7 +17,20 @@ class Extension(Registry):
17
17
  self._on_load: Optional[Callable] = None
18
18
  self._on_unload: Optional[Callable] = None
19
19
 
20
- def get_room(self, room_id: str) -> Room:
20
+ def get_room(self, room_id: str) -> Room | None:
21
+ """Retrieve a `Room` instance by its Matrix room ID.
22
+
23
+ Returns the `Room` object corresponding to `room_id` if it exists in
24
+ the client's known rooms. Returns `None` if the room cannot be found.
25
+
26
+ ## Example
27
+
28
+ ```python
29
+ room = extension.get_room("!abc123:matrix.org")
30
+ if room:
31
+ print(room.name)
32
+ ```
33
+ """
21
34
  if self.bot is None:
22
35
  raise RuntimeError("Extension is not loaded")
23
36
  return self.bot.get_room(room_id)
@@ -6,4 +6,4 @@ from matrix.room import Room
6
6
  class BotLike(Protocol):
7
7
  prefix: str | None
8
8
 
9
- def get_room(self, room_id: str) -> Room: ...
9
+ def get_room(self, room_id: str) -> Room | None: ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrix-python
3
- Version: 1.4.3a0
3
+ Version: 1.4.4a0
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>
@@ -313,6 +313,30 @@ async def test_command_executes(bot):
313
313
  assert called, "Expected command handler to be called"
314
314
 
315
315
 
316
+ @pytest.mark.asyncio
317
+ async def test_command_not_processed_without_prefix(bot, room):
318
+ called = False
319
+
320
+ @bot.command()
321
+ async def greet(ctx):
322
+ nonlocal called
323
+ called = True
324
+
325
+ event = RoomMessageText.from_dict(
326
+ {
327
+ "content": {"body": "greet", "msgtype": "m.text"},
328
+ "event_id": "$id",
329
+ "origin_server_ts": 123456,
330
+ "sender": "@user:matrix.org",
331
+ "type": "m.room.message",
332
+ }
333
+ )
334
+
335
+ await bot._process_commands(room, event)
336
+
337
+ assert not called
338
+
339
+
316
340
  @pytest.mark.asyncio
317
341
  async def test_error_decorator_requires_coroutine(bot):
318
342
  with pytest.raises(TypeError):
@@ -376,18 +400,37 @@ def test_command_duplicate_raises(bot):
376
400
  pass
377
401
 
378
402
 
403
+ import asyncio
404
+
405
+
406
+ async def start_and_stop(coro):
407
+ task = asyncio.create_task(coro)
408
+ await asyncio.sleep(0) # allow startup
409
+ task.cancel()
410
+ await asyncio.gather(task, return_exceptions=True)
411
+
412
+
379
413
  @pytest.mark.asyncio
380
414
  async def test_run_uses_token():
381
415
  bot = Bot()
382
416
  bot._load_config("tests/config_fixture_token.yaml")
383
417
 
384
418
  bot._client.sync_forever = AsyncMock()
385
- bot.on_ready = AsyncMock()
419
+ bot._on_ready = AsyncMock()
420
+
421
+ # unblock readiness
422
+ bot._synced.set()
423
+
424
+ task = asyncio.create_task(bot.run())
425
+
426
+ await asyncio.sleep(0)
427
+ await asyncio.sleep(0)
386
428
 
387
- await bot.run()
429
+ task.cancel()
430
+ await asyncio.gather(task, return_exceptions=True)
388
431
 
389
432
  assert bot._client.access_token == "abc123"
390
- bot.on_ready.assert_awaited_once()
433
+ bot._on_ready.assert_awaited_once()
391
434
  bot._client.sync_forever.assert_awaited_once()
392
435
 
393
436
 
@@ -397,7 +440,15 @@ async def test_run_with_username_and_password(bot):
397
440
  bot._client.sync_forever = AsyncMock()
398
441
  bot._on_ready = AsyncMock()
399
442
 
400
- await bot.run()
443
+ bot._synced.set()
444
+
445
+ task = asyncio.create_task(bot.run())
446
+
447
+ await asyncio.sleep(0)
448
+ await asyncio.sleep(0)
449
+
450
+ task.cancel()
451
+ await asyncio.gather(task, return_exceptions=True)
401
452
 
402
453
  bot._client.login.assert_awaited_once_with("grace1234")
403
454
  bot._on_ready.assert_awaited_once()
@@ -418,6 +469,39 @@ def test_start_handles_keyboard_interrupt(caplog):
418
469
  bot._client.close.assert_awaited_once()
419
470
 
420
471
 
472
+ @pytest.mark.asyncio
473
+ async def test_on_ready_called_only_once(bot):
474
+ # Prepare
475
+ bot._synced.set()
476
+ bot._on_ready = AsyncMock()
477
+
478
+ # Simulate run
479
+ await bot._wait_until_synced()
480
+ await bot._on_ready()
481
+
482
+ bot._on_ready.assert_awaited_once()
483
+
484
+
485
+ @pytest.mark.asyncio
486
+ async def test_scheduler_starts_after_ready(bot):
487
+ bot._synced.set()
488
+
489
+ order = []
490
+
491
+ async def ready():
492
+ order.append("ready")
493
+
494
+ bot._on_ready = AsyncMock(side_effect=ready)
495
+ bot.scheduler.start = MagicMock(side_effect=lambda: order.append("scheduler"))
496
+
497
+ # Simulate run
498
+ await bot._wait_until_synced()
499
+ await bot._on_ready()
500
+ bot.scheduler.start()
501
+
502
+ assert order == ["ready", "scheduler"]
503
+
504
+
421
505
  @pytest.mark.asyncio
422
506
  async def test_scheduled_task_in_scheduler(bot):
423
507
  @bot.schedule("* * * * *")
@@ -743,3 +827,25 @@ def test_unload_extension_logs_unloading(bot: Bot, loaded_extension: Extension):
743
827
  bot.unload_extension(loaded_extension.name)
744
828
 
745
829
  bot.log.debug.assert_any_call("unloaded extension '%s'", loaded_extension.name)
830
+
831
+
832
+ def test_unload_extension_removes_only_its_jobs(bot: Bot):
833
+ ext_a = Extension(name="a")
834
+ ext_b = Extension(name="b")
835
+
836
+ @ext_a.schedule("* * * * *")
837
+ async def task():
838
+ pass
839
+
840
+ @ext_b.schedule("* * * * *")
841
+ async def task():
842
+ pass
843
+
844
+ bot.load_extension(ext_a)
845
+ bot.load_extension(ext_b)
846
+
847
+ bot.unload_extension("a")
848
+
849
+ job_names = [j.name for j in bot.scheduler.jobs]
850
+
851
+ assert "task" in job_names
File without changes