Skip to content
Merged
2 changes: 2 additions & 0 deletions matrix/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .help import HelpCommand
from .checks import cooldown
from .room import Room
from .space import Space
from .message import Message
from .extension import Extension

Expand All @@ -28,6 +29,7 @@
"HelpCommand",
"cooldown",
"Room",
"Space",
"Message",
"Extension",
]
10 changes: 4 additions & 6 deletions matrix/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from nio import AsyncClient, Event, MatrixRoom

from .room import Room
from .room import Room, make_room
from .space import Space
from .group import Group
from .config import Config
Expand Down Expand Up @@ -88,7 +88,7 @@ def get_room(self, room_id: str) -> Room | None:

Returns the `Room` object corresponding to `room_id` if it exists in
the client's known rooms. Returns `None` if the room cannot be found.
If the room is a space, a `Space` instance is returned instead.
Returns a typed subclass if the room type is registered (e.g. `Space` for m.space rooms).

## Example

Expand All @@ -100,8 +100,7 @@ def get_room(self, room_id: str) -> Room | None:
```
"""
if matrix_room := self.client.rooms.get(room_id):
room_cls = Space if matrix_room.room_type == "m.space" else Room
return room_cls(matrix_room=matrix_room, client=self.client)
return make_room(matrix_room, self.client)
return None

def get_rooms(self) -> list[Room]:
Expand All @@ -123,8 +122,7 @@ def get_rooms(self) -> list[Room]:
rooms = []

for matrix_room in self.client.rooms.values():
room_cls = Space if matrix_room.room_type == "m.space" else Room
rooms.append(room_cls(matrix_room=matrix_room, client=self.client))
rooms.append(make_room(matrix_room, self.client))

return rooms

Expand Down
16 changes: 16 additions & 0 deletions matrix/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,26 @@
)
from matrix.types import File, Image, Audio, Video

_registry: dict[str, type["Room"]] = {}


def make_room(matrix_room: MatrixRoom, client: AsyncClient) -> "Room":
room_cls = _registry.get(matrix_room.room_type, Room)
return room_cls(matrix_room, client)


class Room:
"""Represents a Matrix room and provides methods to interact with it."""

def __init_subclass__(cls, room_type: str | None = None, **kwargs: Any) -> None:
super().__init_subclass__(**kwargs)
if room_type:
if room_type in _registry:
raise ValueError(
f"Room type '{room_type}' is already registered by {_registry[room_type].__name__}"
)
_registry[room_type] = cls

def __init__(self, matrix_room: MatrixRoom, client: AsyncClient) -> None:
self._matrix_room: MatrixRoom = matrix_room
self._client: AsyncClient = client
Expand Down
30 changes: 27 additions & 3 deletions matrix/space.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
from .room import Room
from matrix.room import Room, make_room


class Space(Room):
pass
class Space(Room, room_type="m.space"):
def get_children(self) -> list[Room]:
"""Return the child rooms and spaces of this space that the bot has joined.

Children the bot has not joined are silently omitted.

## Example

```python
space = bot.get_space("!space123:matrix.org")

for child in space.get_children():
print(child.name)
```
"""
children = []

for room_id in self.children:
matrix_room = self._client.rooms.get(room_id)

if not matrix_room:
continue

children.append(make_room(matrix_room, self._client))

return children
2 changes: 1 addition & 1 deletion tests/test_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from unittest.mock import AsyncMock, MagicMock, patch
from nio import MatrixRoom, RoomMessageText

from matrix.bot import Bot, Config, Extension, Room, Space
from matrix import Bot, Config, Extension, Room, Space
from matrix.errors import (
CheckError,
CommandNotFoundError,
Expand Down
42 changes: 41 additions & 1 deletion tests/test_room.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from unittest.mock import AsyncMock, Mock, MagicMock
from nio import MatrixRoom, Event
from matrix.errors import MatrixError
from matrix.room import Room
from matrix.room import Room, make_room
from matrix.space import Space
from matrix.message import Message


Expand Down Expand Up @@ -387,3 +388,42 @@ def test_room_client_property__expect_async_client(room, client):
def test_room_unknown_attribute__expect_attribute_error(room):
with pytest.raises(AttributeError):
_ = room.nonexistent_attribute


def test_make_room__with_regular_room__expect_room_instance(client):
matrix_room = MatrixRoom(
room_id="!room:example.com", own_user_id="@bot:example.com"
)

result = make_room(matrix_room, client)

assert type(result) is Room


def test_make_room__with_space_room__expect_space_instance(client):
matrix_room = MatrixRoom(
room_id="!space:example.com", own_user_id="@bot:example.com"
)
matrix_room.room_type = "m.space"

result = make_room(matrix_room, client)

assert type(result) is Space


def test_make_room__with_unknown_room_type__expect_room_instance(client):
matrix_room = MatrixRoom(
room_id="!room:example.com", own_user_id="@bot:example.com"
)
matrix_room.room_type = "m.unknown"

result = make_room(matrix_room, client)

assert type(result) is Room


def test_room_duplicate_room_type__expect_value_error():
with pytest.raises(ValueError):

class DuplicateSpace(Room, room_type="m.space"):
pass
94 changes: 94 additions & 0 deletions tests/test_space.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import pytest
from unittest.mock import AsyncMock, Mock
from nio import MatrixRoom
from matrix.room import Room
from matrix.space import Space


@pytest.fixture
def client():
return AsyncMock()


@pytest.fixture
def matrix_space():
space = MatrixRoom(room_id="!space:example.com", own_user_id="@bot:example.com")
space.name = "Test Space"
space.room_type = "m.space"
return space


@pytest.fixture
def space(matrix_space, client):
return Space(matrix_space, client)


def test_get_children__with_room_child__expect_room_instance(
space, matrix_space, client
):
child = MatrixRoom(room_id="!child:example.com", own_user_id="@bot:example.com")
child.name = "Child Room"
matrix_space.children = {"!child:example.com"}
client.rooms = {"!child:example.com": child}

result = space.get_children()

assert len(result) == 1
assert type(result[0]) is Room
assert result[0].room_id == "!child:example.com"


def test_get_children__with_space_child__expect_space_instance(
space, matrix_space, client
):
child = MatrixRoom(room_id="!subspace:example.com", own_user_id="@bot:example.com")
child.name = "Sub Space"
child.room_type = "m.space"
matrix_space.children = {"!subspace:example.com"}
client.rooms = {"!subspace:example.com": child}

result = space.get_children()

assert len(result) == 1
assert isinstance(result[0], Space)
assert result[0].room_id == "!subspace:example.com"


def test_get_children__with_unjoined_child__expect_child_skipped(
space, matrix_space, client
):
matrix_space.children = {"!unknown:example.com"}
client.rooms = {}

result = space.get_children()

assert result == []


def test_get_children__with_no_children__expect_empty_list(space, matrix_space, client):
matrix_space.children = set()
client.rooms = {}

result = space.get_children()

assert result == []


def test_get_children__with_mixed_children__expect_correct_types(
space, matrix_space, client
):
room_child = MatrixRoom(room_id="!room:example.com", own_user_id="@bot:example.com")
space_child = MatrixRoom(room_id="!sub:example.com", own_user_id="@bot:example.com")
space_child.room_type = "m.space"
matrix_space.children = {"!room:example.com", "!sub:example.com"}
client.rooms = {
"!room:example.com": room_child,
"!sub:example.com": space_child,
}

result = space.get_children()

assert len(result) == 2
types = {r.room_id: type(r) for r in result}
assert types["!room:example.com"] is Room
assert types["!sub:example.com"] is Space
Loading