-
Notifications
You must be signed in to change notification settings - Fork 13
Add dog command with random picture feature #347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
33e9f5e
7a05347
f2f88b2
aec5d29
f0d0381
c8f0bd2
492d1cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| import random | ||
|
|
||
| import discord | ||
| from discord.ext import commands | ||
|
|
||
| from europython_discord.dog.config import DogConfig | ||
| from europython_discord.dog.dogclient import DogClient | ||
|
|
||
| _logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class DogCog(commands.Cog): | ||
| def __init__( | ||
| self, | ||
| bot: commands.Bot, | ||
| config: DogConfig, | ||
| client: DogClient | None = None, | ||
| ) -> None: | ||
| self.bot = bot | ||
| self.config = config | ||
| self._client = client or DogClient() | ||
| _logger.info("Cog 'Dog' has been initialized") | ||
|
|
||
| @commands.hybrid_command(name="dog", description="Get a random dog picture") | ||
| async def dog_command(self, ctx: commands.Context) -> None: | ||
| image_url = await self._client.fetch_random_dog() | ||
| if image_url is None: | ||
| message = random.choice(self.config.error_messages) # noqa: S311 | ||
| await ctx.send(message) | ||
| return | ||
|
|
||
| embed = discord.Embed() | ||
| embed.set_image(url=image_url) | ||
| await ctx.send(embed=embed) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| from pydantic import BaseModel | ||
|
|
||
|
|
||
| class DogConfig(BaseModel): | ||
| error_messages: list[str] = ["404: Dog not found. Have you checked under the couch? 🛋️"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
|
|
||
| import aiohttp | ||
|
|
||
| _logger = logging.getLogger(__name__) | ||
|
|
||
| DOG_API_URL = "https://dog.ceo/api/breeds/image/random" | ||
|
|
||
|
|
||
| class DogClient: | ||
| def __init__(self) -> None: | ||
| self._session = aiohttp.ClientSession() | ||
|
|
||
| async def close(self) -> None: | ||
| await self._session.close() | ||
|
|
||
| async def fetch_random_dog(self) -> str | None: | ||
| try: | ||
| async with self._session.get(DOG_API_URL) as response: | ||
| response.raise_for_status() | ||
| data = await response.json() | ||
| except Exception: | ||
| _logger.exception("Failed to fetch dog image") | ||
| return None | ||
|
|
||
| return data["message"] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a lot of setup and indirection for such simple tests. Can this be simplified?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @NMertsch Thanks! I think the fixture overhead is actually what makes those tests simple - they handle all the boring wiring so each test case is just 3-4 lines of "arrange, act, assert." That said, there aren't any other cog tests in this repo to compare against, so I tried to set some standard that could be followed in the future for more complicated commands as well. For now, I just inlined a stray fixture |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| from unittest.mock import AsyncMock, MagicMock | ||
|
|
||
| import pytest | ||
| from discord.ext import commands | ||
|
|
||
| from europython_discord.dog.cog import DogCog | ||
| from europython_discord.dog.config import DogConfig | ||
| from europython_discord.dog.dogclient import DogClient | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def config() -> DogConfig: | ||
| return DogConfig() | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def bot() -> MagicMock: | ||
| return MagicMock(spec=commands.Bot) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def dog_url() -> str: | ||
| return "https://images.dog.ceo/dog.jpg" | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def mock_client(dog_url: str) -> DogClient: | ||
| client = MagicMock(spec=DogClient) | ||
| client.fetch_random_dog.return_value = dog_url | ||
| return client | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def cog(bot: MagicMock, config: DogConfig, mock_client: DogClient) -> DogCog: | ||
| return DogCog(bot, config, client=mock_client) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def ctx() -> AsyncMock: | ||
| mock = AsyncMock(spec=commands.Context) | ||
| mock.send = AsyncMock() | ||
| return mock | ||
|
|
||
|
|
||
| async def test_dog_command_success(cog: DogCog, ctx: AsyncMock, dog_url: str) -> None: | ||
| await cog.dog_command.callback(cog, ctx) | ||
|
|
||
| ctx.send.assert_awaited_once() | ||
| embed = ctx.send.call_args.kwargs["embed"] | ||
| assert embed.image.url == dog_url | ||
|
|
||
|
|
||
| async def test_dog_command_api_error(cog: DogCog, ctx: AsyncMock, mock_client: DogClient) -> None: | ||
| mock_client.fetch_random_dog.return_value = None | ||
|
|
||
| await cog.dog_command.callback(cog, ctx) | ||
|
|
||
| ctx.send.assert_awaited_once() | ||
| text = ctx.send.call_args.args[0] | ||
|
|
||
| assert text in cog.config.error_messages |
Uh oh!
There was an error while loading. Please reload this page.