From 67055c06b9446ca770cf7ff63ca254a80c1d8045 Mon Sep 17 00:00:00 2001 From: Brian LaFond <52360893+iroxusux@users.noreply.github.com> Date: Sat, 12 Apr 2025 20:35:00 -0400 Subject: [PATCH 1/4] again, fixing imports as the 'tests' were rebased per suggestion. --- tests/types/.testenv => .testenv | 0 .vscode/settings.json | 12 +++ PyDiscoBot/__init__.py | 25 +++--- PyDiscoBot/bot.py | 14 ++-- PyDiscoBot/embed_frames/__init__.py | 16 ++-- PyDiscoBot/embed_frames/admin.py | 17 ++-- PyDiscoBot/embed_frames/frame.py | 6 +- PyDiscoBot/embed_frames/notification.py | 2 +- .../embed_frames/test_frames.py | 13 +-- PyDiscoBot/services/cmds/__init__.py | 15 ++-- PyDiscoBot/services/cmds/clearchannel.py | 4 +- PyDiscoBot/services/cmds/datetounix.py | 10 +-- PyDiscoBot/services/cmds/echo.py | 4 +- PyDiscoBot/services/cmds/help.py | 4 +- PyDiscoBot/services/cmds/sync.py | 4 +- .../services/cmds/test_commands.py | 31 +++---- PyDiscoBot/tasks/__init__.py | 2 + PyDiscoBot/tasks/admin.py | 14 ++-- .../tasks/test_tasks.py | 4 +- tests/types/bot.py => PyDiscoBot/test_bot.py | 4 +- PyDiscoBot/types/__init__.py | 42 +++++----- PyDiscoBot/types/cmd.py | 6 +- PyDiscoBot/types/mock/__init__.py | 25 ++++++ .../types/mock}/mock_bot.py | 18 ++--- .../types/{unittest => mock}/mock_channel.py | 0 .../mock_connection_state.py | 0 .../types/{unittest => mock}/mock_guild.py | 0 .../{unittest => mock}/mock_interaction.py | 0 .../types/{unittest => mock}/mock_member.py | 0 .../types/{unittest => mock}/mock_message.py | 0 .../types/{unittest => mock}/mock_user.py | 0 PyDiscoBot/types/task.py | 81 ++++++++++++++----- PyDiscoBot/types/unittest/__init__.py | 19 ----- tests/__init__.py | 12 --- tests/embed_frames/__init__.py | 10 --- tests/services/__init__.py | 10 --- tests/services/cmds/__init__.py | 10 --- tests/tasks/__init__.py | 10 --- tests/types/__init__.py | 12 --- 39 files changed, 228 insertions(+), 228 deletions(-) rename tests/types/.testenv => .testenv (100%) create mode 100644 .vscode/settings.json rename tests/embed_frames/frames.py => PyDiscoBot/embed_frames/test_frames.py (57%) rename tests/services/cmds/commands.py => PyDiscoBot/services/cmds/test_commands.py (79%) rename tests/tasks/tasks.py => PyDiscoBot/tasks/test_tasks.py (86%) rename tests/types/bot.py => PyDiscoBot/test_bot.py (83%) create mode 100644 PyDiscoBot/types/mock/__init__.py rename {tests/types => PyDiscoBot/types/mock}/mock_bot.py (68%) rename PyDiscoBot/types/{unittest => mock}/mock_channel.py (100%) rename PyDiscoBot/types/{unittest => mock}/mock_connection_state.py (100%) rename PyDiscoBot/types/{unittest => mock}/mock_guild.py (100%) rename PyDiscoBot/types/{unittest => mock}/mock_interaction.py (100%) rename PyDiscoBot/types/{unittest => mock}/mock_member.py (100%) rename PyDiscoBot/types/{unittest => mock}/mock_message.py (100%) rename PyDiscoBot/types/{unittest => mock}/mock_user.py (100%) delete mode 100644 PyDiscoBot/types/unittest/__init__.py delete mode 100644 tests/__init__.py delete mode 100644 tests/embed_frames/__init__.py delete mode 100644 tests/services/__init__.py delete mode 100644 tests/services/cmds/__init__.py delete mode 100644 tests/tasks/__init__.py delete mode 100644 tests/types/__init__.py diff --git a/tests/types/.testenv b/.testenv similarity index 100% rename from tests/types/.testenv rename to .testenv diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..33c6d5e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "python.testing.unittestArgs": [ + "-v", + "-s", + "./pydiscobot", + "-p", + "test_*.py" + ], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true + +} \ No newline at end of file diff --git a/PyDiscoBot/__init__.py b/PyDiscoBot/__init__.py index 46969e9..db812b2 100644 --- a/PyDiscoBot/__init__.py +++ b/PyDiscoBot/__init__.py @@ -1,23 +1,18 @@ """PyDiscoBot - a bot by irox """ -from .bot import Bot -from .types import BotNotLoaded, EmbedField, IllegalChannel, InsufficientPrivilege -from .types import ReportableError, Pagination, InteractionPagination -from .services import const, cmds from . import embed_frames +from . import services +from . import tasks +from . import types +from . import bot -__version__ = "1.1.3" + +__version__ = "1.1.4" __all__ = ( - "Bot", - 'ReportableError', - 'IllegalChannel', - 'InsufficientPrivilege', - 'BotNotLoaded', - "Pagination", - "InteractionPagination", - 'const', - 'cmds', + 'bot', 'embed_frames', - 'EmbedField', + 'services', + 'tasks', + 'types', ) diff --git a/PyDiscoBot/bot.py b/PyDiscoBot/bot.py index 1dec46e..a08bb98 100644 --- a/PyDiscoBot/bot.py +++ b/PyDiscoBot/bot.py @@ -10,13 +10,13 @@ import discord from discord.ext import commands as disco_commands from discord import app_commands -from .embed_frames import get_notification -from .services import const, channels -from .services.cmds import Commands +from .embed_frames.notification import get_notification +from .services import cmds, const, channels from .services.log import logger from .tasks.admin import AdminTask -from .types import AdminInfo, IllegalChannel, BotNotLoaded, ReportableError -from .types import InsufficientPrivilege, Tasker +from .types.admin_info import AdminInfo +from .types.err import InsufficientPrivilege, IllegalChannel, BotNotLoaded, ReportableError +from .types.tasker import Tasker class Bot(disco_commands.Bot): @@ -42,7 +42,7 @@ class Bot(disco_commands.Bot): command_cogs: list[:class:`discord.ext.commands.Cog`] A list of commands to append to this bot when initializing. During the __init__ call, this list will be appended asyncronously to the bot's cogs. - After modifying what commands a bot has access to, or the parameters of the commands, + After modifying what commands a bot has access to, or the parameters of the commands, a 'sync' command will be required to sync the bot tree. Examples @@ -82,7 +82,7 @@ def __init__(self, self._tasker = Tasker() self._tasker.append(AdminTask(self)) - command_cogs.extend(Commands) + command_cogs.extend(cmds.Commands) for cog in command_cogs: asyncio.run(self.add_cog(cog(self))) diff --git a/PyDiscoBot/embed_frames/__init__.py b/PyDiscoBot/embed_frames/__init__.py index 9966ef1..09e4431 100644 --- a/PyDiscoBot/embed_frames/__init__.py +++ b/PyDiscoBot/embed_frames/__init__.py @@ -1,14 +1,16 @@ """frames module for discord embeds """ -from .frame import get_frame -from .admin import get_admin_frame -from .notification import get_notification +from . import admin +from . import frame +from . import notification +from . import test_frames -__version__ = '1.1.2' +__version__ = '1.1.4' __all__ = ( - 'get_frame', - 'get_admin_frame', - 'get_notification', + 'admin', + 'frame', + 'notification', + 'test_frames', ) diff --git a/PyDiscoBot/embed_frames/admin.py b/PyDiscoBot/embed_frames/admin.py index 76220d7..01365bd 100644 --- a/PyDiscoBot/embed_frames/admin.py +++ b/PyDiscoBot/embed_frames/admin.py @@ -3,12 +3,12 @@ from __future__ import annotations import discord -from pydiscobot.services.const import DEF_TIME_FORMAT -from pydiscobot.types import AdminInfo, EmbedField +from pydiscobot.services import const +from pydiscobot.types import admin_info, embed_field from .frame import get_frame -def get_admin_frame(info: AdminInfo) -> discord.Embed: +def get_admin_frame(info: admin_info.AdminInfo) -> discord.Embed: """Get built-in :class:`discord.Embed` (or 'frame') to display :class:`Bot` :class:`AdminInfo`. .. ------------------------------------------------------------ @@ -41,11 +41,12 @@ async def post_admin_info(self, embed = get_frame('**Bot Info**', 'For help, type `/help`', [ - EmbedField('Version', f"`{info.version}`"), - EmbedField('Boot Time', f"`{info.boot_time.strftime(DEF_TIME_FORMAT)}`", True), - EmbedField('Current Tick', f"`{info.current_tick}`"), - EmbedField('Last Time', f"`{info.last_time}`"), - EmbedField('Cycle Time', f"`{info.cycle_time}`s", True) + embed_field.EmbedField('Version', f"`{info.version}`"), + embed_field.EmbedField( + 'Boot Time', f"`{info.boot_time.strftime(const.DEF_TIME_FORMAT)}`", True), + embed_field.EmbedField('Current Tick', f"`{info.current_tick}`"), + embed_field.EmbedField('Last Time', f"`{info.last_time}`"), + embed_field.EmbedField('Cycle Time', f"`{info.cycle_time}`s", True) ]) return embed diff --git a/PyDiscoBot/embed_frames/frame.py b/PyDiscoBot/embed_frames/frame.py index c26f5eb..46ca7cd 100644 --- a/PyDiscoBot/embed_frames/frame.py +++ b/PyDiscoBot/embed_frames/frame.py @@ -6,8 +6,8 @@ import datetime from typing import Optional, Union import discord -from pydiscobot.services.const import DEF_TIME_FORMAT -from pydiscobot.types import EmbedField +from pydiscobot.services import const +from pydiscobot.types.embed_field import EmbedField def get_frame(title: Optional[str] = None, @@ -103,7 +103,7 @@ async def send_notification(self, color=discord.Color.from_str(str(color)), title=title, description=descr) - .set_footer(text=f'Generated: {datetime.datetime.now().strftime(DEF_TIME_FORMAT)}') + .set_footer(text=f'Generated: {datetime.datetime.now().strftime(const.DEF_TIME_FORMAT)}') .set_thumbnail(url=thumbnail)) if not fields: diff --git a/PyDiscoBot/embed_frames/notification.py b/PyDiscoBot/embed_frames/notification.py index ad3d813..1dfe40e 100644 --- a/PyDiscoBot/embed_frames/notification.py +++ b/PyDiscoBot/embed_frames/notification.py @@ -3,7 +3,7 @@ from __future__ import annotations import discord -from pydiscobot.types import EmbedField +from pydiscobot.types.embed_field import EmbedField from .frame import get_frame diff --git a/tests/embed_frames/frames.py b/PyDiscoBot/embed_frames/test_frames.py similarity index 57% rename from tests/embed_frames/frames.py rename to PyDiscoBot/embed_frames/test_frames.py index fa16e34..25e7b60 100644 --- a/tests/embed_frames/frames.py +++ b/PyDiscoBot/embed_frames/test_frames.py @@ -1,9 +1,11 @@ """test frames functionality for pydisco bot """ +from __future__ import annotations + import unittest import discord -from pydiscobot import embed_frames -from pydiscobot.types import AdminInfo +from pydiscobot.embed_frames import frame, admin, notification +from pydiscobot.types import admin_info class TestFrames(unittest.TestCase): @@ -13,14 +15,15 @@ class TestFrames(unittest.TestCase): def test_get_frame(self): """test bot can compile and receive default frame """ - self.assertTrue(isinstance(embed_frames.get_frame(), discord.Embed)) + frm = frame.get_frame() + self.assertTrue(isinstance(frm, discord.Embed)) def test_get_admin_frame(self): """test bot can compile and receive admin frame """ - self.assertTrue(isinstance(embed_frames.get_admin_frame(AdminInfo()), discord.Embed)) + self.assertTrue(isinstance(admin.get_admin_frame(admin_info.AdminInfo()), discord.Embed)) def test_get_notification_frame(self): """test bot can compile and receive notification frame """ - self.assertTrue(isinstance(embed_frames.get_notification('bing bong'), discord.Embed)) + self.assertTrue(isinstance(notification.get_notification('bing bong'), discord.Embed)) diff --git a/PyDiscoBot/services/cmds/__init__.py b/PyDiscoBot/services/cmds/__init__.py index dff865f..62b7194 100644 --- a/PyDiscoBot/services/cmds/__init__.py +++ b/PyDiscoBot/services/cmds/__init__.py @@ -5,14 +5,9 @@ from .echo import Echo from .help import Help from .sync import Sync +from . import test_commands -__version__ = '1.1.2' - -__all__ = ( - 'Commands', -) - Commands = [ ClearChannel, DateToUnix, @@ -20,3 +15,11 @@ Help, Sync, ] + + +__version__ = '1.1.4' + +__all__ = ( + 'Commands', + 'test_commands', +) diff --git a/PyDiscoBot/services/cmds/clearchannel.py b/PyDiscoBot/services/cmds/clearchannel.py index 4247c9f..aa1cedc 100644 --- a/PyDiscoBot/services/cmds/clearchannel.py +++ b/PyDiscoBot/services/cmds/clearchannel.py @@ -6,10 +6,10 @@ import discord from discord import app_commands from pydiscobot.services.channels import clear_messages -from pydiscobot.types import Cmd +from pydiscobot.types import cmd -class ClearChannel(Cmd): +class ClearChannel(cmd.Cmd): """ClearChannel command cog. """ diff --git a/PyDiscoBot/services/cmds/datetounix.py b/PyDiscoBot/services/cmds/datetounix.py index 9a4d7b2..9182d70 100644 --- a/PyDiscoBot/services/cmds/datetounix.py +++ b/PyDiscoBot/services/cmds/datetounix.py @@ -8,7 +8,7 @@ import discord from discord import app_commands from pydiscobot.embed_frames import frame -from pydiscobot.types import Cmd, EmbedField +from pydiscobot.types import cmd, embed_field ERR = '\n'.join([ @@ -18,7 +18,7 @@ ]) -class DateToUnix(Cmd): +class DateToUnix(cmd.Cmd): """convert date to unix string date is placed into embed, broken apart, so it can be copied and pasted by the user. """ @@ -77,9 +77,9 @@ async def datetounix(self, d = datetime.datetime.strptime(_d, '%y/%m/%d %H:%M:%S') unix_time = time.mktime(d.timetuple()) - f = [EmbedField('date ->', - f'template: <`t:{str(int(unix_time))}:F`>\n' - f'')] + f = [embed_field.EmbedField('date ->', + f'template: <`t:{str(int(unix_time))}:F`>\n' + f'')] await interaction.response.send_message(embed=frame.get_frame('**Date To Unix**', '', f)) diff --git a/PyDiscoBot/services/cmds/echo.py b/PyDiscoBot/services/cmds/echo.py index 620793b..eaedffd 100644 --- a/PyDiscoBot/services/cmds/echo.py +++ b/PyDiscoBot/services/cmds/echo.py @@ -6,10 +6,10 @@ import discord from discord import app_commands -from pydiscobot.types import Cmd +from pydiscobot.types import cmd -class Echo(Cmd): +class Echo(cmd.Cmd): """echo string """ diff --git a/PyDiscoBot/services/cmds/help.py b/PyDiscoBot/services/cmds/help.py index 041a63b..f04acc2 100644 --- a/PyDiscoBot/services/cmds/help.py +++ b/PyDiscoBot/services/cmds/help.py @@ -5,12 +5,12 @@ import discord from discord import app_commands from pydiscobot.embed_frames import frame -from pydiscobot.types import Cmd +from pydiscobot.types import cmd MSG = '**help**', 'If you have an issue, please reach out to `irox_rl`.' -class Help(Cmd): +class Help(cmd.Cmd): """help """ diff --git a/PyDiscoBot/services/cmds/sync.py b/PyDiscoBot/services/cmds/sync.py index c83d816..294e0ad 100644 --- a/PyDiscoBot/services/cmds/sync.py +++ b/PyDiscoBot/services/cmds/sync.py @@ -7,10 +7,10 @@ import discord from discord import app_commands from discord.ext import commands -from pydiscobot.types import Cmd +from pydiscobot.types import cmd -class Sync(Cmd): +class Sync(cmd.Cmd): """sync commands """ diff --git a/tests/services/cmds/commands.py b/PyDiscoBot/services/cmds/test_commands.py similarity index 79% rename from tests/services/cmds/commands.py rename to PyDiscoBot/services/cmds/test_commands.py index 212ea5d..2ca3892 100644 --- a/tests/services/cmds/commands.py +++ b/PyDiscoBot/services/cmds/test_commands.py @@ -1,10 +1,11 @@ """test commands for pydisco bot """ +from __future__ import annotations + import asyncio import unittest import discord -from tests.types import MockBot -from pydiscobot.types.unittest import MockInteraction +from pydiscobot.types.mock import mock_bot, mock_interaction from pydiscobot.services.cmds.datetounix import ERR @@ -15,7 +16,7 @@ class TestCommands(unittest.TestCase): def test_clearchannel(self): """test clearchannel command """ - bot = MockBot.as_ready() + bot = mock_bot.MockBot.as_ready() cog = bot.cogs['ClearChannel'] self.assertIsNotNone(cog) @@ -25,13 +26,13 @@ def test_clearchannel(self): # do it asyncio.run(cmd.callback(self=cmd, - interaction=MockInteraction(), + interaction=mock_interaction.MockInteraction(), message_count=10)) # check upper level err with self.assertRaises(ValueError) as context: asyncio.run(cmd.callback(self=cmd, - interaction=MockInteraction(), + interaction=mock_interaction.MockInteraction(), message_count=250)) self.assertTrue(isinstance(context.exception, ValueError)) @@ -39,7 +40,7 @@ def test_clearchannel(self): # check lwr level err with self.assertRaises(ValueError) as context: asyncio.run(cmd.callback(self=cmd, - interaction=MockInteraction(), + interaction=mock_interaction.MockInteraction(), message_count=0)) self.assertTrue(isinstance(context.exception, ValueError)) @@ -47,7 +48,7 @@ def test_clearchannel(self): def test_datetounix(self): """test datetounix command """ - bot = MockBot.as_ready() + bot = mock_bot.MockBot.as_ready() cog = bot.cogs['DateToUnix'] self.assertIsNotNone(cog) @@ -64,7 +65,7 @@ async def test_callback_fail(embed): # create dummy interaction # run to validate command sends ERR asyncio.run(cmd.callback(self=cmd, - interaction=MockInteraction(test_callback_fail), + interaction=mock_interaction.MockInteraction(test_callback_fail), year='a', month='b', day='c', @@ -74,7 +75,7 @@ async def test_callback_fail(embed): # if we know it errs, lets check that it DOESNT err asyncio.run(cmd.callback(self=cmd, - interaction=MockInteraction(test_callback_succeed), + interaction=mock_interaction.MockInteraction(test_callback_succeed), year='25', month='8', day='16', @@ -85,7 +86,7 @@ async def test_callback_fail(embed): def test_echo(self): """test echo command """ - bot = MockBot.as_ready() + bot = mock_bot.MockBot.as_ready() echo_cog = bot.cogs['Echo'] self.assertIsNotNone(echo_cog) @@ -102,13 +103,13 @@ async def test_callback(message): # create dummy interaction # run the command to validate at least an echo works with a built bot asyncio.run(echo_cmd.callback(self=echo_cmd, - interaction=MockInteraction(test_callback), + interaction=mock_interaction.MockInteraction(test_callback), message=sent_value)) def test_help(self): """test help command """ - bot = MockBot.as_ready() + bot = mock_bot.MockBot.as_ready() cog = bot.cogs['Help'] self.assertIsNotNone(cog) @@ -122,12 +123,12 @@ async def test_callback(embed): # create dummy interaction # run the command to validate at least an echo works with a built bot asyncio.run(cmd.callback(self=cmd, - interaction=MockInteraction(test_callback))) + interaction=mock_interaction.MockInteraction(test_callback))) def test_sync(self): """test sync command """ - bot = MockBot.as_ready() + bot = mock_bot.MockBot.as_ready() cog = bot.cogs['Sync'] self.assertIsNotNone(cog) @@ -139,6 +140,6 @@ def test_sync(self): # this `should` err anyways, we've made a mock struct after all with self.assertRaises(discord.app_commands.errors.MissingApplicationID) as context: asyncio.run(cmd.callback(self=cog, - interaction=MockInteraction())) + interaction=mock_interaction.MockInteraction())) self.assertTrue(isinstance(context.exception, discord.app_commands.errors.MissingApplicationID)) diff --git a/PyDiscoBot/tasks/__init__.py b/PyDiscoBot/tasks/__init__.py index 2a57fe1..c4cfbf4 100644 --- a/PyDiscoBot/tasks/__init__.py +++ b/PyDiscoBot/tasks/__init__.py @@ -2,9 +2,11 @@ """ from .admin import AdminTask +from .test_tasks import TestTasks __version__ = '1.1.2' __all__ = ( 'AdminTask', + 'TestTasks', ) diff --git a/PyDiscoBot/tasks/admin.py b/PyDiscoBot/tasks/admin.py index aac8c31..05faab5 100644 --- a/PyDiscoBot/tasks/admin.py +++ b/PyDiscoBot/tasks/admin.py @@ -6,13 +6,13 @@ from datetime import datetime from typing import Optional import discord -import pydiscobot -from pydiscobot.types import Task +from pydiscobot import bot +from pydiscobot.embed_frames import admin from pydiscobot.services import channels -from pydiscobot import embed_frames +from pydiscobot.types import task -class AdminTask(Task): +class AdminTask(task.Task): """Administrative task for :class:`pydiscobot.Bot`. Manages updating :class:`AdminInfo`. @@ -36,7 +36,7 @@ class AdminTask(Task): """ def __init__(self, - parent: pydiscobot.Bot): + parent: bot.Bot): super().__init__(parent) self._msg: Optional[discord.Message] = None @@ -55,14 +55,14 @@ async def _msg_ch(self): if self._msg: try: - await self._msg.edit(embed=embed_frames.get_admin_frame(self.parent.admin_info)) + await self._msg.edit(embed=admin.get_admin_frame(self.parent.admin_info)) return except (discord.errors.NotFound, AttributeError, discord.errors.DiscordServerError): self.logger.info('creating new message...') await channels.clear_messages(self.parent.admin_info.channels.admin, 100) self._msg = await self.parent.admin_info.channels.admin.send( - embed=embed_frames.get_admin_frame(self.parent.admin_info) + embed=admin.get_admin_frame(self.parent.admin_info) ) def _time(self): diff --git a/tests/tasks/tasks.py b/PyDiscoBot/tasks/test_tasks.py similarity index 86% rename from tests/tasks/tasks.py rename to PyDiscoBot/tasks/test_tasks.py index 0d9823c..4e83b7b 100644 --- a/tests/tasks/tasks.py +++ b/PyDiscoBot/tasks/test_tasks.py @@ -4,7 +4,7 @@ import asyncio import unittest -from tests.types import MockBot +from pydiscobot.types.mock import mock_bot class TestTasks(unittest.TestCase): @@ -14,7 +14,7 @@ class TestTasks(unittest.TestCase): def test_admin_task(self): """test admin task by ticking """ - bot = MockBot.as_ready() + bot = mock_bot.MockBot.as_ready() self.assertIsNotNone(bot.tasker.by_name('AdminTask')) self.assertEqual(bot.admin_info.current_tick, 0) asyncio.run(bot.tasker.by_name('AdminTask').run()) diff --git a/tests/types/bot.py b/PyDiscoBot/test_bot.py similarity index 83% rename from tests/types/bot.py rename to PyDiscoBot/test_bot.py index 32b5703..d56cd36 100644 --- a/tests/types/bot.py +++ b/PyDiscoBot/test_bot.py @@ -3,7 +3,7 @@ from __future__ import annotations import unittest -from .mock_bot import MockBot +from pydiscobot.types.mock import mock_bot class TestBot(unittest.TestCase): @@ -13,7 +13,7 @@ class TestBot(unittest.TestCase): def test_build(self): """test bot build without err """ - bot = MockBot.as_ready() + bot = mock_bot.MockBot.as_ready() self.assertTrue(bot.admin_info.initialized) self.assertIsNotNone(bot.admin_info.cycle_time) self.assertTrue(isinstance(bot.admin_info.cycle_time, int)) diff --git a/PyDiscoBot/types/__init__.py b/PyDiscoBot/types/__init__.py index d9a433a..1736c83 100644 --- a/PyDiscoBot/types/__init__.py +++ b/PyDiscoBot/types/__init__.py @@ -1,27 +1,27 @@ """PyDiscoBot built-in types """ -from . import unittest -from .admin_info import AdminInfo -from .cmd import Cmd -from .embed_field import EmbedField -from .err import BotNotLoaded, IllegalChannel, InsufficientPrivilege, ReportableError -from .pagination import Pagination, InteractionPagination -from .task import Task -from .tasker import Tasker +from . import mock +from . import admin_channels +from . import admin_info +from . import cmd +from . import embed_field +from . import err +from . import pagination +from . import task +from . import tasker +from .. import test_bot -__version__ = '1.1.3' +__version__ = '1.1.4' __all__ = ( - 'AdminInfo', - 'BotNotLoaded', - 'Cmd', - 'EmbedField', - 'IllegalChannel', - 'InsufficientPrivilege', - 'Pagination', - 'InteractionPagination', - 'ReportableError', - 'Task', - 'Tasker', - 'unittest', + 'mock', + 'admin_channels', + 'admin_info', + 'cmd', + 'embed_field', + 'err', + 'pagination', + 'task', + 'tasker', + 'test_bot', ) diff --git a/PyDiscoBot/types/cmd.py b/PyDiscoBot/types/cmd.py index 42702f4..be9af5e 100644 --- a/PyDiscoBot/types/cmd.py +++ b/PyDiscoBot/types/cmd.py @@ -4,7 +4,7 @@ from __future__ import annotations from discord.ext import commands -import pydiscobot +from pydiscobot import bot class Cmd(commands.Cog): @@ -33,11 +33,11 @@ class Cmd(commands.Cog): """ def __init__(self, - parent: pydiscobot.Bot): + parent: bot.Bot): self._parent = parent @property - def parent(self) -> pydiscobot.Bot: + def parent(self) -> bot.Bot: """get parent of this command Returns: diff --git a/PyDiscoBot/types/mock/__init__.py b/PyDiscoBot/types/mock/__init__.py new file mode 100644 index 0000000..ae1fbfc --- /dev/null +++ b/PyDiscoBot/types/mock/__init__.py @@ -0,0 +1,25 @@ +"""PyDiscoBot built-in test types +Provide mock data and meta schemes to allow bot to test +Such as MockGuild, MockMessage, MockUser, MockInteraction, etc + """ +from . import mock_bot +from . import mock_channel +from . import mock_connection_state +from . import mock_guild +from . import mock_interaction +from . import mock_member +from . import mock_message +from . import mock_user + +__version__ = '1.1.4' + +__all__ = ( + 'mock_bot', + 'mock_channel', + 'mock_connection_state', + 'mock_guild', + 'mock_interaction', + 'mock_member', + 'mock_message', + 'mock_user', +) diff --git a/tests/types/mock_bot.py b/PyDiscoBot/types/mock/mock_bot.py similarity index 68% rename from tests/types/mock_bot.py rename to PyDiscoBot/types/mock/mock_bot.py index 66c9bdb..3edef2d 100644 --- a/tests/types/mock_bot.py +++ b/PyDiscoBot/types/mock/mock_bot.py @@ -7,12 +7,12 @@ from typing import Self import discord import dotenv -from pydiscobot.bot import Bot -from pydiscobot.types.unittest.mock_user import MockUserData -from pydiscobot.types.unittest.mock_connection_state import MockConnectionState +from pydiscobot import bot +from .mock_user import MockUserData +from .mock_connection_state import MockConnectionState -class MockBot(Bot): +class MockBot(bot.Bot): """Mock :class:`Bot` to be used in unittesting """ @@ -26,19 +26,15 @@ def as_mock(cls) -> Self: dir_path = os.path.dirname(os.path.realpath(__file__)) dotenv.load_dotenv(f'{dir_path}\\.testenv') intents = discord.Intents(8) - # noinspection PyDunderSlots intents.guilds = True - # noinspection PyDunderSlots intents.members = True - # noinspection PyDunderSlots intents.message_content = True - # noinspection PyDunderSlots intents.messages = True - bot = cls('!', intents, []) + b = cls('!', intents, []) data = MockUserData.generic() data['id'] = 12341234 # change this in case any checks on the default user happen against the bot - bot._connection.user = discord.User(state=MockConnectionState(), data=data) - return bot + b._connection.user = discord.User(state=MockConnectionState(), data=data) + return b @classmethod def as_ready(cls) -> Self: diff --git a/PyDiscoBot/types/unittest/mock_channel.py b/PyDiscoBot/types/mock/mock_channel.py similarity index 100% rename from PyDiscoBot/types/unittest/mock_channel.py rename to PyDiscoBot/types/mock/mock_channel.py diff --git a/PyDiscoBot/types/unittest/mock_connection_state.py b/PyDiscoBot/types/mock/mock_connection_state.py similarity index 100% rename from PyDiscoBot/types/unittest/mock_connection_state.py rename to PyDiscoBot/types/mock/mock_connection_state.py diff --git a/PyDiscoBot/types/unittest/mock_guild.py b/PyDiscoBot/types/mock/mock_guild.py similarity index 100% rename from PyDiscoBot/types/unittest/mock_guild.py rename to PyDiscoBot/types/mock/mock_guild.py diff --git a/PyDiscoBot/types/unittest/mock_interaction.py b/PyDiscoBot/types/mock/mock_interaction.py similarity index 100% rename from PyDiscoBot/types/unittest/mock_interaction.py rename to PyDiscoBot/types/mock/mock_interaction.py diff --git a/PyDiscoBot/types/unittest/mock_member.py b/PyDiscoBot/types/mock/mock_member.py similarity index 100% rename from PyDiscoBot/types/unittest/mock_member.py rename to PyDiscoBot/types/mock/mock_member.py diff --git a/PyDiscoBot/types/unittest/mock_message.py b/PyDiscoBot/types/mock/mock_message.py similarity index 100% rename from PyDiscoBot/types/unittest/mock_message.py rename to PyDiscoBot/types/mock/mock_message.py diff --git a/PyDiscoBot/types/unittest/mock_user.py b/PyDiscoBot/types/mock/mock_user.py similarity index 100% rename from PyDiscoBot/types/unittest/mock_user.py rename to PyDiscoBot/types/mock/mock_user.py diff --git a/PyDiscoBot/types/task.py b/PyDiscoBot/types/task.py index faff4b9..8ef3643 100644 --- a/PyDiscoBot/types/task.py +++ b/PyDiscoBot/types/task.py @@ -4,8 +4,7 @@ from abc import ABC, abstractmethod from logging import Logger -from typing import Any -import pydiscobot +from pydiscobot import bot from pydiscobot.services import log @@ -58,38 +57,82 @@ def run(self): """ def __init__(self, - parent: pydiscobot.Bot): - self._parent = parent + parent: bot.Bot): + self._parent: bot.Bot = parent self._logger = log.logger(self.__class__.__name__) @property def name(self) -> str: - """get the name of this task + """Get the `name` of this :class:`Task`. - Returns: - str: name - """ + .. ------------------------------------------------------------ + + Returns + ----------- + name: :class:`str` + The name of this :class:`Task`. + + """ return self.__class__.__name__ @property def logger(self) -> Logger: - """get this task's logger + """Get the :class:`Logger` of this :class:`Task`. + + .. ------------------------------------------------------------ + + Returns + ----------- + logger: :class:`Logger` + The :class:`Logger` of this :class:`Task`. - Returns: - Logger: logger - """ + """ return self._logger @property - def parent(self) -> Any: - """get this task's parent + def parent(self) -> bot.Bot: + """Get the :class:`pydiscobot.Bot` of this :class:`Task`. + + .. ------------------------------------------------------------ + + Returns + ----------- + parent: :class:`pydiscobot.Bot` + The parent :class:`pydiscobot.Bot` that owns this :class:`Task`. - Returns: - Any: parent - """ + """ return self._parent @abstractmethod async def run(self): - """run the task - """ + """Abstract method that is called by :class:`pydiscobot.Bot` during it's ticks. + + A class inheriting this task must override this method. + + .. ------------------------------------------------------------ + + Example + ---------- + + Create a child :type:`class` that inherits this class :class:`Task` + and overrides the :callable:`run` function. + + .. code-block:: python + + import discord + from pydiscobot.types import Task + + class MyTask(Task): + 'this is my task class!' + + def __init__(self, + parent: pydiscobot.Bot): + super().__init__(parent) + ... # initialize the task + + def run(self): + 'this override gets triggered by the bot every tick!' + ... # do some logic + + + """ diff --git a/PyDiscoBot/types/unittest/__init__.py b/PyDiscoBot/types/unittest/__init__.py deleted file mode 100644 index 5e20057..0000000 --- a/PyDiscoBot/types/unittest/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -"""PyDiscoBot built-in unittest types -Provide mock data and meta schemes to allow bot to test -Such as MockGuild, MockMessage, MockUser, MockInteraction, etc - """ -from .mock_channel import MockChannel -from .mock_connection_state import MockConnectionState -from .mock_interaction import MockInteraction, MockInteractionResponse -from .mock_user import MockUser, MockUserData - -__version__ = '1.1.3' - -__all__ = ( - 'MockChannel', - 'MockConnectionState', - 'MockInteraction', - 'MockInteractionResponse', - 'MockUser', - 'MockUserData', -) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index f212ece..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Unit Tests for PyDiscoBot - """ -from . import types -from . import services - - -__version__ = "1.1.3" - -__all__ = ( - 'types', - 'services', -) diff --git a/tests/embed_frames/__init__.py b/tests/embed_frames/__init__.py deleted file mode 100644 index dec4d56..0000000 --- a/tests/embed_frames/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Services Unit Tests for PyDiscoBot - """ -from .frames import TestFrames - - -__version__ = "1.1.3" - -__all__ = ( - 'TestFrames', -) diff --git a/tests/services/__init__.py b/tests/services/__init__.py deleted file mode 100644 index 9a34f8b..0000000 --- a/tests/services/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Services Unit Tests for PyDiscoBot - """ -from . import cmds - - -__version__ = "1.1.3" - -__all__ = ( - 'cmds', -) diff --git a/tests/services/cmds/__init__.py b/tests/services/cmds/__init__.py deleted file mode 100644 index d7c87c6..0000000 --- a/tests/services/cmds/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Command Unit Tests for PyDiscoBot - """ -from .commands import TestCommands - - -__version__ = "1.1.3" - -__all__ = ( - 'TestCommands', -) diff --git a/tests/tasks/__init__.py b/tests/tasks/__init__.py deleted file mode 100644 index 3ca0058..0000000 --- a/tests/tasks/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Services Unit Tests for PyDiscoBot - """ -from .tasks import TestTasks - - -__version__ = "1.1.3" - -__all__ = ( - 'TestTasks', -) diff --git a/tests/types/__init__.py b/tests/types/__init__.py deleted file mode 100644 index 486a15e..0000000 --- a/tests/types/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Types meant ONLY for unit testing, and do not belong in general production code - """ -from .bot import TestBot -from .mock_bot import MockBot - - -__version__ = "1.1.3" - -__all__ = ( - 'TestBot', - 'MockBot', -) From 28916e72ac4cccea0dded8675f8ac6841e6b4a3f Mon Sep 17 00:00:00 2001 From: Brian LaFond <52360893+iroxusux@users.noreply.github.com> Date: Tue, 15 Apr 2025 22:38:45 -0400 Subject: [PATCH 2/4] rebuild the structure to alleviate import issues and improve rigidity of program structure. lots of things changed... sorry Nigel, i'll try to not make this commit about anything other than the structural changes.... I've added some comments to files that had to change irregardless. But did not add functionality or change anything further than the importing scheme itself. --- .testenv | 43 --- .vscode/settings.json | 6 +- PyDiscoBot/__init__.py | 31 +- PyDiscoBot/bot.py | 294 ++++++++++-------- PyDiscoBot/{services => }/channels.py | 7 + PyDiscoBot/cog.py | 58 ++++ .../{services/cmds => commands}/__init__.py | 9 +- .../cmds => commands}/clearchannel.py | 13 +- .../{services/cmds => commands}/datetounix.py | 15 +- .../{services/cmds => commands}/echo.py | 6 +- .../{services/cmds => commands}/help.py | 8 +- .../{services/cmds => commands}/sync.py | 6 +- PyDiscoBot/commands/test_commands.py | 249 +++++++++++++++ PyDiscoBot/{services => }/const.py | 1 + PyDiscoBot/embed_frames/__init__.py | 16 - PyDiscoBot/embed_frames/admin.py | 52 ---- PyDiscoBot/embed_frames/notification.py | 44 --- PyDiscoBot/embed_frames/test_frames.py | 29 -- PyDiscoBot/{embed_frames => }/frame.py | 108 ++++++- PyDiscoBot/{services => }/log.py | 0 PyDiscoBot/services/__init__.py | 14 - PyDiscoBot/services/cmds/test_commands.py | 145 --------- PyDiscoBot/task.py | 112 +++++++ PyDiscoBot/tasks/__init__.py | 4 +- PyDiscoBot/tasks/{admin.py => status.py} | 50 +-- PyDiscoBot/tasks/test_tasks.py | 19 +- PyDiscoBot/test_bot.py | 19 -- PyDiscoBot/test_pydiscobot.py | 34 ++ PyDiscoBot/types/__init__.py | 49 ++- PyDiscoBot/types/admin_channels.py | 25 -- PyDiscoBot/types/admin_info.py | 48 --- PyDiscoBot/types/bot.py | 75 +++++ PyDiscoBot/types/cmd.py | 46 --- PyDiscoBot/types/cog.py | 59 ++++ PyDiscoBot/types/embed_field.py | 30 +- PyDiscoBot/types/err.py | 16 +- PyDiscoBot/types/mock/__init__.py | 25 -- PyDiscoBot/types/mock/mock_bot.py | 48 --- PyDiscoBot/types/mock/mock_channel.py | 41 --- .../types/mock/mock_connection_state.py | 11 - PyDiscoBot/types/mock/mock_guild.py | 27 -- PyDiscoBot/types/mock/mock_interaction.py | 27 -- PyDiscoBot/types/mock/mock_member.py | 42 --- PyDiscoBot/types/mock/mock_message.py | 81 ----- PyDiscoBot/types/mock/mock_user.py | 59 ---- PyDiscoBot/types/pagination.py | 10 + PyDiscoBot/types/status.py | 158 ++++++++++ PyDiscoBot/types/task.py | 96 +----- PyDiscoBot/types/tasker.py | 54 +++- build.sh | 10 + test.sh | 5 + 51 files changed, 1281 insertions(+), 1153 deletions(-) delete mode 100644 .testenv rename PyDiscoBot/{services => }/channels.py (98%) create mode 100644 PyDiscoBot/cog.py rename PyDiscoBot/{services/cmds => commands}/__init__.py (70%) rename PyDiscoBot/{services/cmds => commands}/clearchannel.py (85%) rename PyDiscoBot/{services/cmds => commands}/datetounix.py (89%) rename PyDiscoBot/{services/cmds => commands}/echo.py (94%) rename PyDiscoBot/{services/cmds => commands}/help.py (93%) rename PyDiscoBot/{services/cmds => commands}/sync.py (97%) create mode 100644 PyDiscoBot/commands/test_commands.py rename PyDiscoBot/{services => }/const.py (94%) delete mode 100644 PyDiscoBot/embed_frames/__init__.py delete mode 100644 PyDiscoBot/embed_frames/admin.py delete mode 100644 PyDiscoBot/embed_frames/notification.py delete mode 100644 PyDiscoBot/embed_frames/test_frames.py rename PyDiscoBot/{embed_frames => }/frame.py (53%) rename PyDiscoBot/{services => }/log.py (100%) delete mode 100644 PyDiscoBot/services/__init__.py delete mode 100644 PyDiscoBot/services/cmds/test_commands.py create mode 100644 PyDiscoBot/task.py rename PyDiscoBot/tasks/{admin.py => status.py} (53%) delete mode 100644 PyDiscoBot/test_bot.py create mode 100644 PyDiscoBot/test_pydiscobot.py delete mode 100644 PyDiscoBot/types/admin_channels.py delete mode 100644 PyDiscoBot/types/admin_info.py create mode 100644 PyDiscoBot/types/bot.py delete mode 100644 PyDiscoBot/types/cmd.py create mode 100644 PyDiscoBot/types/cog.py delete mode 100644 PyDiscoBot/types/mock/__init__.py delete mode 100644 PyDiscoBot/types/mock/mock_bot.py delete mode 100644 PyDiscoBot/types/mock/mock_channel.py delete mode 100644 PyDiscoBot/types/mock/mock_connection_state.py delete mode 100644 PyDiscoBot/types/mock/mock_guild.py delete mode 100644 PyDiscoBot/types/mock/mock_interaction.py delete mode 100644 PyDiscoBot/types/mock/mock_member.py delete mode 100644 PyDiscoBot/types/mock/mock_message.py delete mode 100644 PyDiscoBot/types/mock/mock_user.py create mode 100644 PyDiscoBot/types/status.py create mode 100644 build.sh create mode 100644 test.sh diff --git a/.testenv b/.testenv deleted file mode 100644 index 28638ee..0000000 --- a/.testenv +++ /dev/null @@ -1,43 +0,0 @@ -# test.env file for PyDiscoBot testing -# copy this and fill as you need, but this file will not run anything as is -# version edited: 1.1.4 -# -# Version is used for tracking purposes (imagine that) -VERSION='1.1.4' -# -# Cycle time is how often the bot's periodic task runs. This is in seconds -CYCLE_TIME=600 -# -# Discord token is the private API token supplied by Discord's developer portal when the bot is created -# This is a secret token, DO NOT SHARE THIS TOKEN WITH ANYONE! -# The first link describes how to create a bot -# https://discordpy.readthedocs.io/en/stable/discord.html -# The second link takes you to the developer portal in order to make a bot -# https://discord.com/developers/applications -DISCORD_TOKEN=not_a_chance_bozo -# -# *************************************************************** -# Fill the following in per the requirements of your application -# *************************************************************** -# admin channel ID -# The admin channel posts uptime data of the bot. -# This is not required but is recommended. -# To get the Discord ID, enable developer options in Discord and right click on the channel. -# Select "Copy Channel ID" and paste it here -ADMIN_CHANNEL= -# -# notification channel ID -# The channel the bot will post notifications to when necessary. Like, sprocket updates or errors. -# This is not required but is recommended -# To get the Discord ID, enable developer options in Discord and right click on the channel. -# Select "Copy Channel ID" and paste it here -NOTIFICATION_CHANNEL= -# -# server icon -# This is the 'Emoji' ID of the 'Emoji' that would represent your franchise. -# This emoji will appear on certain embedded posts / messages. -# Getting the server icon out of an emoji can be funky. -# Post the emoji, right click and "Open Link" and copy the number from {} below -# https://cdn.discordapp.com/emojis/ {THIS IS THE NUMBER YOU WANT }.webp?sizeblahblahblah -SERVER_ICON = 'https://images-ext-1.discordapp.net/external/8g0PflayqZyQZe8dqTD_wYGPcCpucRWAsnIjV4amZhc/https/i.imgur.com/6anH1sI.png?format=webp&quality=lossless&width=619&height=619' -# diff --git a/.vscode/settings.json b/.vscode/settings.json index 33c6d5e..3cf78df 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,11 +2,11 @@ "python.testing.unittestArgs": [ "-v", "-s", - "./pydiscobot", + ".", "-p", "test_*.py" ], - "python.testing.pytestEnabled": false, - "python.testing.unittestEnabled": true + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false } \ No newline at end of file diff --git a/PyDiscoBot/__init__.py b/PyDiscoBot/__init__.py index db812b2..2c5da5a 100644 --- a/PyDiscoBot/__init__.py +++ b/PyDiscoBot/__init__.py @@ -1,18 +1,31 @@ """PyDiscoBot - a bot by irox """ -from . import embed_frames -from . import services -from . import tasks -from . import types -from . import bot - +from . import ( + commands, + tasks, + types, + bot, + channels, + cog, + const, + frame, + log, + task, + test_pydiscobot +) __version__ = "1.1.4" __all__ = ( - 'bot', - 'embed_frames', - 'services', + 'commands', 'tasks', 'types', + 'bot', + 'channels', + 'cog', + 'const', + 'frame', + 'log', + 'task', + 'test_pydiscobot' ) diff --git a/PyDiscoBot/bot.py b/PyDiscoBot/bot.py index a08bb98..868131d 100644 --- a/PyDiscoBot/bot.py +++ b/PyDiscoBot/bot.py @@ -2,24 +2,35 @@ """ from __future__ import annotations + import asyncio import datetime +from io import StringIO import os -from logging import Logger -from typing import Union -import discord -from discord.ext import commands as disco_commands -from discord import app_commands -from .embed_frames.notification import get_notification -from .services import cmds, const, channels -from .services.log import logger -from .tasks.admin import AdminTask -from .types.admin_info import AdminInfo -from .types.err import InsufficientPrivilege, IllegalChannel, BotNotLoaded, ReportableError -from .types.tasker import Tasker +import sys +from typing import Optional, Union +import unittest -class Bot(disco_commands.Bot): +import discord +from discord import app_commands +from discord.ext import commands + +from .const import ERR_BAD_PRV, ERR_RATE_LMT +from .channels import find_ch +from .commands import Commands +from .frame import get_notification_frame +from .log import logger +from .tasks import StatusTask +from .types import ( + Status, + BaseBot, + Tasker, + ReportableError +) + + +class Bot(BaseBot, commands.Bot): """Represents a Discord bot, wrapped with built-in logic This class is a subclass of :class:`discord.ext.commands.Bot` and as a result @@ -71,127 +82,107 @@ class Bot(disco_commands.Bot): def __init__(self, command_prefix: Union[str, list], bot_intents: discord.Intents, - command_cogs: list[disco_commands.Cog]): + command_cogs: list[commands.Cog]): + super().__init__(command_prefix=command_prefix, intents=bot_intents) self._logger = logger(self.__class__.__name__) - self.logger.info('initializing...') - - self._admin_info = AdminInfo() + self._status = Status() self._tasker = Tasker() - self._tasker.append(AdminTask(self)) + self._tasker.append(StatusTask(self)) - command_cogs.extend(cmds.Commands) + command_cogs.extend(Commands) for cog in command_cogs: asyncio.run(self.add_cog(cog(self))) self.tree.on_error = self.on_tree_error - @property - def admin_info(self) -> AdminInfo: - """ return admin info for the bot - """ - return self._admin_info - - @property - def logger(self) -> Logger: - """get logger of the bot + async def notify(self, + message: Union[str, Exception]) -> None: + """Send a message to the `Bot`'s notification channel (if exists). - Returns: - Logger: logger - """ - return self._logger + If there is no notification channel, the `message` is instead printed to console. - @property - def tasker(self) -> Tasker: - """get this bot's task list + .. ------------------------------------------------------------ - Returns: - Tasker: task list (Tasker) - """ - return self._tasker + Arguments + ----------- + message Union[:class:`str`, :class:`Exception`] + The message to be sent. Will be wrapped in a generic :class:`discord.Embed`. - async def notify(self, - message: str | Exception) -> None: - """ Helper function to send error or notification messages to notify channel with a single parameter.\n - **If a notification channel does not exist**, the notification is printed to console instead\n - **param message**: message to report\n - **returns**: None\n """ if not message: return - if not self._admin_info.channels.notification: - return print(message) - await self.send_notification(ctx=self._admin_info.channels.notification, - text=message, + + if not self.status.channels.notification: + print(message) + return + + await self.send_notification(dest=self.status.channels.notification, + msg=message, as_reply=False) - async def send_notification(self, - ctx: discord.abc.Messageable, - text: str, - as_reply: bool = False, - as_followup: bool = False) -> None: - """ Helper function to send notifications + async def on_ready(self, + suppress_task=False) -> None: + """method called by discord api when the bot connects to the gateway server and is ready for production + + .. ------------------------------------------------------------ + + Arguments + ----------- + suppress_task :type:`bool` + Whether to suppress the periodic task or not. Defaults to `False`. """ - if isinstance(ctx, discord.ext.commands.Context): - if as_reply and ctx.author is not None: - await ctx.reply(embed=get_notification(text)) - else: - await ctx.send(embed=get_notification(text)) + self._logger.info('PyDiscoBot on_ready...') + if self._status.initialized: + self._logger.warning('already initialized!') + return - elif isinstance(ctx, discord.Interaction): - if as_followup: - await ctx.followup.send(embed=get_notification(text)) - else: - try: - return await ctx.response.send_message(embed=get_notification(text)) - except discord.errors.InteractionResponded: - return await ctx.followup.send(embed=get_notification(text)) - - elif isinstance(ctx, discord.TextChannel): - await ctx.send(embed=get_notification(text)) - - async def on_command_error(self, - ctx: discord.ext.commands.Context, - error) -> None: - """ Override of discord.Bot on_command_error - If CommandNotFound, simply reply to the user of the error.\n - If not, raise the error naturally\n - """ - if isinstance(error, discord.ext.commands.errors.CommandNotFound): - await ctx.reply(const.ERR_BAD_CMD) - elif isinstance(error, discord.ext.commands.errors.MissingRequiredArgument): - await ctx.reply(const.ERR_MSG_PARAM) - elif isinstance(error, discord.HTTPException): - await ctx.reply(const.ERR_RATE_LMT) - elif isinstance(error, InsufficientPrivilege): - await ctx.reply(const.ERR_BAD_PRV) - elif isinstance(error, IllegalChannel): - await ctx.reply(const.ERR_BAD_CH) - elif isinstance(error, BotNotLoaded): - await ctx.reply(const.ERR_BOT_NL) - elif isinstance(error, ReportableError): - await ctx.reply(str(error)) - else: - await self.notify(f'Error encountered:\n{str(error)}') - raise error + admin_channel_token = os.getenv('ADMIN_CHANNEL') + notification_channel_token = os.getenv('NOTIFICATION_CHANNEL') + + if admin_channel_token: + self._logger.info('initializing admin channel...') + self._status.channels.admin = find_ch(self.guilds, + admin_channel_token) + if not self._status.channels.admin: + self._logger.warning('admin channel not found...') + + if notification_channel_token: + self._logger.info('initializing notification channel...') + self._status.channels.notification = find_ch(self.guilds, + notification_channel_token) + if not self._status.channels.notification: + self._logger.warning('notification channel not found...') + + self._status.initialized = True + self._logger.info("POST -> %s", datetime.datetime.now().strftime('%c')) + + if not suppress_task: + self._tasker.run.change_interval(seconds=self.status.cycle_time) + self._tasker.run.start() async def on_tree_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError): """override of parent error method - Args: - interaction (discord.Interaction): interaction that caused the error - error (app_commands.AppCommandError): error raised + .. ------------------------------------------------------------ + + Arguments + ----------- + interaction :class:`discord.Interaction` + `interaction` that caused the error. + error :class:`app_commands.AppCommandError` + Error raised. """ if isinstance(error, app_commands.CommandOnCooldown): - await interaction.response.send_message(const.ERR_RATE_LMT) + await interaction.response.send_message(ERR_RATE_LMT) return elif isinstance(error, app_commands.MissingPermissions): - await interaction.response.send_message(const.ERR_BAD_PRV) + await interaction.response.send_message(ERR_BAD_PRV) return elif isinstance(error, discord.app_commands.CommandInvokeError): @@ -204,38 +195,83 @@ async def on_tree_error(self, await self.notify(error) raise error - async def on_ready(self, - suppress_task=False) -> None: - """method called by discord api when the bot connects to the gateway server and is ready for production + async def send_notification(self, + dest: Union[commands.Context, discord.Interaction, discord.TextChannel], + msg: str, + as_reply: Optional[bool] = False, + as_followup: Optional[bool] = False) -> None: + """Send a notification `embed` to a specified `Channel`, `Context` or `Interaction`. + + .. ------------------------------------------------------------ + + Arguments + ----------- + dest Union[:class:`commands.Context`, :class:`discord.Interaction`, :class:`discord.TextChannel`] + The destination to send a notification `Embed` to. + + msg :type:`str` + The message to be sent in the body of the notification `Embed`. + + as_reply Optional[:class:`bool`] + Defaults to ``False``. Set if you want the bot to reply with the message. + + This works with both `Context` and `Interaction`. + + as_followup Optional[:class:`bool`] + Defaults to ``False``. Set if you want the bot to followup to a defer. + + This ``only`` works with an `Interaction` that has been deferred. + + Otherwise, this WILL raise an `Exception` - Args: - suppress_task (bool, optional): run periodic task or not. Defaults to False. """ - self._logger.info('PyDiscoBot on_ready...') - if self._admin_info.initialized: - self._logger.warning('already initialized!') - return + if isinstance(dest, commands.Context): + embed = get_notification_frame(msg) + if as_reply and dest.author is not None: + await dest.reply(embed=embed) + else: + await dest.send(embed=embed) - admin_channel_token = os.getenv('ADMIN_CHANNEL') - notification_channel_token = os.getenv('NOTIFICATION_CHANNEL') + elif isinstance(dest, discord.Interaction): + if as_followup: + await dest.followup.send(embed=embed) + else: + try: + await dest.response.send_message(embed=embed) + except discord.errors.InteractionResponded: + await dest.followup.send(embed=embed) - if admin_channel_token: - self._logger.info('initializing admin channel...') - self._admin_info.channels.admin = channels.find_ch(self.guilds, - admin_channel_token) - if not self._admin_info.channels.admin: - self._logger.warning('admin channel not found...') + elif isinstance(dest, discord.TextChannel): + await dest.send(embed=embed) - if notification_channel_token: - self._logger.info('initializing notification channel...') - self._admin_info.channels.notification = channels.find_ch(self.guilds, - notification_channel_token) - if not self._admin_info.channels.notification: - self._logger.warning('notification channel not found...') + else: + raise TypeError( + 'Invalid type was passed.\n' + 'Destination must be of type `Context`, `Interaction` or `TextChannel`.') - self._admin_info.initialized = True - self._logger.info("POST -> %s", datetime.datetime.now().strftime('%c')) - if not suppress_task: - self._tasker.run.change_interval(seconds=self.admin_info.cycle_time) - self._tasker.run.start() +class TestBaseBot(unittest.TestCase): + """test case for `BaseBot` + """ + + def test_notify(self): + """test bot notify + """ + bot = Bot('!', discord.Intents(8), []) + + def notify_callback(**kwargs): + ... + + capt = StringIO() + sys.stdout = capt + + asyncio.run(bot.notify(None)) + self.assertEqual(capt.getvalue(), '') + + value = 'This is a notification!' + asyncio.run(bot.notify(value)) + self.assertEqual(capt.getvalue(), value) + + bot.status.channels.notification = notify_callback + + sys.stdout = sys.__stdout__ diff --git a/PyDiscoBot/services/channels.py b/PyDiscoBot/channels.py similarity index 98% rename from PyDiscoBot/services/channels.py rename to PyDiscoBot/channels.py index c4772d3..dae65a7 100644 --- a/PyDiscoBot/services/channels.py +++ b/PyDiscoBot/channels.py @@ -2,10 +2,17 @@ """ from __future__ import annotations + from typing import Union import discord +__all__ = ( + 'find_ch', + 'clear_messages', +) + + def find_ch(guilds: list[discord.Guild], channel_id: Union[str, int]) -> Union[discord.abc.GuildChannel, None]: """Get a :class:`discord.abc.GuildChannel` with a corresponding `id`, or none if it is not found. diff --git a/PyDiscoBot/cog.py b/PyDiscoBot/cog.py new file mode 100644 index 0000000..7390858 --- /dev/null +++ b/PyDiscoBot/cog.py @@ -0,0 +1,58 @@ +"""Common discord command + all discord commands should be derived from the meta class + """ +from __future__ import annotations + + +from typing import Optional, TYPE_CHECKING + + +from . import types + + +if TYPE_CHECKING: + from .bot import Bot + + +class Cog(types.BaseCog): + """Pseudo 'meta' discord bot `Cog`. + + A :class:`Cog` is a container for discord `commands` and discord `app_commands`. + + Though this is not an :class:`ABC`, this class describes the meta + relationship the cog has within this environment. + + That being said, all commands used for the bot should be + derived from this 'meta' `Cog`. + + .. ------------------------------------------------------------ + + Arguments + ----------- + parent: Optional[:class:`Bot`] + The parent :class:`Bot` of this `Cog`. + + .. ------------------------------------------------------------ + + Attributes + ----------- + parent: :class:`Bot` | None + The parent :class:`Bot` of this `Cog`. + + """ + + def __init__(self, + parent: Optional[Bot] = None): + self._parent: Bot = parent + + @property + def parent(self) -> Optional[Bot]: + """The parent :class:`Bot` of this `Cog`. + + .. ------------------------------------------------------------ + + Returns + ----------- + parent: :class:`Bot` | None + """ + return self._parent diff --git a/PyDiscoBot/services/cmds/__init__.py b/PyDiscoBot/commands/__init__.py similarity index 70% rename from PyDiscoBot/services/cmds/__init__.py rename to PyDiscoBot/commands/__init__.py index 62b7194..b68785a 100644 --- a/PyDiscoBot/services/cmds/__init__.py +++ b/PyDiscoBot/commands/__init__.py @@ -5,7 +5,7 @@ from .echo import Echo from .help import Help from .sync import Sync -from . import test_commands +from .test_commands import TestCommands Commands = [ @@ -21,5 +21,10 @@ __all__ = ( 'Commands', - 'test_commands', + 'ClearChannel', + 'DateToUnix', + 'Echo', + 'Help', + 'Sync', + 'TestCommands', ) diff --git a/PyDiscoBot/services/cmds/clearchannel.py b/PyDiscoBot/commands/clearchannel.py similarity index 85% rename from PyDiscoBot/services/cmds/clearchannel.py rename to PyDiscoBot/commands/clearchannel.py index aa1cedc..b0ede45 100644 --- a/PyDiscoBot/services/cmds/clearchannel.py +++ b/PyDiscoBot/commands/clearchannel.py @@ -3,13 +3,18 @@ """ from __future__ import annotations + import discord from discord import app_commands -from pydiscobot.services.channels import clear_messages -from pydiscobot.types import cmd -class ClearChannel(cmd.Cmd): +from .. import ( + channels, + cog +) + + +class ClearChannel(cog.Cog): """ClearChannel command cog. """ @@ -34,4 +39,4 @@ async def clearchannel(self, """ await interaction.response.defer() - await clear_messages(interaction.channel, message_count) + await channels.clear_messages(interaction.channel, message_count) diff --git a/PyDiscoBot/services/cmds/datetounix.py b/PyDiscoBot/commands/datetounix.py similarity index 89% rename from PyDiscoBot/services/cmds/datetounix.py rename to PyDiscoBot/commands/datetounix.py index 9182d70..459cc0e 100644 --- a/PyDiscoBot/services/cmds/datetounix.py +++ b/PyDiscoBot/commands/datetounix.py @@ -3,12 +3,15 @@ """ from __future__ import annotations + import datetime import time import discord from discord import app_commands -from pydiscobot.embed_frames import frame -from pydiscobot.types import cmd, embed_field + +from .. import frame +from .. import cog +from ..types import EmbedField ERR = '\n'.join([ @@ -18,7 +21,7 @@ ]) -class DateToUnix(cmd.Cmd): +class DateToUnix(cog.Cog): """convert date to unix string date is placed into embed, broken apart, so it can be copied and pasted by the user. """ @@ -77,9 +80,9 @@ async def datetounix(self, d = datetime.datetime.strptime(_d, '%y/%m/%d %H:%M:%S') unix_time = time.mktime(d.timetuple()) - f = [embed_field.EmbedField('date ->', - f'template: <`t:{str(int(unix_time))}:F`>\n' - f'')] + f = [EmbedField('date ->', + f'template: <`t:{str(int(unix_time))}:F`>\n' + f'')] await interaction.response.send_message(embed=frame.get_frame('**Date To Unix**', '', f)) diff --git a/PyDiscoBot/services/cmds/echo.py b/PyDiscoBot/commands/echo.py similarity index 94% rename from PyDiscoBot/services/cmds/echo.py rename to PyDiscoBot/commands/echo.py index eaedffd..59d30c0 100644 --- a/PyDiscoBot/services/cmds/echo.py +++ b/PyDiscoBot/commands/echo.py @@ -4,12 +4,14 @@ """ from __future__ import annotations + import discord from discord import app_commands -from pydiscobot.types import cmd + +from .. import cog -class Echo(cmd.Cmd): +class Echo(cog.Cog): """echo string """ diff --git a/PyDiscoBot/services/cmds/help.py b/PyDiscoBot/commands/help.py similarity index 93% rename from PyDiscoBot/services/cmds/help.py rename to PyDiscoBot/commands/help.py index f04acc2..c39e3ae 100644 --- a/PyDiscoBot/services/cmds/help.py +++ b/PyDiscoBot/commands/help.py @@ -2,15 +2,17 @@ """ from __future__ import annotations + import discord from discord import app_commands -from pydiscobot.embed_frames import frame -from pydiscobot.types import cmd +from .. import frame +from .. import cog + MSG = '**help**', 'If you have an issue, please reach out to `irox_rl`.' -class Help(cmd.Cmd): +class Help(cog.Cog): """help """ diff --git a/PyDiscoBot/services/cmds/sync.py b/PyDiscoBot/commands/sync.py similarity index 97% rename from PyDiscoBot/services/cmds/sync.py rename to PyDiscoBot/commands/sync.py index 294e0ad..7001be2 100644 --- a/PyDiscoBot/services/cmds/sync.py +++ b/PyDiscoBot/commands/sync.py @@ -4,13 +4,15 @@ """ from __future__ import annotations + import discord from discord import app_commands from discord.ext import commands -from pydiscobot.types import cmd + +from .. import cog -class Sync(cmd.Cmd): +class Sync(cog.Cog): """sync commands """ diff --git a/PyDiscoBot/commands/test_commands.py b/PyDiscoBot/commands/test_commands.py new file mode 100644 index 0000000..1eda722 --- /dev/null +++ b/PyDiscoBot/commands/test_commands.py @@ -0,0 +1,249 @@ +"""test commands for pydisco bot + """ +from __future__ import annotations + + +import asyncio +from typing import Optional +import unittest +import discord +from discord.state import ConnectionState +from discord.ext import commands + + +from .clearchannel import ClearChannel +from .datetounix import DateToUnix, ERR +from .echo import Echo +from .help import Help +from .sync import Sync + + +__all__ = ( + 'MockGuild', + 'MockChannel', + 'MockInteractionResponse', + 'MockInteraction', + 'TestCommands', +) + + +class MockGuild(discord.Guild): + """mock Discord Guild + """ + guild_member_count = 69 + guild_name = 'Test Guild' + guild_id = 1234567890 + guild_owner_id = 123123123123 + mock_data = { + 'member_count': guild_member_count, + 'name': guild_name, + 'verification_level': 0, + 'notification_level': 0, + 'id': guild_id, + 'owner_id': guild_owner_id, + } + + def __init__(self): + super().__init__(data=MockGuild.mock_data, state=ConnectionState(dispatch=None, + handlers=None, + hooks=None, + http=None)) + + +class MockChannel(discord.TextChannel): + """mock Discord Text Channel + """ + channel_id = 69420 + channel_name = 'Big Doinkers Anon.' + guild = MockGuild() + state = discord.state.ConnectionState(dispatch=None, + handlers=None, + hooks=None, + http=None) + + def __init__(self, + name: str | None = None, + chid: int | None = None, + guild: discord.Guild | None = None): + data = { + 'id': MockChannel.channel_id if not chid else chid, + 'name': MockChannel.name if not name else name, + 'type': discord.channel.TextChannel, + 'position': 0, + 'guild': MockChannel.guild if not guild else guild + } + super().__init__(state=MockChannel.state, guild=data['guild'], data=data) + + async def delete_messages(self, + *args, + **kwargs): + """dummy method + """ + + async def history(self, + *args, + **kwargs): + """dummy method + """ + for _ in range(0, 0): + yield None + + +class MockInteractionResponse: + """make-shift interaction response to support unit-testing + """ + + def __init__(self, + send_message_cb: Optional[callable] = None): + self.send_message: callable = send_message_cb + + async def defer(self): + """dummy callback for interaction response -> defer + """ + + +class MockInteraction: + """make-shift interaction to support unit-testing + """ + + def __init__(self, + send_message_cb: Optional[callable] = None): + self.channel = MockChannel() + self.response: MockInteractionResponse = MockInteractionResponse(send_message_cb) + + +class TestCommands(unittest.TestCase): + """test commands for pydisco bot + """ + + def test_clearchannel(self): + """test clearchannel command + """ + cog = ClearChannel() + self.assertIsNotNone(cog) + + cmd = next((x for x in cog.get_app_commands() if x is not None), None) + self.assertIsNotNone(cmd) + + setattr(self, 'msgs_deleted', 0) + + # check it doesn't err + async def test_cb(*_, **__): + setattr(self, 'msgs_deleted', getattr(self, 'msgs_deleted')+1) + + interaction = MockInteraction() + interaction.channel.delete_messages = test_cb + + asyncio.run(cmd.callback(self=cmd, + interaction=interaction, + message_count=10)) + + self.assertEqual(getattr(self, 'msgs_deleted'), 1) + + # check upper level err + with self.assertRaises(ValueError) as context: + asyncio.run(cmd.callback(self=cmd, + interaction=MockInteraction(), + message_count=250)) + + self.assertTrue(isinstance(context.exception, ValueError)) + + # check lwr level err + with self.assertRaises(ValueError) as context: + asyncio.run(cmd.callback(self=cmd, + interaction=MockInteraction(), + message_count=0)) + + self.assertTrue(isinstance(context.exception, ValueError)) + + def test_datetounix(self): + """test datetounix command + """ + cog = DateToUnix() + self.assertIsNotNone(cog) + + cmd = next((x for x in cog.get_app_commands() if x is not None), None) + self.assertIsNotNone(cmd) + + async def test_callback_succeed(embed): + self.assertTrue(isinstance(embed, discord.Embed)) + + async def test_callback_fail(embed): + self.assertEqual(ERR, embed) + + # create dummy interaction + # run to validate command sends ERR + asyncio.run(cmd.callback(self=cmd, + interaction=MockInteraction(test_callback_fail), + year='a', + month='b', + day='c', + hour='d', + minute='e', + second='f')) + + # if we know it errs, lets check that it DOESNT err + asyncio.run(cmd.callback(self=cmd, + interaction=MockInteraction(test_callback_succeed), + year='25', + month='8', + day='16', + hour='12', + minute='00', + second='00')) + + def test_echo(self): + """test echo command + """ + cog = Echo() + self.assertIsNotNone(cog) + + cmd = next((x for x in cog.get_app_commands() if x is not None), None) + self.assertIsNotNone(cmd) + + sent_value = 'echo these nutz' + + async def test_callback(message): + self.assertEqual(sent_value, message) + + # create dummy interaction + # run the command to validate at least an echo works with a built bot + asyncio.run(cmd.callback(self=cmd, + interaction=MockInteraction(test_callback), + message=sent_value)) + + def test_help(self): + """test help command + """ + cog = Help() + self.assertIsNotNone(cog) + + cmd = next((x for x in cog.get_app_commands() if x is not None), None) + self.assertIsNotNone(cmd) + + async def test_callback(embed): + self.assertTrue(isinstance(embed, discord.Embed)) + + # create dummy interaction + # run the command to validate at least an echo works with a built bot + asyncio.run(cmd.callback(self=cmd, + interaction=MockInteraction(test_callback))) + + def test_sync(self): + """test sync command + """ + bot = commands.Bot('!', intents=discord.Intents(0)) + cog = Sync(bot) + cog._parent = bot + self.assertIsNotNone(cog) + + cmd = next((x for x in cog.get_app_commands() if x is not None), None) + self.assertIsNotNone(cmd) + + # check err raises by discord package + # this `should` err anyways, we've made a mock struct after all + with self.assertRaises(discord.app_commands.errors.MissingApplicationID) as context: + asyncio.run(cmd.callback(self=cog, + interaction=MockInteraction())) + + self.assertTrue(isinstance(context.exception, discord.app_commands.errors.MissingApplicationID)) diff --git a/PyDiscoBot/services/const.py b/PyDiscoBot/const.py similarity index 94% rename from PyDiscoBot/services/const.py rename to PyDiscoBot/const.py index bc3d2cf..b59a9b8 100644 --- a/PyDiscoBot/services/const.py +++ b/PyDiscoBot/const.py @@ -1,5 +1,6 @@ """PyDiscoBot constants """ +from __future__ import annotations import os diff --git a/PyDiscoBot/embed_frames/__init__.py b/PyDiscoBot/embed_frames/__init__.py deleted file mode 100644 index 09e4431..0000000 --- a/PyDiscoBot/embed_frames/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -"""frames module for discord embeds - """ - -from . import admin -from . import frame -from . import notification -from . import test_frames - -__version__ = '1.1.4' - -__all__ = ( - 'admin', - 'frame', - 'notification', - 'test_frames', -) diff --git a/PyDiscoBot/embed_frames/admin.py b/PyDiscoBot/embed_frames/admin.py deleted file mode 100644 index 01365bd..0000000 --- a/PyDiscoBot/embed_frames/admin.py +++ /dev/null @@ -1,52 +0,0 @@ -"""admin embed - """ -from __future__ import annotations - -import discord -from pydiscobot.services import const -from pydiscobot.types import admin_info, embed_field -from .frame import get_frame - - -def get_admin_frame(info: admin_info.AdminInfo) -> discord.Embed: - """Get built-in :class:`discord.Embed` (or 'frame') to display :class:`Bot` :class:`AdminInfo`. - - .. ------------------------------------------------------------ - - Arguments - ----------- - info: :class:`AdminInfo` - The Administrative Information describing it's parent :class:`Bot`. - - .. ------------------------------------------------------------ - - Examples - ---------- - - Get a :class:`discord.Embed` to display a to Admin channel. - - .. code-block:: python - - import discord - from pydiscobot.embed_frames import get_admin_frame - - async def post_admin_info(self, - channel: discord.TextChannel): - '''post admin info to channel''' - - admin_embed = get_admin_frame(self.bot.admin_info) - await channel.send(embed=admin_embed) - - """ - embed = get_frame('**Bot Info**', - 'For help, type `/help`', - [ - embed_field.EmbedField('Version', f"`{info.version}`"), - embed_field.EmbedField( - 'Boot Time', f"`{info.boot_time.strftime(const.DEF_TIME_FORMAT)}`", True), - embed_field.EmbedField('Current Tick', f"`{info.current_tick}`"), - embed_field.EmbedField('Last Time', f"`{info.last_time}`"), - embed_field.EmbedField('Cycle Time', f"`{info.cycle_time}`s", True) - ]) - - return embed diff --git a/PyDiscoBot/embed_frames/notification.py b/PyDiscoBot/embed_frames/notification.py deleted file mode 100644 index 1dfe40e..0000000 --- a/PyDiscoBot/embed_frames/notification.py +++ /dev/null @@ -1,44 +0,0 @@ -"""notification frame - """ -from __future__ import annotations - -import discord -from pydiscobot.types.embed_field import EmbedField -from .frame import get_frame - - -def get_notification(text: str) -> discord.Embed: - """Get built-in :class:`discord.Embed` (or 'frame') to display a standard notification. - - .. ------------------------------------------------------------ - - Arguments - ----------- - text: :class:`str` - The message to display in the body of the :class:`discord.Embed`. - - .. ------------------------------------------------------------ - - Examples - ---------- - - Get a :class:`discord.Embed` to display a general notification. - - .. code-block:: python - - import discord - from pydiscobot.embed_frames import get_notification - - async def send_notfication(self, - channel: discord.TextChannel): - '''post notification to channel''' - - embed = get_notification('How are ya doin today?') - - await channel.send(embed=embed) - - """ - embed = get_frame('**Notification**', - None, - [EmbedField('Message', value=text, inline=True)]) - return embed diff --git a/PyDiscoBot/embed_frames/test_frames.py b/PyDiscoBot/embed_frames/test_frames.py deleted file mode 100644 index 25e7b60..0000000 --- a/PyDiscoBot/embed_frames/test_frames.py +++ /dev/null @@ -1,29 +0,0 @@ -"""test frames functionality for pydisco bot - """ -from __future__ import annotations - -import unittest -import discord -from pydiscobot.embed_frames import frame, admin, notification -from pydiscobot.types import admin_info - - -class TestFrames(unittest.TestCase): - """test frames for pydisco bot - """ - - def test_get_frame(self): - """test bot can compile and receive default frame - """ - frm = frame.get_frame() - self.assertTrue(isinstance(frm, discord.Embed)) - - def test_get_admin_frame(self): - """test bot can compile and receive admin frame - """ - self.assertTrue(isinstance(admin.get_admin_frame(admin_info.AdminInfo()), discord.Embed)) - - def test_get_notification_frame(self): - """test bot can compile and receive notification frame - """ - self.assertTrue(isinstance(notification.get_notification('bing bong'), discord.Embed)) diff --git a/PyDiscoBot/embed_frames/frame.py b/PyDiscoBot/frame.py similarity index 53% rename from PyDiscoBot/embed_frames/frame.py rename to PyDiscoBot/frame.py index 46ca7cd..0a626a3 100644 --- a/PyDiscoBot/embed_frames/frame.py +++ b/PyDiscoBot/frame.py @@ -3,11 +3,15 @@ """ from __future__ import annotations + import datetime from typing import Optional, Union +import unittest + import discord -from pydiscobot.services import const -from pydiscobot.types.embed_field import EmbedField + +from . import const +from .types import EmbedField, Status def get_frame(title: Optional[str] = None, @@ -117,3 +121,103 @@ async def send_notification(self, inline=field.inline) return embed + + +def get_status_frame(info: Status) -> discord.Embed: + """Get built-in :class:`discord.Embed` (or 'frame') to display :class:`Bot` :class:`Status`. + + .. ------------------------------------------------------------ + + Arguments + ----------- + info: :class:`Status` + The Information describing it's parent :class:`Bot`. + + .. ------------------------------------------------------------ + + Examples + ---------- + + Get a :class:`discord.Embed` to display a to channel. + + .. code-block:: python + + import discord + from pydiscobot.embed_frames import get_status_frame + + async def post_info(self, + channel: discord.TextChannel): + '''post info to channel''' + + embed = get_status_frame(self.bot.status_info) + await channel.send(embed=embed) + + """ + embed = get_frame('**Bot Info**', + 'For help, type `/help`', + [ + EmbedField('Version', f"`{info.version}`"), + EmbedField('Boot Time', f"`{info.boot_time.strftime(const.DEF_TIME_FORMAT)}`", True), + EmbedField('Current Tick', f"`{info.current_tick}`"), + EmbedField('Last Time', f"`{info.last_time}`"), + EmbedField('Cycle Time', f"`{info.cycle_time}`s", True) + ]) + + return embed + + +def get_notification_frame(text: str) -> discord.Embed: + """Get built-in :class:`discord.Embed` (or 'frame') to display a standard notification. + + .. ------------------------------------------------------------ + + Arguments + ----------- + text: :class:`str` + The message to display in the body of the :class:`discord.Embed`. + + .. ------------------------------------------------------------ + + Examples + ---------- + + Get a :class:`discord.Embed` to display a general notification. + + .. code-block:: python + + import discord + from pydiscobot.embed_frames import get_notification + + async def send_notfication(self, + channel: discord.TextChannel): + '''post notification to channel''' + + embed = get_notification('How are ya doin today?') + + await channel.send(embed=embed) + + """ + embed = get_frame('**Notification**', + None, + [EmbedField('Message', value=text, inline=True)]) + return embed + + +class TestFrames(unittest.TestCase): + """test frames for pydisco bot + """ + + def test_get_frame(self): + """test bot can compile and receive default frame + """ + self.assertTrue(isinstance(get_frame(), discord.Embed)) + + def test_get_status_frame(self): + """test bot can compile and receive status frame + """ + self.assertTrue(isinstance(get_status_frame(Status()), discord.Embed)) + + def test_get_notification_frame(self): + """test bot can compile and receive notification frame + """ + self.assertTrue(isinstance(get_notification_frame('bing bong'), discord.Embed)) diff --git a/PyDiscoBot/services/log.py b/PyDiscoBot/log.py similarity index 100% rename from PyDiscoBot/services/log.py rename to PyDiscoBot/log.py diff --git a/PyDiscoBot/services/__init__.py b/PyDiscoBot/services/__init__.py deleted file mode 100644 index 834589f..0000000 --- a/PyDiscoBot/services/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -"""PyDiscoBot logical services - commands, channels, constants, loggers, etc. - """ - -from . import cmds, channels, const, log - -__version__ = '1.1.2' - -__all__ = ( - 'cmds', - 'channels', - 'const', - 'log', -) diff --git a/PyDiscoBot/services/cmds/test_commands.py b/PyDiscoBot/services/cmds/test_commands.py deleted file mode 100644 index 2ca3892..0000000 --- a/PyDiscoBot/services/cmds/test_commands.py +++ /dev/null @@ -1,145 +0,0 @@ -"""test commands for pydisco bot - """ -from __future__ import annotations - -import asyncio -import unittest -import discord -from pydiscobot.types.mock import mock_bot, mock_interaction -from pydiscobot.services.cmds.datetounix import ERR - - -class TestCommands(unittest.TestCase): - """test commands for pydisco bot - """ - - def test_clearchannel(self): - """test clearchannel command - """ - bot = mock_bot.MockBot.as_ready() - - cog = bot.cogs['ClearChannel'] - self.assertIsNotNone(cog) - - cmd = next((x for x in cog.get_app_commands() if x is not None), None) - self.assertIsNotNone(cmd) - - # do it - asyncio.run(cmd.callback(self=cmd, - interaction=mock_interaction.MockInteraction(), - message_count=10)) - - # check upper level err - with self.assertRaises(ValueError) as context: - asyncio.run(cmd.callback(self=cmd, - interaction=mock_interaction.MockInteraction(), - message_count=250)) - - self.assertTrue(isinstance(context.exception, ValueError)) - - # check lwr level err - with self.assertRaises(ValueError) as context: - asyncio.run(cmd.callback(self=cmd, - interaction=mock_interaction.MockInteraction(), - message_count=0)) - - self.assertTrue(isinstance(context.exception, ValueError)) - - def test_datetounix(self): - """test datetounix command - """ - bot = mock_bot.MockBot.as_ready() - - cog = bot.cogs['DateToUnix'] - self.assertIsNotNone(cog) - - cmd = next((x for x in cog.get_app_commands() if x is not None), None) - self.assertIsNotNone(cmd) - - async def test_callback_succeed(embed): - self.assertTrue(isinstance(embed, discord.Embed)) - - async def test_callback_fail(embed): - self.assertEqual(ERR, embed) - - # create dummy interaction - # run to validate command sends ERR - asyncio.run(cmd.callback(self=cmd, - interaction=mock_interaction.MockInteraction(test_callback_fail), - year='a', - month='b', - day='c', - hour='d', - minute='e', - second='f')) - - # if we know it errs, lets check that it DOESNT err - asyncio.run(cmd.callback(self=cmd, - interaction=mock_interaction.MockInteraction(test_callback_succeed), - year='25', - month='8', - day='16', - hour='12', - minute='00', - second='00')) - - def test_echo(self): - """test echo command - """ - bot = mock_bot.MockBot.as_ready() - - echo_cog = bot.cogs['Echo'] - self.assertIsNotNone(echo_cog) - - echo_cmd = next((x for x in echo_cog.get_app_commands() if x is not None), None) - self.assertIsNotNone(echo_cmd) - - sent_value = 'echo these nutz' - - async def test_callback(message): - bot.logger.info(message) # nice - self.assertEqual(sent_value, message) - - # create dummy interaction - # run the command to validate at least an echo works with a built bot - asyncio.run(echo_cmd.callback(self=echo_cmd, - interaction=mock_interaction.MockInteraction(test_callback), - message=sent_value)) - - def test_help(self): - """test help command - """ - bot = mock_bot.MockBot.as_ready() - - cog = bot.cogs['Help'] - self.assertIsNotNone(cog) - - cmd = next((x for x in cog.get_app_commands() if x is not None), None) - self.assertIsNotNone(cmd) - - async def test_callback(embed): - self.assertTrue(isinstance(embed, discord.Embed)) - - # create dummy interaction - # run the command to validate at least an echo works with a built bot - asyncio.run(cmd.callback(self=cmd, - interaction=mock_interaction.MockInteraction(test_callback))) - - def test_sync(self): - """test sync command - """ - bot = mock_bot.MockBot.as_ready() - - cog = bot.cogs['Sync'] - self.assertIsNotNone(cog) - - cmd = next((x for x in cog.get_app_commands() if x is not None), None) - self.assertIsNotNone(cmd) - - # check err raises by discord package - # this `should` err anyways, we've made a mock struct after all - with self.assertRaises(discord.app_commands.errors.MissingApplicationID) as context: - asyncio.run(cmd.callback(self=cog, - interaction=mock_interaction.MockInteraction())) - - self.assertTrue(isinstance(context.exception, discord.app_commands.errors.MissingApplicationID)) diff --git a/PyDiscoBot/task.py b/PyDiscoBot/task.py new file mode 100644 index 0000000..c1bc27a --- /dev/null +++ b/PyDiscoBot/task.py @@ -0,0 +1,112 @@ +"""provide an abc base class for all tasks to derive from for consistent operations + """ +from __future__ import annotations + + +from logging import Logger +from typing import TYPE_CHECKING + + +from .log import logger +from .types import BaseTask + + +if TYPE_CHECKING: + from .bot import Bot + + +__all__ = ( + 'Task', +) + + +class Task(BaseTask): + """PyDiscoBot :class:`Task` for logical, periodic operations. + + All user tasks should be derived from this :class:`Task`. + + .. ------------------------------------------------------------ + + Attributes + ----------- + logger: :class:`Logger` + The logger for this :class:`Task` + + .. ------------------------------------------------------------ + + Examples + ---------- + + Create a child :type:`class` that inherits this class :class:`Task` + + .. code-block:: python + + import discord + from pydiscobot import Task + + class MyTask(Task): + 'this is my task class!' + + def __init__(self, + parent: pydiscobot.Bot): + super().__init__(parent) + ... + + def run(self): + 'this override gets triggered by the bot every tick!' + ... + + """ + + def __init__(self, + parent: Bot): + super().__init__(parent) + self._logger = logger(self.name) + + @property + def logger(self) -> Logger: + """Get the :class:`pydiscobot.Bot` of this :class:`Task`. + + .. ------------------------------------------------------------ + + Returns + ----------- + parent: :class:`pydiscobot.Bot` + The parent :class:`pydiscobot.Bot` that owns this :class:`Task`. + + """ + return self._logger + + async def run(self): + """Method that is called by :class:`pydiscobot.Bot` during it's ticks. + + A class inheriting this task must override this method. + + .. ------------------------------------------------------------ + + Example + ---------- + + Create a child :type:`class` that inherits this class :class:`Task` + and overrides the :callable:`run` function. + + .. code-block:: python + + import discord + from pydiscobot.types import Task + + class MyTask(Task): + 'this is my task class!' + + def __init__(self, + parent: pydiscobot.Bot): + super().__init__(parent) + ... # initialize the task + + def run(self): + 'this override gets triggered by the bot every tick!' + ... # do some logic + + + """ + raise NotImplementedError() diff --git a/PyDiscoBot/tasks/__init__.py b/PyDiscoBot/tasks/__init__.py index c4cfbf4..711feb5 100644 --- a/PyDiscoBot/tasks/__init__.py +++ b/PyDiscoBot/tasks/__init__.py @@ -1,12 +1,12 @@ """PyDiscoBot built-in tasks """ -from .admin import AdminTask +from .status import StatusTask from .test_tasks import TestTasks __version__ = '1.1.2' __all__ = ( - 'AdminTask', + 'StatusTask', 'TestTasks', ) diff --git a/PyDiscoBot/tasks/admin.py b/PyDiscoBot/tasks/status.py similarity index 53% rename from PyDiscoBot/tasks/admin.py rename to PyDiscoBot/tasks/status.py index 05faab5..afe0aa0 100644 --- a/PyDiscoBot/tasks/admin.py +++ b/PyDiscoBot/tasks/status.py @@ -3,27 +3,22 @@ """ from __future__ import annotations -from datetime import datetime -from typing import Optional + +from typing import Optional, TYPE_CHECKING import discord -from pydiscobot import bot -from pydiscobot.embed_frames import admin -from pydiscobot.services import channels -from pydiscobot.types import task +from .. import channels, frame +from .. import task -class AdminTask(task.Task): - """Administrative task for :class:`pydiscobot.Bot`. +if TYPE_CHECKING: + from ..types import BaseBot - Manages updating :class:`AdminInfo`. - Also manages posting infos to admin :class:`discord.TextChannel` (if it exists). - .. ------------------------------------------------------------ +class StatusTask(task.Task): + """Status task for :class:`pydiscobot.Bot`. - Arguments - ----------- - parent: :class:`pydiscobot.Bot` - The bot this task belongs to. + Manages updating :class:`Status`. + Also manages posting infos to admin :class:`discord.TextChannel` (if it exists). .. ------------------------------------------------------------ @@ -36,7 +31,7 @@ class AdminTask(task.Task): """ def __init__(self, - parent: bot.Bot): + parent: BaseBot): super().__init__(parent) self._msg: Optional[discord.Message] = None @@ -49,35 +44,24 @@ def message(self) -> Optional[discord.Message]: """ async def _msg_ch(self): - if not self.parent.admin_info.channels.admin: + if not self.parent.status.channels.admin: self.logger.warning('no admin channel available...') return if self._msg: try: - await self._msg.edit(embed=admin.get_admin_frame(self.parent.admin_info)) + await self._msg.edit(embed=frame.get_status_frame(self.parent.status)) return except (discord.errors.NotFound, AttributeError, discord.errors.DiscordServerError): self.logger.info('creating new message...') - await channels.clear_messages(self.parent.admin_info.channels.admin, 100) - self._msg = await self.parent.admin_info.channels.admin.send( - embed=admin.get_admin_frame(self.parent.admin_info) + await channels.clear_messages(self.parent.status.channels.admin, 100) + self._msg = await self.parent.status.channels.admin.send( + embed=frame.get_status_frame(self.parent.status) ) - def _time(self): - """ time function - """ - self.parent.admin_info.last_time = datetime.now() - - async def _admin(self): - """ periodic task admin - """ - self.parent.admin_info.current_tick += 1 - self._time() - async def run(self): """run the admin task """ await self._msg_ch() - await self._admin() + self.parent.status.tick() diff --git a/PyDiscoBot/tasks/test_tasks.py b/PyDiscoBot/tasks/test_tasks.py index 4e83b7b..99c1b1d 100644 --- a/PyDiscoBot/tasks/test_tasks.py +++ b/PyDiscoBot/tasks/test_tasks.py @@ -4,7 +4,12 @@ import asyncio import unittest -from pydiscobot.types.mock import mock_bot + + +import discord + + +from .. import bot class TestTasks(unittest.TestCase): @@ -12,10 +17,10 @@ class TestTasks(unittest.TestCase): """ def test_admin_task(self): - """test admin task by ticking + """test status task by ticking """ - bot = mock_bot.MockBot.as_ready() - self.assertIsNotNone(bot.tasker.by_name('AdminTask')) - self.assertEqual(bot.admin_info.current_tick, 0) - asyncio.run(bot.tasker.by_name('AdminTask').run()) - self.assertEqual(bot.admin_info.current_tick, 1) + _bot = bot.Bot('!', discord.Intents(8), []) + self.assertIsNotNone(_bot.tasker.by_name('StatusTask')) + self.assertEqual(_bot.status.current_tick, 0) + asyncio.run(_bot.tasker.by_name('StatusTask').run()) + self.assertEqual(_bot.status.current_tick, 1) diff --git a/PyDiscoBot/test_bot.py b/PyDiscoBot/test_bot.py deleted file mode 100644 index d56cd36..0000000 --- a/PyDiscoBot/test_bot.py +++ /dev/null @@ -1,19 +0,0 @@ -"""test class for pydisco bot - """ -from __future__ import annotations - -import unittest -from pydiscobot.types.mock import mock_bot - - -class TestBot(unittest.TestCase): - """test class for pydisco bot - """ - - def test_build(self): - """test bot build without err - """ - bot = mock_bot.MockBot.as_ready() - self.assertTrue(bot.admin_info.initialized) - self.assertIsNotNone(bot.admin_info.cycle_time) - self.assertTrue(isinstance(bot.admin_info.cycle_time, int)) diff --git a/PyDiscoBot/test_pydiscobot.py b/PyDiscoBot/test_pydiscobot.py new file mode 100644 index 0000000..698637f --- /dev/null +++ b/PyDiscoBot/test_pydiscobot.py @@ -0,0 +1,34 @@ +"""testing suite for PyDiscoBot + """ +from __future__ import annotations + + +import asyncio +import unittest +import discord + + +from .bot import Bot +from .task import Task + + +__all__ = ( + 'TestPyDiscoBot', +) + + +class TestPyDiscoBot(unittest.TestCase): + """test pydiscobot suite + """ + + def test_task_class(self): + """test task class + """ + bot = Bot('!', discord.Intents(8), []) + task = Task(bot) + self.assertIsNotNone(task) + self.assertEqual(task.name, 'Task') + with self.assertRaises(NotImplementedError) as context: + asyncio.run(task.run()) + + self.assertTrue(isinstance(context.exception, NotImplementedError)) diff --git a/PyDiscoBot/types/__init__.py b/PyDiscoBot/types/__init__.py index 1736c83..d6afe99 100644 --- a/PyDiscoBot/types/__init__.py +++ b/PyDiscoBot/types/__init__.py @@ -1,27 +1,46 @@ """PyDiscoBot built-in types """ -from . import mock -from . import admin_channels -from . import admin_info -from . import cmd -from . import embed_field -from . import err -from . import pagination -from . import task -from . import tasker -from .. import test_bot +from . import ( + bot, + cog, + embed_field, + err, + pagination, + status, + task, + tasker, +) + +from .status import Status +from .bot import BaseBot +from .cog import BaseCog +from .embed_field import EmbedField +from .err import BotNotLoaded, IllegalChannel, InsufficientPrivilege, ReportableError +from .pagination import Pagination, InteractionPagination +from .task import BaseTask +from .tasker import Tasker __version__ = '1.1.4' __all__ = ( - 'mock', - 'admin_channels', - 'admin_info', - 'cmd', + 'bot', + 'cog', 'embed_field', 'err', 'pagination', + 'status', 'task', 'tasker', - 'test_bot', + 'Status', + 'BaseBot', + 'BaseCog', + 'EmbedField', + 'BotNotLoaded', + 'IllegalChannel', + 'InsufficientPrivilege', + 'ReportableError', + 'Pagination', + 'InteractionPagination', + 'BaseTask', + 'Tasker', ) diff --git a/PyDiscoBot/types/admin_channels.py b/PyDiscoBot/types/admin_channels.py deleted file mode 100644 index 7e6d61d..0000000 --- a/PyDiscoBot/types/admin_channels.py +++ /dev/null @@ -1,25 +0,0 @@ -"""bot administrative channels - """ - -from dataclasses import dataclass -from typing import Optional -import discord - - -@dataclass -class AdminChannels: - """Administrative channels dataclass for the parent :class:`Bot`. - - .. ------------------------------------------------------------ - - Attributes - ----------- - admin: Optional[:class:`discord.abc.GuildChannel`] - Administrative channel where admin embed is posted. - - notification: Optional[:class:`discord.abc.GuildChannel`] - Notification channel where infos are posted - - """ - admin: Optional[discord.abc.GuildChannel] = None - notification: Optional[discord.abc.GuildChannel] = None diff --git a/PyDiscoBot/types/admin_info.py b/PyDiscoBot/types/admin_info.py deleted file mode 100644 index db0ea0b..0000000 --- a/PyDiscoBot/types/admin_info.py +++ /dev/null @@ -1,48 +0,0 @@ -"""bot administrative information - """ - -import os -from datetime import datetime -from .admin_channels import AdminChannels - - -class AdminInfo: - """Administrative information for the parent :class:`Bot` - - This data class describes the meta information about the bot. - - .. ------------------------------------------------------------ - - Attributes - ----------- - version: :class:`str` - Current running version the :class:`Bot` booted with. - - boot_time: :class:`datetime.datetime` - Datetime describing the moment the bot initialized this class. - - last_time: :class:`datetime.datetime` - Datetime describing the last time the :class:`Bot` ran it's administrative task. - - cycle_time: :class:`int` - Number of **seconds** between periodic bot ticks. - - current_tick: :class:`int` - Number describing the amount of ticks performed. - - channels: :class:`AdminChannels` - Administrative channels for the :class:`Bot` to use. - - """ - - def __init__(self): - self.version: str = os.getenv('VERSION', 'N/A?') - self.boot_time: datetime = datetime.now() - self.last_time: datetime = datetime.now() - try: - self.cycle_time: int = int(os.getenv('CYCLE_TIME', '600')) - except KeyError: - self.cycle_time: int = 600 # 10 minute default time - self.current_tick: int = 0 - self.channels: AdminChannels = AdminChannels - self.initialized: bool = False diff --git a/PyDiscoBot/types/bot.py b/PyDiscoBot/types/bot.py new file mode 100644 index 0000000..87b2d9c --- /dev/null +++ b/PyDiscoBot/types/bot.py @@ -0,0 +1,75 @@ +"""bot type module + """ +from __future__ import annotations + + +from logging import Logger +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from .status import Status + from .tasker import Tasker + + +__all__ = ( + 'BaseBot', +) + + +class BaseBot: + """Base bot type. + + .. ------------------------------------------------------------ + + Attributes + ----------- + admin_info: :class:`AdminInfo` + Administrative information for the bot, such as `ticks` and `version` + + logger: :class:`Logger` + Logging module for the bot. Displays information to terminal. + + tasker: :class:`Tasker` + Periodic task manager for the bot. Calls periodic tasks on `tick`. + + """ + _status: Status + _logger: Logger + _tasker: Tasker + + @property + def status(self) -> Status: + """ Administrative information for the bot, such as `ticks` and `version` + + .. ------------------------------------------------------------ + + Returns + ----------- + :class:`pydiscobot.types.AdminInfo` + """ + return self._status + + @property + def logger(self) -> Logger: + """Logging module for the bot. Displays information to terminal. + + .. ------------------------------------------------------------ + + Returns + ----------- + :class:`logging.Logger` + """ + return self._logger + + @property + def tasker(self) -> Tasker: + """Periodic task manager for the bot. Calls periodic tasks on `tick`. + + .. ------------------------------------------------------------ + + Returns + ----------- + :class:`pydiscobot.types.Tasker` + """ + return self._tasker diff --git a/PyDiscoBot/types/cmd.py b/PyDiscoBot/types/cmd.py deleted file mode 100644 index be9af5e..0000000 --- a/PyDiscoBot/types/cmd.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Common discord command - all discord commands should be derived from the meta class - """ -from __future__ import annotations - -from discord.ext import commands -from pydiscobot import bot - - -class Cmd(commands.Cog): - """Pseudo 'meta' discord bot `Command`. - - Though this is not an :class:`ABC`, this class describes the meta - relationship the command has within this environment. - - That being said, all commands used for the bot should be - derived from this 'meta' `Command` - - .. ------------------------------------------------------------ - - Arguments - ----------- - parent: :class:`pydiscobot.Bot` - The parent :class:`Bot` of this `Command`. - - .. ------------------------------------------------------------ - - Attributes - ----------- - parent: :class:`pydiscobot.Bot` - The parent :class:`Bot` of this `Command`. - - """ - - def __init__(self, - parent: bot.Bot): - self._parent = parent - - @property - def parent(self) -> bot.Bot: - """get parent of this command - - Returns: - bot.Bot: parent bot - """ - return self._parent diff --git a/PyDiscoBot/types/cog.py b/PyDiscoBot/types/cog.py new file mode 100644 index 0000000..c0a6c4c --- /dev/null +++ b/PyDiscoBot/types/cog.py @@ -0,0 +1,59 @@ +"""Common discord command + all discord commands should be derived from the meta class + """ +from __future__ import annotations + +from typing import TYPE_CHECKING + + +from discord.ext import commands + + +if TYPE_CHECKING: + from .bot import BaseBot + + +__all__ = ( + 'BaseCog', +) + + +class BaseCog(commands.Cog): + """Base `Cog` for PyDiscoBot commands. + + A :class:`Cog` is a container for discord `commands` and discord `app_commands`. + + Though this is not an :class:`ABC`, this class describes the meta + relationship the cog has within this environment. + + That being said, all commands used for the bot should be + derived from this 'meta' `Cog`. + + .. ------------------------------------------------------------ + + Arguments + ----------- + parent: :class:`Bot` + The parent :class:`Bot` of this `Cog`. + + .. ------------------------------------------------------------ + + Attributes + ----------- + parent: :class:`Bot` + The parent :class:`Bot` of this `Cog`. + + """ + _parent: BaseBot + + @property + def parent(self) -> BaseBot: + """ Parent container for this cog. + + .. ------------------------------------------------------------ + + Returns + ----------- + :class:`pydiscobot.types.BaseBot` + """ + return self._parent diff --git a/PyDiscoBot/types/embed_field.py b/PyDiscoBot/types/embed_field.py index 2ba2213..3b907b0 100644 --- a/PyDiscoBot/types/embed_field.py +++ b/PyDiscoBot/types/embed_field.py @@ -2,13 +2,39 @@ """ from __future__ import annotations + from dataclasses import dataclass +from typing import Optional + + +__all__ = ( + 'EmbedField', +) @dataclass class EmbedField: - """helper class to create embed fields more easily + """Embed field to inject into a :class:`discord.Embed`. + + .. ------------------------------------------------------------ + + Arguments + ----------- + name: :type:`str` + Name of the field to display. + + value: :type:`str` + Value to display in the field. + + inline: Optional[:type:`bool`] + Defaults to ``False``. + + Whether to display this field in-line with others. + + + .. ------------------------------------------------------------ + """ name: str value: str - inline: bool = False + inline: Optional[bool] = False diff --git a/PyDiscoBot/types/err.py b/PyDiscoBot/types/err.py index 6bab49a..1ad06a1 100644 --- a/PyDiscoBot/types/err.py +++ b/PyDiscoBot/types/err.py @@ -1,26 +1,34 @@ """provide error types for defining raise conditions """ +from __future__ import annotations + from discord.ext import commands +__all__ = ( + 'BotNotLoaded', + 'InsufficientPrivilege', + 'IllegalChannel', + 'ReportableError' +) + + class BotNotLoaded(commands.CheckFailure): """bot has not finished loading yet. """ - pass class InsufficientPrivilege(commands.CheckFailure): """user does not have correct privileges. """ - pass class IllegalChannel(commands.CheckFailure): """you cannot do that action in this channel. """ - pass class ReportableError(Exception): - pass + """reportable error to append text for notifications to + """ diff --git a/PyDiscoBot/types/mock/__init__.py b/PyDiscoBot/types/mock/__init__.py deleted file mode 100644 index ae1fbfc..0000000 --- a/PyDiscoBot/types/mock/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -"""PyDiscoBot built-in test types -Provide mock data and meta schemes to allow bot to test -Such as MockGuild, MockMessage, MockUser, MockInteraction, etc - """ -from . import mock_bot -from . import mock_channel -from . import mock_connection_state -from . import mock_guild -from . import mock_interaction -from . import mock_member -from . import mock_message -from . import mock_user - -__version__ = '1.1.4' - -__all__ = ( - 'mock_bot', - 'mock_channel', - 'mock_connection_state', - 'mock_guild', - 'mock_interaction', - 'mock_member', - 'mock_message', - 'mock_user', -) diff --git a/PyDiscoBot/types/mock/mock_bot.py b/PyDiscoBot/types/mock/mock_bot.py deleted file mode 100644 index 3edef2d..0000000 --- a/PyDiscoBot/types/mock/mock_bot.py +++ /dev/null @@ -1,48 +0,0 @@ -"""mock Bot - """ -from __future__ import annotations - -import asyncio -import os -from typing import Self -import discord -import dotenv -from pydiscobot import bot -from .mock_user import MockUserData -from .mock_connection_state import MockConnectionState - - -class MockBot(bot.Bot): - """Mock :class:`Bot` to be used in unittesting - """ - - @classmethod - def as_mock(cls) -> Self: - """get this bot as a generated mock bot - - Returns: - Self: an instance of bot - """ - dir_path = os.path.dirname(os.path.realpath(__file__)) - dotenv.load_dotenv(f'{dir_path}\\.testenv') - intents = discord.Intents(8) - intents.guilds = True - intents.members = True - intents.message_content = True - intents.messages = True - b = cls('!', intents, []) - data = MockUserData.generic() - data['id'] = 12341234 # change this in case any checks on the default user happen against the bot - b._connection.user = discord.User(state=MockConnectionState(), data=data) - return b - - @classmethod - def as_ready(cls) -> Self: - """get this mock bot as an already initialized bot - - Returns: - Self: initialized mock bot - """ - x = cls.as_mock() - asyncio.run(x.on_ready(True)) - return x diff --git a/PyDiscoBot/types/mock/mock_channel.py b/PyDiscoBot/types/mock/mock_channel.py deleted file mode 100644 index 588aa57..0000000 --- a/PyDiscoBot/types/mock/mock_channel.py +++ /dev/null @@ -1,41 +0,0 @@ -"""mock Discord Text Channel - """ -import discord -from .mock_guild import MockGuild -from .mock_connection_state import MockConnectionState - - -class MockChannel(discord.TextChannel): - """mock Discord Text Channel - """ - channel_id = 69420 - channel_name = 'Big Doinkers Anon.' - guild = MockGuild() - state = MockConnectionState() - - def __init__(self, - name: str | None = None, - chid: int | None = None, - guild: discord.Guild | None = None): - data = { - 'id': MockChannel.channel_id if not chid else chid, - 'name': MockChannel.name if not name else name, - 'type': discord.channel.TextChannel, - 'position': 0, - 'guild': MockChannel.guild if not guild else guild - } - super().__init__(state=MockChannel.state, guild=data['guild'], data=data) - - async def delete_messages(self, - *args, - **kwargs): - """dummy method - """ - - async def history(self, - *args, - **kwargs): - """dummy method - """ - for _ in range(0, 0): - yield None diff --git a/PyDiscoBot/types/mock/mock_connection_state.py b/PyDiscoBot/types/mock/mock_connection_state.py deleted file mode 100644 index 8852fa5..0000000 --- a/PyDiscoBot/types/mock/mock_connection_state.py +++ /dev/null @@ -1,11 +0,0 @@ -"""mock connection state to be used for unit testing - """ -from discord.state import ConnectionState - - -class MockConnectionState(ConnectionState): - """mock connection state to be used for unit testing - """ - - def __init__(self): - super().__init__(dispatch=None, handlers=None, hooks=None, http=None) diff --git a/PyDiscoBot/types/mock/mock_guild.py b/PyDiscoBot/types/mock/mock_guild.py deleted file mode 100644 index d803e9e..0000000 --- a/PyDiscoBot/types/mock/mock_guild.py +++ /dev/null @@ -1,27 +0,0 @@ -"""mock Discord Guild - """ -import discord -from discord.state import ConnectionState - - -class MockGuild(discord.Guild): - """mock Discord Guild - """ - guild_member_count = 69 - guild_name = 'Test Guild' - guild_id = 1234567890 - guild_owner_id = 123123123123 - mock_data = { - 'member_count': guild_member_count, - 'name': guild_name, - 'verification_level': 0, - 'notification_level': 0, - 'id': guild_id, - 'owner_id': guild_owner_id, - } - - def __init__(self): - super().__init__(data=MockGuild.mock_data, state=ConnectionState(dispatch=None, - handlers=None, - hooks=None, - http=None)) diff --git a/PyDiscoBot/types/mock/mock_interaction.py b/PyDiscoBot/types/mock/mock_interaction.py deleted file mode 100644 index 8b2594d..0000000 --- a/PyDiscoBot/types/mock/mock_interaction.py +++ /dev/null @@ -1,27 +0,0 @@ -"""make-shift interaction datas to support unit testing - """ -from typing import Optional -from .mock_channel import MockChannel - - -class MockInteractionResponse: - """make-shift interaction response to support unit-testing - """ - - def __init__(self, - send_message_cb: Optional[callable] = None): - self.send_message: callable = send_message_cb - - async def defer(self): - """dummy callback for interaction response -> defer - """ - - -class MockInteraction: - """make-shift interaction to support unit-testing - """ - - def __init__(self, - send_message_cb: Optional[callable] = None): - self.channel = MockChannel() - self.response: MockInteractionResponse = MockInteractionResponse(send_message_cb) diff --git a/PyDiscoBot/types/mock/mock_member.py b/PyDiscoBot/types/mock/mock_member.py deleted file mode 100644 index a8c8de6..0000000 --- a/PyDiscoBot/types/mock/mock_member.py +++ /dev/null @@ -1,42 +0,0 @@ -"""mock Discord Member - """ -import discord -from .mock_guild import MockGuild -from .mock_connection_state import MockConnectionState - - -class MockMember(discord.Member): - """mock Discord Member - """ - member_id = 69420 - member_name = 'Big Doinkers Anon.' - guild = MockGuild() - connection = MockConnectionState() - - def __init__(self, - name: str | None = None, - memid: int | None = None, - guild: discord.Guild | None = None): - data = { - 'name': MockMember.member_name if not name else name, - 'guild': MockMember.guild if not guild else guild, - 'roles': [], - 'flags': 0, - 'user': { - 'username': MockMember.member_name if not name else name, - 'discriminator': 'your mom?', - 'avatar': None, - 'global_name': None, - 'id': MockMember.member_id if not memid else memid, - 'bot': False, - 'system': False, - 'mfa_enabled': False, - 'locale': 'en-US', - 'verified': False, - 'email': None, - 'flags': 0, - 'premium_type': 0, - 'public_flags': 0, - } - } - super().__init__(data=data, guild=data['guild'], state=MockMember.connection) diff --git a/PyDiscoBot/types/mock/mock_message.py b/PyDiscoBot/types/mock/mock_message.py deleted file mode 100644 index 2498e02..0000000 --- a/PyDiscoBot/types/mock/mock_message.py +++ /dev/null @@ -1,81 +0,0 @@ -"""mock Discord Message - """ -import datetime -from typing import Optional, TypedDict, Union -from typing_extensions import NotRequired -import discord -from .mock_connection_state import MockConnectionState -from .mock_channel import MockChannel -from .mock_user import MockUserData - - -class MockMessageData(TypedDict): - """mock message data - """ - id: int - author: discord.User - content: str - timestamp: str - edited_timestamp: Optional[str] - tts: bool - mention_everyone: bool - mentions: list[discord.User] - mention_roles: list[int] - attachments: list[discord.Attachment] - embeds: list[discord.Embed] - pinned: bool - type: discord.MessageType - member: NotRequired[discord.Member] - mention_channels: NotRequired[list[dict]] - reactions: NotRequired[list[discord.Reaction]] - nonce: NotRequired[Union[int, str]] - webhook_id: NotRequired[int] - activity: NotRequired[dict] - application: NotRequired[discord.MessageApplication] - application_id: NotRequired[int] - message_reference: NotRequired[discord.MessageReference] - flags: NotRequired[int] - sticker_items: NotRequired[list[discord.StickerItem]] - referenced_message: NotRequired[Optional[discord.Message]] - interaction: NotRequired[discord.MessageInteraction] - components: NotRequired[list[discord.Component]] - position: NotRequired[int] - role_subscription_data: NotRequired[dict] - - @classmethod - def generic(cls): - """get data as generic - - Returns: - dict: data - """ - return cls({ - 'id': 123456, - 'author': MockUserData.generic(), - 'attachments': [], - 'embeds': [], - 'edited_timestamp': datetime.datetime.now().isoformat(), - 'type': discord.MessageType.chat_input_command, - 'pinned': False, - 'mention_everyone': False, - 'tts': False, - 'content': 'this is some content, right?', - }) - - -class MockMessage(discord.Message): - """mock Discord Message - """ - - def __init__(self, *, - state=None, - channel=None, - data: MockMessageData = None): - if not state: - state = MockConnectionState() - if not channel: - channel = MockChannel() - if not data: - data = MockMessageData.generic() - - super().__init__(state=state, channel=channel, data=data) diff --git a/PyDiscoBot/types/mock/mock_user.py b/PyDiscoBot/types/mock/mock_user.py deleted file mode 100644 index 36599b6..0000000 --- a/PyDiscoBot/types/mock/mock_user.py +++ /dev/null @@ -1,59 +0,0 @@ -"""mock Discord User - """ -from typing import Optional, Literal, TypedDict -import discord -from .mock_connection_state import MockConnectionState - - -class MockUserData(TypedDict): - """mock message data - """ - username: Optional[str] - discriminator: Optional[str] - avatar: None - global_name: None - id: int - bot: bool - system: bool - mfa_enabled: bool - locale: str - verified: bool - email: str - flags: int - premium_type: Literal[0, 1, 2] - public_flags: int - - @classmethod - def generic(cls): - """get data as generic - - Returns: - dict: data - """ - return cls({ - 'username': 'beef', - 'discriminator': 'your mom?', - 'avatar': None, - 'global_name': None, - 'id': 696969420, - 'bot': False, - 'system': False, - 'mfa_enabled': False, - 'locale': 'en-US', - 'verified': False, - 'email': None, - 'flags': 0, - 'premium_type': 0, - 'public_flags': 0, - }) - - -class MockUser(discord.User): - """mock Discord User - """ - - def __init__(self, - data: Optional[dict]): - if not data: - data = MockUserData.generic() - super().__init__(state=MockConnectionState(), data=data) diff --git a/PyDiscoBot/types/pagination.py b/PyDiscoBot/types/pagination.py index b5acf74..bf829a5 100644 --- a/PyDiscoBot/types/pagination.py +++ b/PyDiscoBot/types/pagination.py @@ -1,10 +1,20 @@ """create a scrollable / interactable callback display for user interaction """ +from __future__ import annotations + from typing import Callable, Optional + + import discord +__all__ = ( + 'Pagination', + 'InteractionPagination', +) + + class Pagination(discord.ui.View): """interactable page scrolling view """ diff --git a/PyDiscoBot/types/status.py b/PyDiscoBot/types/status.py new file mode 100644 index 0000000..5fefa67 --- /dev/null +++ b/PyDiscoBot/types/status.py @@ -0,0 +1,158 @@ +"""status module + """ +from __future__ import annotations + + +import os +from datetime import datetime +from typing import Optional + + +import discord + + +__all__ = ( + 'Status', +) + + +class _StatusChannels: + """Status channels for :class:`Status`. + + .. ------------------------------------------------------------ + + Attributes + ----------- + admin: Optional[:class:`discord.abc.GuildChannel`] + Channel where admin embed is posted. + + notification: Optional[:class:`discord.abc.GuildChannel`] + Channel where notifications are posted. + + """ + admin: Optional[discord.abc.GuildChannel] = None + notification: Optional[discord.abc.GuildChannel] = None + + +class Status: + """Status container for the parent :class:`Bot` + + This data class describes the meta information about bot status. + + .. ------------------------------------------------------------ + + Attributes + ----------- + version: :class:`str` + Current running version the :class:`Bot` booted with. + + boot_time: :class:`datetime.datetime` + Datetime describing the moment the bot initialized this class. + + last_time: :class:`datetime.datetime` + Datetime describing the last time the :class:`Bot` ran it's administrative task. + + cycle_time: :class:`int` + Number of **seconds** between periodic bot ticks. + + current_tick: :class:`int` + Number describing the amount of ticks performed. + + channels: :class:`_StatusChannels` + Status channels for the :class:`Bot` to use. + + initialized: :type:'bool' + Bot has been initialized memory. + + """ + + def __init__(self): + self._version: str = os.getenv('VERSION', 'N/A?') + self._boot_time: datetime = datetime.now() + self._last_time: datetime = datetime.now() + self._tick: int = 0 + self._channels: _StatusChannels = _StatusChannels() + self._initialized: bool = False + + try: + self._cycle_time: int = int(os.getenv('CYCLE_TIME', '600')) + except KeyError: + self._cycle_time: int = 600 # 10 minute default time + + @property + def version(self) -> str: + """ Current running version the :class:`Bot` booted with. + + Returns + ----------- + :type:`str` + """ + return self._version + + @property + def boot_time(self) -> datetime: + """ Datetime describing the moment the bot initialized this class. + + Returns + ----------- + :class:`datetime.datetime` + """ + return self._boot_time + + @property + def last_time(self) -> datetime: + """ Datetime describing the last time the :class:`Bot` ran it's administrative task. + + Returns + ----------- + :class:`datetime.datetime` + """ + return self._last_time + + @property + def current_tick(self) -> int: + """ Number describing the amount of ticks performed. + + Returns + ----------- + :type:`int` + """ + return self._tick + + @property + def channels(self) -> _StatusChannels: + """ Status channels for the :class:`Bot` to use. + + Returns + ----------- + :class:`pydiscobot.types.status._StatusChannels` + """ + return self._channels + + @property + def initialized(self) -> bool: + """ Bot has been initialized memory. + + Returns + ----------- + :type:`bool` + """ + return self._initialized + + @property + def cycle_time(self) -> int: + """ Number of **seconds** between periodic bot ticks. + + Returns + ----------- + :type:`int` + """ + return self._cycle_time + + def tick(self) -> None: + """Increment the `current_tick` by +1 + + Also, updates the last time to `datetime.now()` + """ + self._tick += 1 + self._last_time = datetime.now() diff --git a/PyDiscoBot/types/task.py b/PyDiscoBot/types/task.py index 8ef3643..c5db7f5 100644 --- a/PyDiscoBot/types/task.py +++ b/PyDiscoBot/types/task.py @@ -2,16 +2,21 @@ """ from __future__ import annotations -from abc import ABC, abstractmethod -from logging import Logger -from pydiscobot import bot -from pydiscobot.services import log +from typing import TYPE_CHECKING -class Task(ABC): - """:class:`ABC` abstract class for :class:`Bot` tasks. - All tasks should be derived from this abstract :class:`Task`. +if TYPE_CHECKING: + from .bot import BaseBot + + +__all__ = ( + 'BaseTask', +) + + +class BaseTask: + """Base class for :class:`Task`. .. ------------------------------------------------------------ @@ -32,34 +37,11 @@ class Task(ABC): .. ------------------------------------------------------------ - Examples - ---------- - - Create a child :type:`class` that inherits this class :class:`Task` - - .. code-block:: python - - import discord - from pydiscobot.types import Task - - class MyTask(Task): - 'this is my task class!' - - def __init__(self, - parent: pydiscobot.Bot): - super().__init__(parent) - ... - - def run(self): - 'this override gets triggered by the bot every tick!' - ... - """ def __init__(self, - parent: bot.Bot): - self._parent: bot.Bot = parent - self._logger = log.logger(self.__class__.__name__) + parent: BaseBot): + self._parent: BaseBot = parent @property def name(self) -> str: @@ -76,21 +58,7 @@ def name(self) -> str: return self.__class__.__name__ @property - def logger(self) -> Logger: - """Get the :class:`Logger` of this :class:`Task`. - - .. ------------------------------------------------------------ - - Returns - ----------- - logger: :class:`Logger` - The :class:`Logger` of this :class:`Task`. - - """ - return self._logger - - @property - def parent(self) -> bot.Bot: + def parent(self) -> BaseBot: """Get the :class:`pydiscobot.Bot` of this :class:`Task`. .. ------------------------------------------------------------ @@ -102,37 +70,3 @@ def parent(self) -> bot.Bot: """ return self._parent - - @abstractmethod - async def run(self): - """Abstract method that is called by :class:`pydiscobot.Bot` during it's ticks. - - A class inheriting this task must override this method. - - .. ------------------------------------------------------------ - - Example - ---------- - - Create a child :type:`class` that inherits this class :class:`Task` - and overrides the :callable:`run` function. - - .. code-block:: python - - import discord - from pydiscobot.types import Task - - class MyTask(Task): - 'this is my task class!' - - def __init__(self, - parent: pydiscobot.Bot): - super().__init__(parent) - ... # initialize the task - - def run(self): - 'this override gets triggered by the bot every tick!' - ... # do some logic - - - """ diff --git a/PyDiscoBot/types/tasker.py b/PyDiscoBot/types/tasker.py index 961749d..d058a20 100644 --- a/PyDiscoBot/types/tasker.py +++ b/PyDiscoBot/types/tasker.py @@ -1,11 +1,24 @@ """bot tasker to periodically run assigned tasks """ +from __future__ import annotations + + +import unittest + from discord.ext import tasks -from .task import Task -class Tasker(list[Task]): +from .task import BaseTask + + +__all__ = ( + 'Tasker', + 'TestTasker', +) + + +class Tasker(list[BaseTask]): """task manager for PyDiscoBot """ @@ -13,18 +26,21 @@ def __init__(self, *args): super().__init__(*args) self._task_hash: dict = {} - def append(self, task: Task): + def __contains__(self, item: BaseTask): + return self._task_hash.get(item, None) is not None + + def append(self, task: BaseTask): if task not in self: super().append(task) self._task_hash[task.name] = task def remove(self, - value: Task): + value: BaseTask): self._task_hash.pop(value.name, None) super().remove(value) def by_name(self, - task_name: str) -> Task | None: + task_name: str) -> BaseTask | None: """get Task by name Args: @@ -43,3 +59,31 @@ async def run(self) -> bool: bool: all tasks completed successfully """ _ = [await task.run() for task in self] + + +class TestTasker(unittest.TestCase): + """test tasker operations + """ + + def test_tasker(self): + """tbd + """ + class BingBong(BaseTask): + pass + + class DingDong(BaseTask): + pass + + task_a = BingBong(None) + task_b = DingDong(None) + + tasker = Tasker() + tasker.append(task_a) + self.assertTrue(len(tasker) == 1) + self.assertIsNotNone(tasker.by_name('BingBong')) + tasker.append(task_b) + self.assertTrue(len(tasker) == 2) + self.assertIsNotNone(tasker.by_name('DingDong')) + tasker.append(task_b) + self.assertTrue(len(tasker) == 2) + self.assertIsNotNone(tasker.by_name('DingDong')) diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..92f44fb --- /dev/null +++ b/build.sh @@ -0,0 +1,10 @@ +source .venv/Scripts/activate + +python -m pip install pip --upgrade + +python -m pip install "PathTo...\PyDiscoBot\." --upgrade +python -m pip install "PathTo...\MLEBot\." --upgrade + + + + read -p "Press Enter to continue..." \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..8d773d5 --- /dev/null +++ b/test.sh @@ -0,0 +1,5 @@ +cd "$(dirname "$0")" + +pytest + + read -p "Press Enter to continue..." \ No newline at end of file From 42aa2076f4b8b23a5026e58b41838ec9b870b2b5 Mon Sep 17 00:00:00 2001 From: Brian LaFond <52360893+iroxusux@users.noreply.github.com> Date: Wed, 16 Apr 2025 16:53:17 -0400 Subject: [PATCH 3/4] removal of .vscode folder, update of test.sh removal of build (will go in MLEBot instead) updated .gitignore to now ignore .vscode directories --- .gitignore | 3 +++ .vscode/settings.json | 12 ------------ build.sh | 10 ---------- test.sh | 3 ++- 4 files changed, 5 insertions(+), 23 deletions(-) delete mode 100644 .vscode/settings.json delete mode 100644 build.sh diff --git a/.gitignore b/.gitignore index 99b908f..dc61549 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ __pycache__/ *.py[cod] *$py.class +# vscode environment +.vscode/ + # bat extension *.bat diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 3cf78df..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "python.testing.unittestArgs": [ - "-v", - "-s", - ".", - "-p", - "test_*.py" - ], - "python.testing.pytestEnabled": true, - "python.testing.unittestEnabled": false - -} \ No newline at end of file diff --git a/build.sh b/build.sh deleted file mode 100644 index 92f44fb..0000000 --- a/build.sh +++ /dev/null @@ -1,10 +0,0 @@ -source .venv/Scripts/activate - -python -m pip install pip --upgrade - -python -m pip install "PathTo...\PyDiscoBot\." --upgrade -python -m pip install "PathTo...\MLEBot\." --upgrade - - - - read -p "Press Enter to continue..." \ No newline at end of file diff --git a/test.sh b/test.sh index 8d773d5..be77a9b 100644 --- a/test.sh +++ b/test.sh @@ -1,4 +1,5 @@ -cd "$(dirname "$0")" +# change this to your .venv directory +source C:/Users/brian/Documents/vscode/workspaces/MinorLeagueEsports/.venv/Scripts/activate pytest From d7d9017da1aa592c788508b08f2987f6bc23ffc8 Mon Sep 17 00:00:00 2001 From: Brian LaFond <52360893+iroxusux@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:00:37 -0400 Subject: [PATCH 4/4] Final changes to get MLEBot posting again. This ends this branch. --- PyDiscoBot/__init__.py | 18 ++++++++++++------ PyDiscoBot/bot.py | 3 ++- PyDiscoBot/types/status.py | 4 ++++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/PyDiscoBot/__init__.py b/PyDiscoBot/__init__.py index 2c5da5a..ef03a43 100644 --- a/PyDiscoBot/__init__.py +++ b/PyDiscoBot/__init__.py @@ -4,28 +4,34 @@ commands, tasks, types, - bot, channels, - cog, const, frame, log, - task, test_pydiscobot ) + +from .bot import Bot +from .cog import Cog +from .task import Task +from .types import EmbedField, InteractionPagination + __version__ = "1.1.4" __all__ = ( 'commands', 'tasks', 'types', - 'bot', 'channels', - 'cog', 'const', 'frame', 'log', 'task', - 'test_pydiscobot' + 'test_pydiscobot', + 'Bot', + 'Cog', + 'EmbedField', + 'InteractionPagination', + 'Task', ) diff --git a/PyDiscoBot/bot.py b/PyDiscoBot/bot.py index 868131d..b733b6c 100644 --- a/PyDiscoBot/bot.py +++ b/PyDiscoBot/bot.py @@ -225,8 +225,9 @@ async def send_notification(self, Otherwise, this WILL raise an `Exception` """ + embed = get_notification_frame(msg) + if isinstance(dest, commands.Context): - embed = get_notification_frame(msg) if as_reply and dest.author is not None: await dest.reply(embed=embed) else: diff --git a/PyDiscoBot/types/status.py b/PyDiscoBot/types/status.py index 5fefa67..cf10bd5 100644 --- a/PyDiscoBot/types/status.py +++ b/PyDiscoBot/types/status.py @@ -139,6 +139,10 @@ def initialized(self) -> bool: """ return self._initialized + @initialized.setter + def initialized(self, value) -> None: + self._initialized = value + @property def cycle_time(self) -> int: """ Number of **seconds** between periodic bot ticks.