matrix-python 1.4.8a0__tar.gz → 1.4.9a0__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 (62) hide show
  1. {matrix_python-1.4.8a0/matrix_python.egg-info → matrix_python-1.4.9a0}/PKG-INFO +1 -1
  2. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/_version.py +3 -3
  3. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/extension.py +17 -9
  4. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/protocols.py +4 -0
  5. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/registry.py +74 -11
  6. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0/matrix_python.egg-info}/PKG-INFO +1 -1
  7. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/tests/test_extension.py +11 -4
  8. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/tests/test_registry.py +63 -1
  9. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/.github/dependabot.yml +0 -0
  10. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/.github/workflows/CODEOWNERS +0 -0
  11. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/.github/workflows/codeql.yml +0 -0
  12. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/.github/workflows/publish.yml +0 -0
  13. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/.github/workflows/scorecard.yml +0 -0
  14. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/.github/workflows/tests.yml +0 -0
  15. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/.gitignore +0 -0
  16. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/CODE_OF_CONDUCT.md +0 -0
  17. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/CONTRIBUTING.md +0 -0
  18. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/LICENSE +0 -0
  19. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/README.md +0 -0
  20. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/examples/README.md +0 -0
  21. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/examples/checks.py +0 -0
  22. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/examples/config.yaml +0 -0
  23. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/examples/cooldown.py +0 -0
  24. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/examples/error_handling.py +0 -0
  25. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/examples/extension.py +0 -0
  26. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/examples/ping.py +0 -0
  27. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/examples/reaction.py +0 -0
  28. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/examples/scheduler.py +0 -0
  29. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/__init__.py +0 -0
  30. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/bot.py +0 -0
  31. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/checks.py +0 -0
  32. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/command.py +0 -0
  33. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/config.py +0 -0
  34. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/content.py +0 -0
  35. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/context.py +0 -0
  36. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/errors.py +0 -0
  37. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/group.py +0 -0
  38. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/help/__init__.py +0 -0
  39. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/help/help_command.py +0 -0
  40. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/help/pagination.py +0 -0
  41. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/message.py +0 -0
  42. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/py.typed +0 -0
  43. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/room.py +0 -0
  44. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/scheduler.py +0 -0
  45. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix/types.py +0 -0
  46. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix_python.egg-info/SOURCES.txt +0 -0
  47. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix_python.egg-info/dependency_links.txt +0 -0
  48. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix_python.egg-info/requires.txt +0 -0
  49. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/matrix_python.egg-info/top_level.txt +0 -0
  50. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/mypy.ini +0 -0
  51. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/pyproject.toml +0 -0
  52. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/setup.cfg +0 -0
  53. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/tests/help/test_default_help_command.py +0 -0
  54. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/tests/help/test_help_command.py +0 -0
  55. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/tests/help/test_pagination.py +0 -0
  56. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/tests/test_bot.py +0 -0
  57. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/tests/test_command.py +0 -0
  58. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/tests/test_config.py +0 -0
  59. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/tests/test_context.py +0 -0
  60. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/tests/test_group.py +0 -0
  61. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/tests/test_message.py +0 -0
  62. {matrix_python-1.4.8a0 → matrix_python-1.4.9a0}/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.8a0
3
+ Version: 1.4.9a0
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.8a0'
22
- __version_tuple__ = version_tuple = (1, 4, 8, 'a0')
21
+ __version__ = version = '1.4.9a0'
22
+ __version_tuple__ = version_tuple = (1, 4, 9, 'a0')
23
23
 
24
- __commit_id__ = commit_id = 'g16bd59854'
24
+ __commit_id__ = commit_id = 'g5f8d673e3'
@@ -1,21 +1,31 @@
1
1
  import inspect
2
2
  import logging
3
- from typing import Callable, Optional
3
+ from typing import Callable
4
4
 
5
5
  from matrix.protocols import BotLike
6
6
  from matrix.registry import Registry
7
+ from matrix.config import Config
7
8
  from matrix.room import Room
8
9
 
9
10
  logger = logging.getLogger(__name__)
10
11
 
11
12
 
12
13
  class Extension(Registry):
13
- def __init__(self, name: str, prefix: Optional[str] = None) -> None:
14
+ def __init__(self, name: str, prefix: str | None = None) -> None:
14
15
  super().__init__(name, prefix=prefix)
15
16
 
16
- self.bot: Optional[BotLike] = None
17
- self._on_load: Optional[Callable] = None
18
- self._on_unload: Optional[Callable] = None
17
+ self._bot: BotLike | None = None
18
+ self._on_load: Callable | None = None
19
+ self._on_unload: Callable | None = None
20
+
21
+ @property
22
+ def bot(self) -> BotLike:
23
+ assert self._bot, "Extension is not loaded"
24
+ return self._bot
25
+
26
+ @property
27
+ def config(self) -> Config:
28
+ return self.bot.config
19
29
 
20
30
  def get_room(self, room_id: str) -> Room | None:
21
31
  """Retrieve a `Room` instance by its Matrix room ID.
@@ -31,12 +41,10 @@ class Extension(Registry):
31
41
  print(room.name)
32
42
  ```
33
43
  """
34
- if self.bot is None:
35
- raise RuntimeError("Extension is not loaded")
36
44
  return self.bot.get_room(room_id)
37
45
 
38
46
  def load(self, bot: BotLike) -> None:
39
- self.bot = bot
47
+ self._bot = bot
40
48
 
41
49
  if self._on_load:
42
50
  self._on_load()
@@ -59,7 +67,7 @@ class Extension(Registry):
59
67
  return func
60
68
 
61
69
  def unload(self) -> None:
62
- self.bot = None
70
+ self._bot = None
63
71
 
64
72
  if self._on_unload:
65
73
  self._on_unload()
@@ -1,9 +1,13 @@
1
1
  from typing import Protocol
2
2
 
3
+ from matrix.config import Config
3
4
  from matrix.room import Room
4
5
 
5
6
 
6
7
  class BotLike(Protocol):
7
8
  prefix: str | None
8
9
 
10
+ @property
11
+ def config(self) -> Config: ...
12
+
9
13
  def get_room(self, room_id: str) -> Room | None: ...
@@ -2,7 +2,20 @@ import inspect
2
2
  import logging
3
3
 
4
4
  from collections import defaultdict
5
- from typing import Any, Callable, Coroutine, Optional, Type, Union, Dict, List
5
+ from typing import (
6
+ TypeVar,
7
+ Any,
8
+ Callable,
9
+ Coroutine,
10
+ Literal,
11
+ Optional,
12
+ Type,
13
+ Union,
14
+ Dict,
15
+ List,
16
+ cast,
17
+ overload,
18
+ )
6
19
 
7
20
  from nio import (
8
21
  Event,
@@ -25,6 +38,8 @@ GroupCallable = Callable[[Callable[..., Coroutine[Any, Any, Any]]], Group]
25
38
  ErrorCallback = Callable[[Exception], Coroutine]
26
39
  CommandErrorCallback = Callable[[Context, Exception], Coroutine[Any, Any, Any]]
27
40
 
41
+ F = TypeVar("F", ErrorCallback, CommandErrorCallback)
42
+
28
43
 
29
44
  class Registry:
30
45
  """
@@ -355,43 +370,91 @@ class Registry:
355
370
 
356
371
  return wrapper
357
372
 
373
+ @overload
358
374
  def error(
359
- self, exception: Optional[type[Exception]] = None
360
- ) -> Callable[[ErrorCallback], ErrorCallback]:
375
+ self,
376
+ exception: Optional[type[Exception]] = None,
377
+ *,
378
+ context: Literal[True],
379
+ ) -> Callable[[CommandErrorCallback], CommandErrorCallback]: ...
380
+
381
+ @overload
382
+ def error(
383
+ self,
384
+ exception: Optional[type[Exception]] = None,
385
+ *,
386
+ context: Literal[False] = ...,
387
+ ) -> Callable[[ErrorCallback], ErrorCallback]: ...
388
+
389
+ def error(
390
+ self,
391
+ exception: Optional[type[Exception]] = None,
392
+ *,
393
+ context: bool = False,
394
+ ) -> Union[
395
+ Callable[[ErrorCallback], ErrorCallback],
396
+ Callable[[CommandErrorCallback], CommandErrorCallback],
397
+ ]:
361
398
  """Decorator to register an error handler.
362
399
 
363
400
  If an exception type is provided, the handler is only invoked for
364
401
  that specific exception. If omitted, the handler acts as a generic
365
402
  fallback for any unhandled error.
366
403
 
404
+ Set ``context=True`` to receive the command context alongside the error,
405
+ useful for command-specific errors where you want to reply to the user.
406
+
367
407
  ## Example
368
408
 
369
409
  ```python
370
410
  @bot.error(ValueError)
371
411
  async def on_value_error(error):
372
- await room.send(f"Bad value: {error}")
412
+ pass
373
413
 
374
414
  @bot.error()
375
415
  async def on_any_error(error):
376
- await room.send(f"Something went wrong: {error}")
416
+ pass
417
+
418
+ @bot.error(CommandNotFoundError, context=True)
419
+ async def on_command_not_found(ctx, error):
420
+ await ctx.reply("Command not found!")
377
421
  ```
378
422
  """
379
423
 
380
- if not exception:
381
- exception = Exception
382
-
383
- def wrapper(func: ErrorCallback) -> ErrorCallback:
424
+ def wrapper(
425
+ func: F,
426
+ ) -> F:
384
427
  if not inspect.iscoroutinefunction(func):
385
428
  raise TypeError("Error handlers must be coroutines")
386
429
 
387
- self._error_handlers[exception] = func
430
+ if context:
431
+ self._register_command_error(
432
+ cast(CommandErrorCallback, func), exception
433
+ )
434
+ else:
435
+ self._register_error(cast(ErrorCallback, func), exception)
388
436
 
389
437
  logger.debug(
390
438
  "registered error handler '%s' on %s",
391
439
  func.__name__,
392
440
  type(self).__name__,
393
441
  )
394
-
395
442
  return func
396
443
 
397
444
  return wrapper
445
+
446
+ def _register_error(
447
+ self, func: ErrorCallback, exception: Optional[type[Exception]] = None
448
+ ) -> None:
449
+ if not exception:
450
+ exception = Exception
451
+ self._error_handlers[exception] = func
452
+
453
+ def _register_command_error(
454
+ self,
455
+ func: CommandErrorCallback,
456
+ exception: Optional[type[Exception]] = None,
457
+ ) -> None:
458
+ if not exception:
459
+ exception = Exception
460
+ self._command_error_handlers[exception] = func
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrix-python
3
- Version: 1.4.8a0
3
+ Version: 1.4.9a0
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>
@@ -3,6 +3,7 @@ import pytest
3
3
  from unittest.mock import MagicMock
4
4
  from typing import Optional
5
5
 
6
+ from matrix.config import Config
6
7
  from matrix.extension import Extension
7
8
  from matrix.room import Room
8
9
 
@@ -10,6 +11,10 @@ from matrix.room import Room
10
11
  class MockBot:
11
12
  prefix: str = "!"
12
13
 
14
+ @property
15
+ def config(self) -> Config:
16
+ return MagicMock(spec=Config)
17
+
13
18
  def __init__(self, room: Optional[Room] = None) -> None:
14
19
  self.get_room = MagicMock(return_value=room or MagicMock(spec=Room))
15
20
 
@@ -41,7 +46,8 @@ def test_init_with_name_only__expect_prefix_is_none():
41
46
 
42
47
 
43
48
  def test_init__expect_bot_is_none(extension: Extension):
44
- assert extension.bot is None
49
+ with pytest.raises(AssertionError):
50
+ _ = extension.bot
45
51
 
46
52
 
47
53
  def test_init__expect_on_load_is_none(extension: Extension):
@@ -190,7 +196,8 @@ def test_unload__expect_bot_cleared(extension: Extension, bot: MockBot):
190
196
  extension.load(bot)
191
197
  extension.unload()
192
198
 
193
- assert extension.bot is None
199
+ with pytest.raises(AssertionError):
200
+ _ = extension.bot
194
201
 
195
202
 
196
203
  def test_unload_with_registered_handler__expect_handler_called(
@@ -217,7 +224,7 @@ def test_unload_with_no_handler__expect_no_error(extension: Extension, bot: Mock
217
224
 
218
225
 
219
226
  def test_get_room_before_load__expect_runtime_error(extension: Extension):
220
- with pytest.raises(RuntimeError, match="Extension is not loaded"):
227
+ with pytest.raises(AssertionError):
221
228
  extension.get_room("!room:example.com")
222
229
 
223
230
 
@@ -241,5 +248,5 @@ def test_get_room_after_unload__expect_runtime_error(
241
248
  extension.load(bot)
242
249
  extension.unload()
243
250
 
244
- with pytest.raises(RuntimeError, match="Extension is not loaded"):
251
+ with pytest.raises(AssertionError):
245
252
  extension.get_room("!room:example.com")
@@ -5,7 +5,7 @@ from nio import RoomMessageText, RoomMemberEvent, TypingNoticeEvent, ReactionEve
5
5
  from matrix.registry import Registry
6
6
  from matrix.command import Command
7
7
  from matrix.group import Group
8
- from matrix.errors import AlreadyRegisteredError
8
+ from matrix.errors import AlreadyRegisteredError, CommandNotFoundError, CheckError
9
9
 
10
10
 
11
11
  @pytest.fixture
@@ -334,6 +334,68 @@ def test_register_error_handler_overwrites_previous_handler__expect_latest_handl
334
334
  assert registry._error_handlers[ValueError] is second_handler
335
335
 
336
336
 
337
+ def test_register_command_error_handler_with_exception_type__expect_handler_in_dict(
338
+ registry: Registry,
339
+ ):
340
+ @registry.error(CommandNotFoundError, context=True)
341
+ async def on_command_not_found(ctx, error):
342
+ pass
343
+
344
+ assert (
345
+ registry._command_error_handlers[CommandNotFoundError] is on_command_not_found
346
+ )
347
+
348
+
349
+ def test_register_command_error_handler_with_non_coroutine__expect_type_error(
350
+ registry: Registry,
351
+ ):
352
+ with pytest.raises(TypeError):
353
+
354
+ @registry.error(CommandNotFoundError, context=True)
355
+ def sync_handler(ctx, error):
356
+ pass
357
+
358
+
359
+ def test_register_multiple_command_error_handlers__expect_all_in_dict(
360
+ registry: Registry,
361
+ ):
362
+ @registry.error(CommandNotFoundError, context=True)
363
+ async def on_command_not_found(ctx, error):
364
+ pass
365
+
366
+ @registry.error(CheckError, context=True)
367
+ async def on_check_error(ctx, error):
368
+ pass
369
+
370
+ assert CommandNotFoundError in registry._command_error_handlers
371
+ assert CheckError in registry._command_error_handlers
372
+
373
+
374
+ def test_register_command_error_handler_overwrites_previous__expect_latest_handler(
375
+ registry: Registry,
376
+ ):
377
+ @registry.error(CommandNotFoundError, context=True)
378
+ async def first_handler(ctx, error):
379
+ pass
380
+
381
+ @registry.error(CommandNotFoundError, context=True)
382
+ async def second_handler(ctx, error):
383
+ pass
384
+
385
+ assert registry._command_error_handlers[CommandNotFoundError] is second_handler
386
+
387
+
388
+ def test_register_error_with_context_false__expect_handler_in_error_handlers(
389
+ registry: Registry,
390
+ ):
391
+ @registry.error(ValueError, context=False)
392
+ async def on_value_error(error):
393
+ pass
394
+
395
+ assert registry._error_handlers[ValueError] is on_value_error
396
+ assert ValueError not in registry._command_error_handlers
397
+
398
+
337
399
  def test_commands_property_with_empty_registry__expect_empty_dict(registry: Registry):
338
400
  assert registry.commands == {}
339
401
 
File without changes