From a4f395cd956e6f90ec66b927830a145f6735af67 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 9 Nov 2025 22:12:05 +0000 Subject: [PATCH 01/16] Provide a pre-built SourceCode cog based on bot version --- pydis_core/exts/__init__.py | 4 +- pydis_core/exts/source.py | 166 ++++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 pydis_core/exts/source.py diff --git a/pydis_core/exts/__init__.py b/pydis_core/exts/__init__.py index 9d59e8ad5..b2d2fa2f8 100644 --- a/pydis_core/exts/__init__.py +++ b/pydis_core/exts/__init__.py @@ -1,4 +1,6 @@ """Reusable Discord cogs.""" -__all__ = [] +from .source import SourceCode + +__all__ = [SourceCode] __all__ = [module.__name__ for module in __all__] diff --git a/pydis_core/exts/source.py b/pydis_core/exts/source.py new file mode 100644 index 000000000..d17940c3b --- /dev/null +++ b/pydis_core/exts/source.py @@ -0,0 +1,166 @@ +import enum +import inspect +from pathlib import Path +from typing import NamedTuple, TYPE_CHECKING + +from discord import Embed +from discord.ext import commands +from discord.utils import escape_markdown + +if TYPE_CHECKING: + from pydis_core import BotBase as Bot + + +class TagIdentifierStub(NamedTuple): + """A minmally functioning stub representing a tag identifier.""" + + group: str | None + name: str + + @classmethod + def from_string(cls, string: str) -> "TagIdentifierStub": + """Create a TagIdentifierStub from a string.""" + split_string = string.split(" ", maxsplit=2) + if len(split_string) == 1: + return cls(None, split_string[0]) + return cls(split_string[0], split_string[1]) + + +class SourceType(enum.StrEnum): + """The types of source objects recognized by the source command.""" + + help_command = enum.auto() + command = enum.auto() + cog = enum.auto() + tag = enum.auto() + extension_not_loaded = enum.auto() + + +class SourceCode(commands.Cog, description="Displays information about the bot's source code."): + """ + Pre-built cog to display source code links for commands and cogs (and if applicable, tags). + + To use this cog, instantiate it with the bot instance and the base GitHub repository URL. + + Args: + bot (:obj:`pydis_core.BotBase`): The bot instance. + github_repo: The base URL to the GitHub repository (e.g. `https://github.com/python-discord/bot`). + """ + + def __init__(self, bot: "Bot", github_repo: str) -> None: + self.bot = bot + self.github_repo = github_repo + + @commands.command(name="source", aliases=("src",)) + async def source_command( + self, + ctx: commands.Context, + *, + source_item: str | None = None, + ) -> None: + """Display information and a GitHub link to the source code of a command, tag, or cog.""" + if not source_item: + embed = Embed(title=f"{self.bot.user.name}'s GitHub Repository") + embed.add_field(name="Repository", value=f"[Go to GitHub]({self.github_repo})") + embed.set_thumbnail(url="https://avatars1.githubusercontent.com/u/9919") + await ctx.send(embed=embed) + return + + obj, source_type = await self._get_source_object(ctx, source_item) + embed = await self._build_embed(obj, source_type) + await ctx.send(embed=embed) + + @staticmethod + async def _get_source_object(ctx: commands.Context, argument: str) -> tuple[object, SourceType]: + """Convert argument into the source object and source type.""" + if argument.lower() == "help": + return ctx.bot.help_command, SourceType.help_command + + cog = ctx.bot.get_cog(argument) + if cog: + return cog, SourceType.cog + + cmd = ctx.bot.get_command(argument) + if cmd: + return cmd, SourceType.command + + tags_cog = ctx.bot.get_cog("Tags") + show_tag = True + + if not tags_cog: + show_tag = False + else: + identifier = TagIdentifierStub.from_string(argument.lower()) + if identifier in tags_cog.tags: + return identifier, SourceType.tag + + escaped_arg = escape_markdown(argument) + + raise commands.BadArgument( + f"Unable to convert '{escaped_arg}' to valid command{', tag,' if show_tag else ''} or Cog." + ) + + def _get_source_link(self, source_item: object, source_type: SourceType) -> tuple[str, str, int | None]: + """ + Build GitHub link of source item, return this link, file location and first line number. + + Raise BadArgument if `source_item` is a dynamically-created object (e.g. via internal eval). + """ + if source_type == SourceType.command: + source_item = inspect.unwrap(source_item.callback) + src = source_item.__code__ + filename = src.co_filename + elif source_type == SourceType.tag: + tags_cog = self.bot.get_cog("Tags") + filename = tags_cog.tags[source_item].file_path + else: + src = type(source_item) + try: + filename = inspect.getsourcefile(src) + except TypeError: + raise commands.BadArgument("Cannot get source for a dynamically-created object.") + + if source_type != SourceType.tag: + try: + lines, first_line_no = inspect.getsourcelines(src) + except OSError: + raise commands.BadArgument("Cannot get source for a dynamically-created object.") + + lines_extension = f"#L{first_line_no}-L{first_line_no+len(lines)-1}" + else: + first_line_no = None + lines_extension = "" + + # Handle tag file location differently than others to avoid errors in some cases + if not first_line_no: + file_location = Path(filename) + else: + file_location = Path(filename).relative_to(Path.cwd()).as_posix() + + url = f"{self.github_repo}/blob/main/{file_location}{lines_extension}" + + return url, file_location, first_line_no or None + + async def _build_embed(self, source_object: object, source_type: SourceType) -> Embed | None: + """Build embed based on source object.""" + url, location, first_line = self._get_source_link(source_object, source_type) + + if source_type == SourceType.help_command: + title = "Help Command" + description = source_object.__doc__.splitlines()[1] + elif source_type == SourceType.command: + description = source_object.short_doc + title = f"Command: {source_object.qualified_name}" + elif source_type == SourceType.tag: + title = f"Tag: {source_object}" + description = "" + else: + title = f"Cog: {source_object.qualified_name}" + description = source_object.description.splitlines()[0] + + embed = Embed(title=title, description=description) + embed.add_field(name="Source Code", value=f"[Go to GitHub]({url})") + line_text = f":{first_line}" if first_line else "" + embed.set_footer(text=f"{location}{line_text}") + + return embed From 643b3b783ba59ac5dcbc24a4f6303c0244e4d19f Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 9 Nov 2025 22:25:57 +0000 Subject: [PATCH 02/16] Update source code modules so documentation can build --- pydis_core/exts/__init__.py | 6 ++++-- pydis_core/exts/source.py | 35 ++++++++++++++++++----------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/pydis_core/exts/__init__.py b/pydis_core/exts/__init__.py index b2d2fa2f8..d853b0ff1 100644 --- a/pydis_core/exts/__init__.py +++ b/pydis_core/exts/__init__.py @@ -1,6 +1,8 @@ """Reusable Discord cogs.""" -from .source import SourceCode +from pydis_core.exts import source -__all__ = [SourceCode] +__all__ = [ + source, +] __all__ = [module.__name__ for module in __all__] diff --git a/pydis_core/exts/source.py b/pydis_core/exts/source.py index d17940c3b..b6f6561ff 100644 --- a/pydis_core/exts/source.py +++ b/pydis_core/exts/source.py @@ -1,3 +1,4 @@ +"""Pre-built cog to display source code links for commands and cogs.""" import enum import inspect from pathlib import Path @@ -11,14 +12,14 @@ from pydis_core import BotBase as Bot -class TagIdentifierStub(NamedTuple): +class _TagIdentifierStub(NamedTuple): """A minmally functioning stub representing a tag identifier.""" group: str | None name: str @classmethod - def from_string(cls, string: str) -> "TagIdentifierStub": + def from_string(cls, string: str) -> "_TagIdentifierStub": """Create a TagIdentifierStub from a string.""" split_string = string.split(" ", maxsplit=2) if len(split_string) == 1: @@ -26,7 +27,7 @@ def from_string(cls, string: str) -> "TagIdentifierStub": return cls(split_string[0], split_string[1]) -class SourceType(enum.StrEnum): +class _SourceType(enum.StrEnum): """The types of source objects recognized by the source command.""" help_command = enum.auto() @@ -71,18 +72,18 @@ async def source_command( await ctx.send(embed=embed) @staticmethod - async def _get_source_object(ctx: commands.Context, argument: str) -> tuple[object, SourceType]: + async def _get_source_object(ctx: commands.Context, argument: str) -> tuple[object, _SourceType]: """Convert argument into the source object and source type.""" if argument.lower() == "help": - return ctx.bot.help_command, SourceType.help_command + return ctx.bot.help_command, _SourceType.help_command cog = ctx.bot.get_cog(argument) if cog: - return cog, SourceType.cog + return cog, _SourceType.cog cmd = ctx.bot.get_command(argument) if cmd: - return cmd, SourceType.command + return cmd, _SourceType.command tags_cog = ctx.bot.get_cog("Tags") show_tag = True @@ -90,9 +91,9 @@ async def _get_source_object(ctx: commands.Context, argument: str) -> tuple[obje if not tags_cog: show_tag = False else: - identifier = TagIdentifierStub.from_string(argument.lower()) + identifier = _TagIdentifierStub.from_string(argument.lower()) if identifier in tags_cog.tags: - return identifier, SourceType.tag + return identifier, _SourceType.tag escaped_arg = escape_markdown(argument) @@ -100,17 +101,17 @@ async def _get_source_object(ctx: commands.Context, argument: str) -> tuple[obje f"Unable to convert '{escaped_arg}' to valid command{', tag,' if show_tag else ''} or Cog." ) - def _get_source_link(self, source_item: object, source_type: SourceType) -> tuple[str, str, int | None]: + def _get_source_link(self, source_item: object, source_type: _SourceType) -> tuple[str, str, int | None]: """ Build GitHub link of source item, return this link, file location and first line number. Raise BadArgument if `source_item` is a dynamically-created object (e.g. via internal eval). """ - if source_type == SourceType.command: + if source_type == _SourceType.command: source_item = inspect.unwrap(source_item.callback) src = source_item.__code__ filename = src.co_filename - elif source_type == SourceType.tag: + elif source_type == _SourceType.tag: tags_cog = self.bot.get_cog("Tags") filename = tags_cog.tags[source_item].file_path else: @@ -120,7 +121,7 @@ def _get_source_link(self, source_item: object, source_type: SourceType) -> tupl except TypeError: raise commands.BadArgument("Cannot get source for a dynamically-created object.") - if source_type != SourceType.tag: + if source_type != _SourceType.tag: try: lines, first_line_no = inspect.getsourcelines(src) except OSError: @@ -141,17 +142,17 @@ def _get_source_link(self, source_item: object, source_type: SourceType) -> tupl return url, file_location, first_line_no or None - async def _build_embed(self, source_object: object, source_type: SourceType) -> Embed | None: + async def _build_embed(self, source_object: object, source_type: _SourceType) -> Embed | None: """Build embed based on source object.""" url, location, first_line = self._get_source_link(source_object, source_type) - if source_type == SourceType.help_command: + if source_type == _SourceType.help_command: title = "Help Command" description = source_object.__doc__.splitlines()[1] - elif source_type == SourceType.command: + elif source_type == _SourceType.command: description = source_object.short_doc title = f"Command: {source_object.qualified_name}" - elif source_type == SourceType.tag: + elif source_type == _SourceType.tag: title = f"Tag: {source_object}" description = "" else: From 7d13d75a1445f3698ea3017b75427d985bc1394e Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 9 Nov 2025 22:27:33 +0000 Subject: [PATCH 03/16] Move avatar to constant --- pydis_core/exts/source.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pydis_core/exts/source.py b/pydis_core/exts/source.py index b6f6561ff..6766b5421 100644 --- a/pydis_core/exts/source.py +++ b/pydis_core/exts/source.py @@ -12,6 +12,8 @@ from pydis_core import BotBase as Bot +GITHUB_AVATAR = "https://avatars1.githubusercontent.com/u/9919" + class _TagIdentifierStub(NamedTuple): """A minmally functioning stub representing a tag identifier.""" @@ -63,7 +65,7 @@ async def source_command( if not source_item: embed = Embed(title=f"{self.bot.user.name}'s GitHub Repository") embed.add_field(name="Repository", value=f"[Go to GitHub]({self.github_repo})") - embed.set_thumbnail(url="https://avatars1.githubusercontent.com/u/9919") + embed.set_thumbnail(url=GITHUB_AVATAR) await ctx.send(embed=embed) return From 2ea5594b3c3272df5ffe6bd3e3ba4c8f5b5d4559 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 9 Nov 2025 22:40:50 +0000 Subject: [PATCH 04/16] Use typed context object --- pydis_core/exts/source.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydis_core/exts/source.py b/pydis_core/exts/source.py index 6766b5421..1edc610cd 100644 --- a/pydis_core/exts/source.py +++ b/pydis_core/exts/source.py @@ -57,7 +57,7 @@ def __init__(self, bot: "Bot", github_repo: str) -> None: @commands.command(name="source", aliases=("src",)) async def source_command( self, - ctx: commands.Context, + ctx: "commands.Context[Bot]", *, source_item: str | None = None, ) -> None: @@ -74,7 +74,7 @@ async def source_command( await ctx.send(embed=embed) @staticmethod - async def _get_source_object(ctx: commands.Context, argument: str) -> tuple[object, _SourceType]: + async def _get_source_object(ctx: "commands.Context[Bot]", argument: str) -> tuple[object, _SourceType]: """Convert argument into the source object and source type.""" if argument.lower() == "help": return ctx.bot.help_command, _SourceType.help_command From 28b7eb63dfa8e0a7d58058aad35fe6c404bc4432 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 9 Nov 2025 22:40:59 +0000 Subject: [PATCH 05/16] SourceType command -> text_command --- pydis_core/exts/source.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pydis_core/exts/source.py b/pydis_core/exts/source.py index 1edc610cd..8689ceb36 100644 --- a/pydis_core/exts/source.py +++ b/pydis_core/exts/source.py @@ -33,7 +33,7 @@ class _SourceType(enum.StrEnum): """The types of source objects recognized by the source command.""" help_command = enum.auto() - command = enum.auto() + text_command = enum.auto() cog = enum.auto() tag = enum.auto() extension_not_loaded = enum.auto() @@ -85,7 +85,7 @@ async def _get_source_object(ctx: "commands.Context[Bot]", argument: str) -> tup cmd = ctx.bot.get_command(argument) if cmd: - return cmd, _SourceType.command + return cmd, _SourceType.text_command tags_cog = ctx.bot.get_cog("Tags") show_tag = True @@ -109,7 +109,7 @@ def _get_source_link(self, source_item: object, source_type: _SourceType) -> tup Raise BadArgument if `source_item` is a dynamically-created object (e.g. via internal eval). """ - if source_type == _SourceType.command: + if source_type == _SourceType.text_command: source_item = inspect.unwrap(source_item.callback) src = source_item.__code__ filename = src.co_filename @@ -151,7 +151,7 @@ async def _build_embed(self, source_object: object, source_type: _SourceType) -> if source_type == _SourceType.help_command: title = "Help Command" description = source_object.__doc__.splitlines()[1] - elif source_type == _SourceType.command: + elif source_type == _SourceType.text_command: description = source_object.short_doc title = f"Command: {source_object.qualified_name}" elif source_type == _SourceType.tag: From 49cc8dd6cf4ea84186fe83a404ee41a103f7aae4 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 9 Nov 2025 22:43:05 +0000 Subject: [PATCH 06/16] Add github icon to source footer --- pydis_core/exts/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_core/exts/source.py b/pydis_core/exts/source.py index 8689ceb36..a7525269f 100644 --- a/pydis_core/exts/source.py +++ b/pydis_core/exts/source.py @@ -164,6 +164,6 @@ async def _build_embed(self, source_object: object, source_type: _SourceType) -> embed = Embed(title=title, description=description) embed.add_field(name="Source Code", value=f"[Go to GitHub]({url})") line_text = f":{first_line}" if first_line else "" - embed.set_footer(text=f"{location}{line_text}") + embed.set_footer(text=f"{location}{line_text}", icon_url=GITHUB_AVATAR) return embed From 8410c1f0f75cb93668498852966315b6b1414882 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 9 Nov 2025 23:19:30 +0000 Subject: [PATCH 07/16] Add source cog to development bot --- dev/bot/__main__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev/bot/__main__.py b/dev/bot/__main__.py index 43c4dbd5c..e8522533b 100644 --- a/dev/bot/__main__.py +++ b/dev/bot/__main__.py @@ -7,6 +7,7 @@ from discord.ext import commands import pydis_core +from pydis_core.exts.source import SourceCode from . import Bot @@ -30,6 +31,7 @@ async def main() -> None: """Run the bot.""" bot.http_session = aiohttp.ClientSession() async with bot: + await bot.add_cog(SourceCode(bot, github_repo="https://github.com/python-discord/bot-core")) await bot.start(os.getenv("BOT_TOKEN")) From 39a263470d2da738399e5fee00f7bf8c932765ca Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 9 Nov 2025 23:19:46 +0000 Subject: [PATCH 08/16] Support commands that come from pydis_core --- pydis_core/exts/source.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/pydis_core/exts/source.py b/pydis_core/exts/source.py index a7525269f..7543d2fe6 100644 --- a/pydis_core/exts/source.py +++ b/pydis_core/exts/source.py @@ -1,6 +1,7 @@ """Pre-built cog to display source code links for commands and cogs.""" import enum import inspect +from importlib import metadata from pathlib import Path from typing import NamedTuple, TYPE_CHECKING @@ -13,6 +14,7 @@ GITHUB_AVATAR = "https://avatars1.githubusercontent.com/u/9919" +BOT_CORE_REPO = "https://github.com/python-discord/bot-core" class _TagIdentifierStub(NamedTuple): """A minmally functioning stub representing a tag identifier.""" @@ -34,7 +36,9 @@ class _SourceType(enum.StrEnum): help_command = enum.auto() text_command = enum.auto() + core_command = enum.auto() cog = enum.auto() + core_cog = enum.auto() tag = enum.auto() extension_not_loaded = enum.auto() @@ -81,10 +85,14 @@ async def _get_source_object(ctx: "commands.Context[Bot]", argument: str) -> tup cog = ctx.bot.get_cog(argument) if cog: + if cog.__module__.startswith("pydis_core.exts"): + return cog, _SourceType.core_cog return cog, _SourceType.cog cmd = ctx.bot.get_command(argument) if cmd: + if cmd.module.startswith("pydis_core.exts"): + return cmd, _SourceType.core_command return cmd, _SourceType.text_command tags_cog = ctx.bot.get_cog("Tags") @@ -109,7 +117,7 @@ def _get_source_link(self, source_item: object, source_type: _SourceType) -> tup Raise BadArgument if `source_item` is a dynamically-created object (e.g. via internal eval). """ - if source_type == _SourceType.text_command: + if source_type == _SourceType.text_command or source_type == _SourceType.core_command: source_item = inspect.unwrap(source_item.callback) src = source_item.__code__ filename = src.co_filename @@ -137,10 +145,25 @@ def _get_source_link(self, source_item: object, source_type: _SourceType) -> tup # Handle tag file location differently than others to avoid errors in some cases if not first_line_no: file_location = Path(filename) + elif source_type == _SourceType.core_command: + package_location = metadata.distribution("pydis_core").locate_file("") / "pydis_core" + internal_location = Path(filename).relative_to(package_location).as_posix() + file_location = "pydis_core/" + internal_location + elif source_type == _SourceType.core_cog: + package_location = metadata.distribution("pydis_core").locate_file("") / "pydis_core" / "exts" + internal_location = Path(filename).relative_to(package_location).as_posix() + file_location = "pydis_core/exts/" + internal_location else: file_location = Path(filename).relative_to(Path.cwd()).as_posix() - url = f"{self.github_repo}/blob/main/{file_location}{lines_extension}" + repo = self.github_repo if source_type != _SourceType.core_command else BOT_CORE_REPO + + if source_type == _SourceType.core_command or source_type == _SourceType.core_cog: + version = f"v{metadata.version('pydis_core')}" + else: + version = "main" + + url = f"{repo}/blob/{version}/{file_location}{lines_extension}" return url, file_location, first_line_no or None @@ -154,6 +177,12 @@ async def _build_embed(self, source_object: object, source_type: _SourceType) -> elif source_type == _SourceType.text_command: description = source_object.short_doc title = f"Command: {source_object.qualified_name}" + elif source_type == _SourceType.core_command: + description = source_object.short_doc + title = f"Core Command: {source_object.qualified_name}" + elif source_type == _SourceType.core_cog: + title = f"Core Cog: {source_object.qualified_name}" + description = source_object.description.splitlines()[0] elif source_type == _SourceType.tag: title = f"Tag: {source_object}" description = "" From 00bc8ada15b87c7e5a91a88db07aefa195386d02 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 9 Nov 2025 23:24:46 +0000 Subject: [PATCH 09/16] Add changelog entry for SourceCode functionality --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 56355dfc9..96402c4d8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,7 @@ Changelog .. XXX: CHANGE DATE BEFORE RELEASE - :release:`12.0.0 <9th November 2025>` +- :feature:`310` Provide a pre-built :obj:`pydis_core.exts.source.SourceCode` cog for providing links to command implementations. - :support:`309` Dependency bumps on all dependencies - :support:`309` Migrate build system from Poetry to uv - :support:`309` Explicit support for Python 3.13 and 3.14 From a3d35acf6bb92b9f2507e213c1f9a8f3e17ffdb3 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 9 Nov 2025 23:45:03 +0000 Subject: [PATCH 10/16] Trim trailing / from GitHub Repo --- pydis_core/exts/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_core/exts/source.py b/pydis_core/exts/source.py index 7543d2fe6..95f3e9677 100644 --- a/pydis_core/exts/source.py +++ b/pydis_core/exts/source.py @@ -56,7 +56,7 @@ class SourceCode(commands.Cog, description="Displays information about the bot's def __init__(self, bot: "Bot", github_repo: str) -> None: self.bot = bot - self.github_repo = github_repo + self.github_repo = github_repo.rstrip("/") @commands.command(name="source", aliases=("src",)) async def source_command( From 5a25124ddd24ee6bda79694feed7ce3931b318b2 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 9 Nov 2025 23:47:36 +0000 Subject: [PATCH 11/16] Replace __module__ with inspect.getmodule --- pydis_core/exts/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_core/exts/source.py b/pydis_core/exts/source.py index 95f3e9677..6c84ae5f0 100644 --- a/pydis_core/exts/source.py +++ b/pydis_core/exts/source.py @@ -85,7 +85,7 @@ async def _get_source_object(ctx: "commands.Context[Bot]", argument: str) -> tup cog = ctx.bot.get_cog(argument) if cog: - if cog.__module__.startswith("pydis_core.exts"): + if inspect.getmodule(cog).__name__.startswith("pydis_core.exts"): return cog, _SourceType.core_cog return cog, _SourceType.cog From 6242c29ed22b82f50263fd1d91608bf61b9efef7 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 9 Nov 2025 23:48:40 +0000 Subject: [PATCH 12/16] Move comment about tag handling --- pydis_core/exts/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydis_core/exts/source.py b/pydis_core/exts/source.py index 6c84ae5f0..c26be2484 100644 --- a/pydis_core/exts/source.py +++ b/pydis_core/exts/source.py @@ -142,7 +142,6 @@ def _get_source_link(self, source_item: object, source_type: _SourceType) -> tup first_line_no = None lines_extension = "" - # Handle tag file location differently than others to avoid errors in some cases if not first_line_no: file_location = Path(filename) elif source_type == _SourceType.core_command: @@ -154,6 +153,7 @@ def _get_source_link(self, source_item: object, source_type: _SourceType) -> tup internal_location = Path(filename).relative_to(package_location).as_posix() file_location = "pydis_core/exts/" + internal_location else: + # Handle tag file location differently than others to avoid errors in some cases file_location = Path(filename).relative_to(Path.cwd()).as_posix() repo = self.github_repo if source_type != _SourceType.core_command else BOT_CORE_REPO From 250d7e429025a036e7fb4dcff473b6f97de828f7 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 9 Nov 2025 23:50:40 +0000 Subject: [PATCH 13/16] Support GITHUB_SHA for creating permalinks --- pydis_core/exts/source.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pydis_core/exts/source.py b/pydis_core/exts/source.py index c26be2484..e54fcc983 100644 --- a/pydis_core/exts/source.py +++ b/pydis_core/exts/source.py @@ -1,6 +1,7 @@ """Pre-built cog to display source code links for commands and cogs.""" import enum import inspect +import os from importlib import metadata from pathlib import Path from typing import NamedTuple, TYPE_CHECKING @@ -160,6 +161,8 @@ def _get_source_link(self, source_item: object, source_type: _SourceType) -> tup if source_type == _SourceType.core_command or source_type == _SourceType.core_cog: version = f"v{metadata.version('pydis_core')}" + elif sha := os.getenv("GITHUB_SHA"): + version = sha else: version = "main" From 959ba9367bd7d1a0dda2f64afdc68c23ba9a273c Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 9 Nov 2025 23:59:12 +0000 Subject: [PATCH 14/16] Include project name in footer --- pydis_core/exts/source.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pydis_core/exts/source.py b/pydis_core/exts/source.py index e54fcc983..ded9c7aaf 100644 --- a/pydis_core/exts/source.py +++ b/pydis_core/exts/source.py @@ -196,6 +196,12 @@ async def _build_embed(self, source_object: object, source_type: _SourceType) -> embed = Embed(title=title, description=description) embed.add_field(name="Source Code", value=f"[Go to GitHub]({url})") line_text = f":{first_line}" if first_line else "" - embed.set_footer(text=f"{location}{line_text}", icon_url=GITHUB_AVATAR) + + if source_type == _SourceType.core_cog or source_type == _SourceType.core_command: + project_name = "pydis_core" + else: + project_name = self.bot.user.name + + embed.set_footer(text=f"{project_name} \N{BLACK CIRCLE} {location}{line_text}", icon_url=GITHUB_AVATAR) return embed From 032f1234c69844dce962a8c2559b269668d19fa9 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 10 Nov 2025 19:44:07 +0000 Subject: [PATCH 15/16] Unignore B904 (raise-without-from-inside-except) --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3121d1b36..13ad72e1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,6 @@ select = ["ALL"] ignore = [ "A005", "ANN002", "ANN003", "ANN204", "ANN206", "ANN401", - "B904", "C401", "C408", "C901", "COM812", "CPY001", From 2fa5aa2037616b0cfd61ac67f6d145e6da12723a Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 10 Nov 2025 19:44:23 +0000 Subject: [PATCH 16/16] Fix B904 across project (raise-without-from-except-inside) --- docs/utils.py | 2 +- pydis_core/_bot.py | 4 ++-- pydis_core/exts/source.py | 8 ++++---- pydis_core/site_api.py | 4 ++-- pydis_core/utils/function.py | 8 ++++---- pydis_core/utils/paste_service.py | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/utils.py b/docs/utils.py index 567f0d180..d11f0d9c5 100644 --- a/docs/utils.py +++ b/docs/utils.py @@ -98,7 +98,7 @@ def linkcode_resolve(repo_link: str, domain: str, info: dict[str, str]) -> str | # These are ClassVars added by pydantic. # Since they're not in our source code, we cannot resolve them to a url. return None - raise Exception(f"Could not find symbol `{symbol_name}` in {module.__name__}.") + raise Exception(f"Could not find symbol `{symbol_name}` in {module.__name__}.") from None start, end = pos _, offset = inspect.getsourcelines(symbol[-2]) diff --git a/pydis_core/_bot.py b/pydis_core/_bot.py index 783715dda..2510bd056 100644 --- a/pydis_core/_bot.py +++ b/pydis_core/_bot.py @@ -317,8 +317,8 @@ async def setup_hook(self) -> None: try: await self.ping_services() - except Exception as e: # noqa: BLE001 - raise StartupError(e) + except Exception as e: + raise StartupError(e) from e async def ping_services(self) -> None: """Ping all required services on setup to ensure they are up before starting.""" diff --git a/pydis_core/exts/source.py b/pydis_core/exts/source.py index ded9c7aaf..b53fe859d 100644 --- a/pydis_core/exts/source.py +++ b/pydis_core/exts/source.py @@ -129,14 +129,14 @@ def _get_source_link(self, source_item: object, source_type: _SourceType) -> tup src = type(source_item) try: filename = inspect.getsourcefile(src) - except TypeError: - raise commands.BadArgument("Cannot get source for a dynamically-created object.") + except TypeError as e: + raise commands.BadArgument("Cannot get source for a dynamically-created object.") from e if source_type != _SourceType.tag: try: lines, first_line_no = inspect.getsourcelines(src) - except OSError: - raise commands.BadArgument("Cannot get source for a dynamically-created object.") + except OSError as e: + raise commands.BadArgument("Cannot get source for a dynamically-created object.") from e lines_extension = f"#L{first_line_no}-L{first_line_no+len(lines)-1}" else: diff --git a/pydis_core/site_api.py b/pydis_core/site_api.py index f948cde3a..bcad781af 100644 --- a/pydis_core/site_api.py +++ b/pydis_core/site_api.py @@ -92,9 +92,9 @@ async def maybe_raise_for_status(response: aiohttp.ClientResponse, *, should_rai try: response_json = await response.json() raise ResponseCodeError(response=response, response_json=response_json) - except aiohttp.ContentTypeError: + except aiohttp.ContentTypeError as e: response_text = await response.text() - raise ResponseCodeError(response=response, response_text=response_text) + raise ResponseCodeError(response=response, response_text=response_text) from e async def request(self, method: str, endpoint: str, *, raise_for_status: bool = True, **kwargs) -> dict | None: """ diff --git a/pydis_core/utils/function.py b/pydis_core/utils/function.py index 2d2b79235..f28722074 100644 --- a/pydis_core/utils/function.py +++ b/pydis_core/utils/function.py @@ -51,16 +51,16 @@ def get_arg_value(name_or_pos: Argument, arguments: BoundArgs) -> typing.Any: try: _name, value = arg_values[arg_pos] - except IndexError: - raise ValueError(f"Argument position {arg_pos} is out of bounds.") + except IndexError as e: + raise ValueError(f"Argument position {arg_pos} is out of bounds.") from e else: return value elif isinstance(name_or_pos, str): arg_name = name_or_pos try: return arguments[arg_name] - except KeyError: - raise ValueError(f"Argument {arg_name!r} doesn't exist.") + except KeyError as e: + raise ValueError(f"Argument {arg_name!r} doesn't exist.") from e else: raise TypeError("'arg' must either be an int (positional index) or a str (keyword).") diff --git a/pydis_core/utils/paste_service.py b/pydis_core/utils/paste_service.py index 140e6cdc2..26b7c7d7b 100644 --- a/pydis_core/utils/paste_service.py +++ b/pydis_core/utils/paste_service.py @@ -98,8 +98,8 @@ async def send_to_paste_service( try: async with http_session.get(f"{paste_url}/api/v1/lexer") as response: response_json = await response.json() # Supported lexers are the keys. - except HTTPException: - raise PasteUploadError("Could not fetch supported lexers from selected paste_url.") + except HTTPException as e: + raise PasteUploadError("Could not fetch supported lexers from selected paste_url.") from e _lexers_supported_by_pastebin[paste_url] = list(response_json)