diff --git a/disagreement/__init__.py b/disagreement/__init__.py index 2498fc2..ac477ee 100644 --- a/disagreement/__init__.py +++ b/disagreement/__init__.py @@ -67,6 +67,47 @@ from .hybrid_context import HybridContext from .interactions import Interaction from .logging_config import setup_logging from . import ui, ext +from .ext.app_commands import ( + AppCommand, + AppCommandContext, + AppCommandGroup, + MessageCommand, + OptionMetadata, + SlashCommand, + UserCommand, + group, + hybrid_command, + message_command, + slash_command, + subcommand, + subcommand_group, +) +from .ext.commands import ( + BadArgument, + CheckAnyFailure, + CheckFailure, + Cog, + Command, + CommandContext, + CommandError, + CommandInvokeError, + CommandNotFound, + CommandOnCooldown, + MaxConcurrencyReached, + MissingRequiredArgument, + ArgumentParsingError, + check, + check_any, + command, + cooldown, + has_any_role, + has_role, + listener, + max_concurrency, + requires_permissions, +) +from .ext.tasks import Task, loop +from .ui import Item, Modal, Select, TextInput, View, button, select, text_input import logging @@ -123,6 +164,51 @@ __all__ = [ "setup_logging", "ui", "ext", + "AppCommand", + "AppCommandContext", + "AppCommandGroup", + "MessageCommand", + "OptionMetadata", + "SlashCommand", + "UserCommand", + "group", + "hybrid_command", + "message_command", + "slash_command", + "subcommand", + "subcommand_group", + "BadArgument", + "CheckAnyFailure", + "CheckFailure", + "Cog", + "Command", + "CommandContext", + "CommandError", + "CommandInvokeError", + "CommandNotFound", + "CommandOnCooldown", + "MaxConcurrencyReached", + "MissingRequiredArgument", + "ArgumentParsingError", + "check", + "check_any", + "command", + "cooldown", + "has_any_role", + "has_role", + "listener", + "max_concurrency", + "requires_permissions", + "Task", + "loop", + "Item", + "Modal", + "Select", + "TextInput", + "View", + "button", + "select", + "text_input", ] diff --git a/docs/commands.md b/docs/commands.md index 989d37e..ef312ec 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -20,7 +20,7 @@ returns ``True``. Checks may be regular or async callables that accept a `CommandContext`. ```python -from disagreement.ext.commands import command, check, CheckFailure +from disagreement import command, check, CheckFailure def is_owner(ctx): return ctx.author.id == "1" @@ -40,7 +40,7 @@ Commands can be rate limited using the ``cooldown`` decorator. The example below restricts usage to once every three seconds per user: ```python -from disagreement.ext.commands import command, cooldown +from disagreement import command, cooldown @command() @cooldown(1, 3.0) @@ -56,7 +56,7 @@ Use `commands.requires_permissions` to ensure the invoking member has the required permissions in the channel. ```python -from disagreement.ext.commands import command, requires_permissions +from disagreement import command, requires_permissions from disagreement import Permissions @command() diff --git a/docs/context_menus.md b/docs/context_menus.md index 81e0cb9..3e9219d 100644 --- a/docs/context_menus.md +++ b/docs/context_menus.md @@ -1,12 +1,11 @@ # Context Menu Commands `disagreement` supports Discord's user and message context menu commands. Use the -`user_command` and `message_command` decorators from `ext.app_commands` to -define them. +`user_command` and `message_command` decorators to define them. ```python from disagreement import User, Message -from disagreement.ext.app_commands import user_command, message_command, AppCommandContext +from disagreement import User, Message, user_command, message_command, AppCommandContext @user_command(name="User Info") async def user_info(ctx: AppCommandContext, user: User) -> None: diff --git a/docs/slash_commands.md b/docs/slash_commands.md index 888a344..09d68a0 100644 --- a/docs/slash_commands.md +++ b/docs/slash_commands.md @@ -1,9 +1,9 @@ # Using Slash Commands -The library provides a slash command framework via the `ext.app_commands` package. Define commands with decorators and register them with Discord. +The library provides a slash command framework to define commands with decorators and register them with Discord. ```python -from disagreement.ext.app_commands import AppCommandGroup +from disagreement import AppCommandGroup bot_commands = AppCommandGroup("bot", "Bot commands") diff --git a/docs/task_loop.md b/docs/task_loop.md index c60a689..3eba198 100644 --- a/docs/task_loop.md +++ b/docs/task_loop.md @@ -1,11 +1,11 @@ # Task Loops -The tasks extension allows you to run functions periodically. Decorate an async function with `@tasks.loop` and start it using `.start()`. +The tasks extension allows you to run functions periodically. Decorate an async function with `@loop` and start it using `.start()`. ```python -from disagreement.ext import tasks +from disagreement import loop -@tasks.loop(minutes=1.0) +@loop(minutes=1.0) async def announce(): print("Hello from a loop") @@ -19,7 +19,7 @@ You can provide the interval in seconds, minutes, hours or as a `datetime.timede ```python import datetime -@tasks.loop(delta=datetime.timedelta(seconds=30)) +@loop(delta=datetime.timedelta(seconds=30)) async def ping(): ... ``` @@ -30,7 +30,7 @@ Handle exceptions raised by the looped coroutine using `on_error`: async def log_error(exc: Exception) -> None: print("Loop failed:", exc) -@tasks.loop(seconds=5.0, on_error=log_error) +@loop(seconds=5.0, on_error=log_error) async def worker(): ... ``` @@ -38,7 +38,7 @@ async def worker(): Run setup and teardown code using `before_loop` and `after_loop`: ```python -@tasks.loop(seconds=5.0) +@loop(seconds=5.0) async def worker(): ... @@ -58,7 +58,7 @@ from datetime import datetime, timedelta time_to_run = (datetime.now() + timedelta(seconds=5)).time() -@tasks.loop(time_of_day=time_to_run) +@loop(time_of_day=time_to_run) async def daily_task(): ... ``` diff --git a/examples/basic_bot.py b/examples/basic_bot.py index 53fead3..f09ed3b 100644 --- a/examples/basic_bot.py +++ b/examples/basic_bot.py @@ -32,11 +32,12 @@ try: GatewayIntent, Message, Guild, - ext, AuthenticationError, DisagreementException, + Cog, + command, + CommandContext, ) - from disagreement.ext import commands except ImportError: print( "Failed to import disagreement. Make sure it's installed or PYTHONPATH is set correctly." @@ -87,26 +88,26 @@ client = Client(token=BOT_TOKEN, intents=intents, command_prefix="!") # --- Define a Cog for example commands --- -class ExampleCog(commands.Cog): # Ensuring this uses commands.Cog +class ExampleCog(Cog): # Ensuring this uses commands.Cog def __init__( self, bot_client ): # Renamed client to bot_client to avoid conflict with self.client super().__init__(bot_client) # Pass the client instance to the base Cog - @commands.command(name="hello", aliases=["hi"]) - async def hello_command(self, ctx: commands.CommandContext, *, who: str = "world"): + @command(name="hello", aliases=["hi"]) + async def hello_command(self, ctx: CommandContext, *, who: str = "world"): """Greets someone.""" await ctx.reply(f"Hello {ctx.author.mention} and {who}!") print(f"Executed 'hello' command for {ctx.author.username}, greeting {who}") - @commands.command() - async def ping(self, ctx: commands.CommandContext): + @command() + async def ping(self, ctx: CommandContext): """Responds with Pong!""" await ctx.reply("Pong!") print(f"Executed 'ping' command for {ctx.author.username}") - @commands.command() - async def me(self, ctx: commands.CommandContext): + @command() + async def me(self, ctx: CommandContext): """Shows information about the invoking user.""" reply_content = ( f"Hello {ctx.author.mention}!\n" @@ -117,8 +118,8 @@ class ExampleCog(commands.Cog): # Ensuring this uses commands.Cog await ctx.reply(reply_content) print(f"Executed 'me' command for {ctx.author.username}") - @commands.command(name="add") - async def add_numbers(self, ctx: commands.CommandContext, num1: int, num2: int): + @command(name="add") + async def add_numbers(self, ctx: CommandContext, num1: int, num2: int): """Adds two numbers.""" result = num1 + num2 await ctx.reply(f"The sum of {num1} and {num2} is {result}.") @@ -126,16 +127,16 @@ class ExampleCog(commands.Cog): # Ensuring this uses commands.Cog f"Executed 'add' command for {ctx.author.username}: {num1} + {num2} = {result}" ) - @commands.command(name="say") - async def say_something(self, ctx: commands.CommandContext, *, text_to_say: str): + @command(name="say") + async def say_something(self, ctx: CommandContext, *, text_to_say: str): """Repeats the text you provide.""" await ctx.reply(f"You said: {text_to_say}") print( f"Executed 'say' command for {ctx.author.username}, saying: {text_to_say}" ) - @commands.command(name="whois") - async def whois(self, ctx: commands.CommandContext, *, name: str): + @command(name="whois") + async def whois(self, ctx: CommandContext, *, name: str): """Looks up a member by username or nickname using the guild cache.""" if not ctx.guild: await ctx.reply("This command can only be used in a guild.") @@ -149,8 +150,8 @@ class ExampleCog(commands.Cog): # Ensuring this uses commands.Cog else: await ctx.reply("Member not found in cache.") - @commands.command(name="quit") - async def quit_command(self, ctx: commands.CommandContext): + @command(name="quit") + async def quit_command(self, ctx: CommandContext): """Shuts down the bot (requires YOUR_USER_ID to be set).""" # Replace YOUR_USER_ID with your actual Discord User ID for a safe shutdown command your_user_id = "YOUR_USER_ID_REPLACE_ME" # IMPORTANT: Replace this diff --git a/examples/component_bot.py b/examples/component_bot.py index b2994e3..43e1de3 100644 --- a/examples/component_bot.py +++ b/examples/component_bot.py @@ -3,8 +3,6 @@ import asyncio from typing import Union from disagreement import ( Client, - ui, - ext, HybridContext, Message, SelectOption, @@ -27,9 +25,14 @@ from disagreement import ( ChannelType, MessageFlags, Interaction, + Cog, + CommandContext, + AppCommandContext, + hybrid_command, + View, + button, + select, ) -from disagreement.ext.commands import Cog, CommandContext -from disagreement.ext.app_commands import AppCommandContext, hybrid_command try: from dotenv import load_dotenv @@ -69,12 +72,12 @@ STOCKS = [ # Define a View class that contains our components -class MyView(ui.View): +class MyView(View): def __init__(self): super().__init__(timeout=180) # 180-second timeout self.click_count = 0 - @ui.button(label="Click Me!", style=ButtonStyle.SUCCESS, emoji="🖱️") + @button(label="Click Me!", style=ButtonStyle.SUCCESS, emoji="🖱️") async def click_me(self, interaction: Interaction): self.click_count += 1 await interaction.respond( @@ -82,7 +85,7 @@ class MyView(ui.View): ephemeral=True, ) - @ui.select( + @select( custom_id="string_select", placeholder="Choose an option", options=[ @@ -108,12 +111,12 @@ class MyView(ui.View): # View for cycling through available stocks -class StockView(ui.View): +class StockView(View): def __init__(self): super().__init__(timeout=180) self.index = 0 - @ui.button(label="Next Stock", style=ButtonStyle.PRIMARY) + @button(label="Next Stock", style=ButtonStyle.PRIMARY) async def next_stock(self, interaction: Interaction): self.index = (self.index + 1) % len(STOCKS) stock = STOCKS[self.index] diff --git a/examples/example_from_readme.py b/examples/example_from_readme.py index 3d7d4a5..b67e8f5 100644 --- a/examples/example_from_readme.py +++ b/examples/example_from_readme.py @@ -4,19 +4,18 @@ import asyncio import os -from disagreement import Client, GatewayIntent -from disagreement.ext import commands +from disagreement import Client, GatewayIntent, Cog, command, CommandContext from dotenv import load_dotenv load_dotenv() -class Basics(commands.Cog): +class Basics(Cog): def __init__(self, client: Client) -> None: super().__init__(client) - @commands.command() - async def ping(self, ctx: commands.CommandContext) -> None: + @command() + async def ping(self, ctx: CommandContext) -> None: await ctx.reply(f"Pong! Gateway Latency: {self.client.latency_ms} ms.") # type: ignore (latency is None during static analysis) diff --git a/examples/moderation_bot.py b/examples/moderation_bot.py index 8143985..4b8dc2a 100644 --- a/examples/moderation_bot.py +++ b/examples/moderation_bot.py @@ -9,8 +9,7 @@ from typing import Set if os.path.join(os.getcwd(), "examples") == os.path.dirname(os.path.abspath(__file__)): sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -from disagreement import Client, GatewayIntent, Member, Message -from disagreement.ext import commands +from disagreement import Client, GatewayIntent, Member, Message, Cog, command, CommandContext try: from dotenv import load_dotenv @@ -37,21 +36,21 @@ client = Client(token=BOT_TOKEN, command_prefix="!", intents=intents) BANNED_WORDS: Set[str] = {"badword1", "badword2"} -class ModerationCog(commands.Cog): +class ModerationCog(Cog): def __init__(self, bot: Client) -> None: super().__init__(bot) - @commands.command() + @command() async def kick( - self, ctx: commands.CommandContext, member: Member, *, reason: str = "" + self, ctx: CommandContext, member: Member, *, reason: str = "" ) -> None: """Kick a member from the guild.""" await member.kick(reason=reason or None) await ctx.reply(f"Kicked {member.display_name}") - @commands.command() + @command() async def ban( - self, ctx: commands.CommandContext, member: Member, *, reason: str = "" + self, ctx: CommandContext, member: Member, *, reason: str = "" ) -> None: """Ban a member from the guild.""" await member.ban(reason=reason or None) diff --git a/examples/reactions.py b/examples/reactions.py index 6912721..8e87991 100644 --- a/examples/reactions.py +++ b/examples/reactions.py @@ -33,8 +33,10 @@ try: Member, HTTPException, AuthenticationError, + Cog, + command, + CommandContext, ) - from disagreement.ext import commands except ImportError: print( "Failed to import disagreement. Make sure it's installed or PYTHONPATH is set correctly." @@ -72,12 +74,12 @@ client = Client(token=BOT_TOKEN, intents=intents, command_prefix="!") # --- Define a Cog for reaction-related commands --- -class ReactionCog(commands.Cog): +class ReactionCog(Cog): def __init__(self, bot_client): super().__init__(bot_client) - @commands.command(name="react") - async def react_command(self, ctx: commands.CommandContext): + @command(name="react") + async def react_command(self, ctx: CommandContext): """Reacts to the command message with a thumbs up.""" try: # The emoji can be a standard Unicode emoji or a custom one in the format '<:name:id>' diff --git a/examples/sample_extension.py b/examples/sample_extension.py index e67da7b..eca463a 100644 --- a/examples/sample_extension.py +++ b/examples/sample_extension.py @@ -1,7 +1,7 @@ -from disagreement.ext import tasks +from disagreement import loop -@tasks.loop(seconds=2.0) +@loop(seconds=2.0) async def ticker() -> None: print("Extension tick") diff --git a/examples/task_loop.py b/examples/task_loop.py index e8f1c1f..a884538 100644 --- a/examples/task_loop.py +++ b/examples/task_loop.py @@ -8,12 +8,12 @@ import sys if os.path.join(os.getcwd(), "examples") == os.path.dirname(os.path.abspath(__file__)): sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -from disagreement.ext import tasks +from disagreement import loop counter = 0 -@tasks.loop(seconds=1.0) +@loop(seconds=1.0) async def ticker() -> None: global counter counter += 1 diff --git a/examples/typing_indicator.py b/examples/typing_indicator.py index 8442285..18a1ece 100644 --- a/examples/typing_indicator.py +++ b/examples/typing_indicator.py @@ -24,8 +24,15 @@ if os.path.join(os.getcwd(), "examples") == os.path.dirname(os.path.abspath(__fi sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) try: - from disagreement import Client, GatewayIntent, HTTPException, AuthenticationError - from disagreement.ext import commands + from disagreement import ( + Client, + GatewayIntent, + HTTPException, + AuthenticationError, + Cog, + command, + CommandContext, + ) except ImportError: print( "Failed to import disagreement. Make sure it's installed or PYTHONPATH is set correctly." @@ -60,12 +67,12 @@ client = Client(token=BOT_TOKEN, intents=intents, command_prefix="!") # --- Define a Cog for the typing indicator command --- -class TypingCog(commands.Cog): +class TypingCog(Cog): def __init__(self, bot_client): super().__init__(bot_client) - @commands.command(name="typing") - async def typing_test_command(self, ctx: commands.CommandContext): + @command(name="typing") + async def typing_test_command(self, ctx: CommandContext): """Shows a typing indicator for 5 seconds.""" await ctx.reply("Showing typing indicator for 5 seconds...") try: