From 1464937f6f45096566574906d9b185787e32bdc0 Mon Sep 17 00:00:00 2001 From: Slipstream Date: Sun, 15 Jun 2025 18:11:44 -0600 Subject: [PATCH] Add async start and sync run (#94) --- README.md | 9 ++----- disagreement/client.py | 43 ++++++++++++++++++--------------- docs/introduction.md | 9 ++----- docs/sharding.md | 7 +----- examples/basic_bot.py | 12 ++++----- examples/component_bot.py | 6 ++--- examples/context_menus.py | 8 +++--- examples/example_from_readme.py | 6 ++--- examples/hybrid_bot.py | 8 +++--- examples/modal_command.py | 6 ++--- examples/modal_send.py | 4 +-- examples/moderation_bot.py | 20 +++++++++------ examples/reactions.py | 8 +++--- examples/sharded_bot.py | 6 ++--- examples/typing_indicator.py | 12 ++++----- 15 files changed, 75 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 7a50c0d..cc6de38 100644 --- a/README.md +++ b/README.md @@ -61,13 +61,8 @@ if not token: intents = disagreement.GatewayIntent.default() | disagreement.GatewayIntent.MESSAGE_CONTENT client = disagreement.Client(token=token, command_prefix="!", intents=intents, mention_replies=True) -async def main() -> None: - client.add_cog(Basics(client)) - await client.run() - - -if __name__ == "__main__": - asyncio.run(main()) +client.add_cog(Basics(client)) +client.run() ``` ### Global Error Handling diff --git a/disagreement/client.py b/disagreement/client.py index 7d05db2..fe0d91f 100644 --- a/disagreement/client.py +++ b/disagreement/client.py @@ -158,9 +158,9 @@ class Client: **(http_options or {}), ) self._event_dispatcher: EventDispatcher = EventDispatcher(client_instance=self) - self._gateway: Optional[GatewayClient] = ( - None # Initialized in run() or connect() - ) + self._gateway: Optional[GatewayClient] = ( + None # Initialized in start() or connect() + ) self.shard_count: Optional[int] = shard_count self.gateway_max_retries: int = gateway_max_retries self.gateway_max_backoff: float = gateway_max_backoff @@ -268,8 +268,8 @@ class Client: await self._initialize_gateway() assert self._gateway is not None # Should be initialized by now - retry_delay = 5 # seconds - max_retries = 5 # For initial connection attempts by Client.run, Gateway has its own internal retries for some cases. + retry_delay = 5 # seconds + max_retries = 5 # For initial connection attempts by Client.start, Gateway has its own internal retries for some cases. for attempt in range(max_retries): try: @@ -300,12 +300,11 @@ class Client: if max_retries == 0: # If max_retries was 0, means no retries attempted raise DisagreementException("Connection failed with 0 retries allowed.") - async def run(self) -> None: - """ - A blocking call that connects the client to Discord and runs until the client is closed. - This method is a coroutine. - It handles login, Gateway connection, and keeping the connection alive. - """ + async def start(self) -> None: + """ + Connect the client to Discord and run until the client is closed. + This method is a coroutine containing the main run loop logic. + """ if self._closed: raise DisagreementException("Client is already closed.") @@ -362,15 +361,19 @@ class Client: except Exception as e: print(f"Error checking gateway receive task: {e}") break # Exit on other errors - await asyncio.sleep(1) # Main loop check interval - except DisagreementException as e: - print(f"Client run loop encountered an error: {e}") - # Error already logged by connect or other methods - except asyncio.CancelledError: - print("Client run loop was cancelled.") - finally: - if not self._closed: - await self.close() + await asyncio.sleep(1) # Main loop check interval + except DisagreementException as e: + print(f"Client run loop encountered an error: {e}") + # Error already logged by connect or other methods + except asyncio.CancelledError: + print("Client run loop was cancelled.") + finally: + if not self._closed: + await self.close() + + def run(self) -> None: + """Synchronously start the client using :func:`asyncio.run`.""" + asyncio.run(self.start()) async def close(self) -> None: """ diff --git a/docs/introduction.md b/docs/introduction.md index 4e4061f..fa2bbbe 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -60,13 +60,8 @@ if not token: intents = GatewayIntent.default() | GatewayIntent.MESSAGE_CONTENT client = Client(token=token, command_prefix="!", intents=intents, mention_replies=True) -async def main() -> None: - client.add_cog(Basics(client)) - await client.run() - - -if __name__ == "__main__": - asyncio.run(main()) +client.add_cog(Basics(client)) +client.run() ``` ### Global Error Handling diff --git a/docs/sharding.md b/docs/sharding.md index aafd933..88b2624 100644 --- a/docs/sharding.md +++ b/docs/sharding.md @@ -8,13 +8,8 @@ manually. and configures the `ShardManager` automatically. ```python -import asyncio import disagreement bot = disagreement.AutoShardedClient(token="YOUR_TOKEN") - -async def main(): - await bot.run() - -asyncio.run(main()) +bot.run() ``` diff --git a/examples/basic_bot.py b/examples/basic_bot.py index f09ed3b..9df4248 100644 --- a/examples/basic_bot.py +++ b/examples/basic_bot.py @@ -67,9 +67,7 @@ BOT_TOKEN = os.environ.get("DISCORD_BOT_TOKEN") # --- Intents Configuration --- # Define the intents your bot needs. For basic message reading and responding: intents = ( - GatewayIntent.GUILDS - | GatewayIntent.GUILD_MESSAGES - | GatewayIntent.MESSAGE_CONTENT + GatewayIntent.GUILDS | GatewayIntent.GUILD_MESSAGES | GatewayIntent.MESSAGE_CONTENT ) # MESSAGE_CONTENT is privileged! # If you don't need message content and only react to commands/mentions, @@ -210,14 +208,14 @@ async def on_guild_available(guild: Guild): # --- Main Execution --- -async def main(): +def main(): print("Starting Disagreement Bot...") try: # Add the Cog to the client client.add_cog(ExampleCog(client)) # Pass client instance to Cog constructor # client.add_cog is synchronous, but it schedules cog.cog_load() if it's async. - await client.run() + client.run() except AuthenticationError: print( "Authentication failed. Please check your bot token and ensure it's correct." @@ -232,7 +230,7 @@ async def main(): finally: if not client.is_closed(): print("Ensuring client is closed...") - await client.close() + asyncio.run(client.close()) print("Bot has been shut down.") @@ -244,4 +242,4 @@ if __name__ == "__main__": # if os.name == 'nt': # asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - asyncio.run(main()) + main() diff --git a/examples/component_bot.py b/examples/component_bot.py index 43e1de3..3cba874 100644 --- a/examples/component_bot.py +++ b/examples/component_bot.py @@ -263,7 +263,7 @@ class ComponentCommandsCog(Cog): ) -async def main(): +def main(): @client.event async def on_ready(): if client.user: @@ -283,8 +283,8 @@ async def main(): ) client.add_cog(ComponentCommandsCog(client)) - await client.run() + client.run() if __name__ == "__main__": - asyncio.run(main()) + main() diff --git a/examples/context_menus.py b/examples/context_menus.py index 1fe5aad..2c43b6a 100644 --- a/examples/context_menus.py +++ b/examples/context_menus.py @@ -65,11 +65,9 @@ client.app_command_handler.add_command(user_info) client.app_command_handler.add_command(quote) -async def main() -> None: - await client.run() +def main() -> None: + client.run() if __name__ == "__main__": - import asyncio - - asyncio.run(main()) + main() diff --git a/examples/example_from_readme.py b/examples/example_from_readme.py index b67e8f5..eaab444 100644 --- a/examples/example_from_readme.py +++ b/examples/example_from_readme.py @@ -27,10 +27,10 @@ intents = GatewayIntent.default() | GatewayIntent.MESSAGE_CONTENT client = Client(token=token, command_prefix="!", intents=intents, mention_replies=True) -async def main() -> None: +def main() -> None: client.add_cog(Basics(client)) - await client.run() + client.run() if __name__ == "__main__": - asyncio.run(main()) + main() diff --git a/examples/hybrid_bot.py b/examples/hybrid_bot.py index 077a6fe..e6a5d26 100644 --- a/examples/hybrid_bot.py +++ b/examples/hybrid_bot.py @@ -230,7 +230,7 @@ class TestCog(Cog): # --- Main Bot Script --- -async def main(): +def main(): bot_token = os.getenv("DISCORD_BOT_TOKEN") application_id = os.getenv("DISCORD_APPLICATION_ID") @@ -291,7 +291,7 @@ async def main(): client.add_cog(TestCog(client)) try: - await client.run() + client.run() except KeyboardInterrupt: logger.info("Bot shutting down...") except Exception as e: @@ -300,7 +300,7 @@ async def main(): ) finally: if not client.is_closed(): - await client.close() + asyncio.run(client.close()) logger.info("Bot has been closed.") @@ -310,6 +310,6 @@ if __name__ == "__main__": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) try: - asyncio.run(main()) + main() except KeyboardInterrupt: logger.info("Main loop interrupted. Exiting.") diff --git a/examples/modal_command.py b/examples/modal_command.py index 71b14d0..0b43508 100644 --- a/examples/modal_command.py +++ b/examples/modal_command.py @@ -62,9 +62,9 @@ async def on_ready(): print("------") -async def main(): - await client.run() +def main(): + client.run() if __name__ == "__main__": - asyncio.run(main()) + main() diff --git a/examples/modal_send.py b/examples/modal_send.py index 5b87fd6..1d543ec 100644 --- a/examples/modal_send.py +++ b/examples/modal_send.py @@ -63,6 +63,4 @@ async def on_ready(): if __name__ == "__main__": - import asyncio - - asyncio.run(client.run()) + client.run() diff --git a/examples/moderation_bot.py b/examples/moderation_bot.py index 4b8dc2a..e6bb44c 100644 --- a/examples/moderation_bot.py +++ b/examples/moderation_bot.py @@ -9,7 +9,15 @@ 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, Cog, command, CommandContext +from disagreement import ( + Client, + GatewayIntent, + Member, + Message, + Cog, + command, + CommandContext, +) try: from dotenv import load_dotenv @@ -26,9 +34,7 @@ if not BOT_TOKEN: sys.exit(1) intents = ( - GatewayIntent.GUILDS - | GatewayIntent.GUILD_MESSAGES - | GatewayIntent.MESSAGE_CONTENT + GatewayIntent.GUILDS | GatewayIntent.GUILD_MESSAGES | GatewayIntent.MESSAGE_CONTENT ) client = Client(token=BOT_TOKEN, command_prefix="!", intents=intents) @@ -78,10 +84,10 @@ async def on_message(message: Message) -> None: ) -async def main() -> None: +def main() -> None: client.add_cog(ModerationCog(client)) - await client.run() + client.run() if __name__ == "__main__": - asyncio.run(main()) + main() diff --git a/examples/reactions.py b/examples/reactions.py index 8e87991..ea1f48a 100644 --- a/examples/reactions.py +++ b/examples/reactions.py @@ -137,11 +137,11 @@ async def on_reaction_remove(reaction: Reaction, user: User | Member): # --- Main Execution --- -async def main(): +def main(): print("Starting Reaction Bot...") try: client.add_cog(ReactionCog(client)) - await client.run() + client.run() except AuthenticationError: print("Authentication failed. Check your bot token.") except Exception as e: @@ -149,9 +149,9 @@ async def main(): traceback.print_exc() finally: if not client.is_closed(): - await client.close() + asyncio.run(client.close()) print("Bot has been shut down.") if __name__ == "__main__": - asyncio.run(main()) + main() diff --git a/examples/sharded_bot.py b/examples/sharded_bot.py index 6893671..305981e 100644 --- a/examples/sharded_bot.py +++ b/examples/sharded_bot.py @@ -34,12 +34,12 @@ async def on_ready(): print("Shard bot ready") -async def main(): +def main(): if not TOKEN: print("DISCORD_BOT_TOKEN environment variable not set") return - await client.run() + client.run() if __name__ == "__main__": - asyncio.run(main()) + main() diff --git a/examples/typing_indicator.py b/examples/typing_indicator.py index 18a1ece..d1c6ef6 100644 --- a/examples/typing_indicator.py +++ b/examples/typing_indicator.py @@ -53,9 +53,7 @@ BOT_TOKEN = os.environ.get("DISCORD_BOT_TOKEN") # --- Intents Configuration --- intents = ( - GatewayIntent.GUILDS - | GatewayIntent.GUILD_MESSAGES - | GatewayIntent.MESSAGE_CONTENT + GatewayIntent.GUILDS | GatewayIntent.GUILD_MESSAGES | GatewayIntent.MESSAGE_CONTENT ) # --- Initialize the Client --- @@ -106,11 +104,11 @@ async def on_ready(): # --- Main Execution --- -async def main(): +def main(): print("Starting Typing Indicator Bot...") try: client.add_cog(TypingCog(client)) - await client.run() + client.run() except AuthenticationError: print("Authentication failed. Check your bot token.") except Exception as e: @@ -118,9 +116,9 @@ async def main(): traceback.print_exc() finally: if not client.is_closed(): - await client.close() + asyncio.run(client.close()) print("Bot has been shut down.") if __name__ == "__main__": - asyncio.run(main()) + main()