-
-
Notifications
You must be signed in to change notification settings - Fork 9
Provide a pre-built SourceCode cog #310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a4f395c
643b3b7
7d13d75
2ea5594
28b7eb6
49cc8dd
8410c1f
39a2634
00bc8ad
a3d35ac
5a25124
6242c29
250d7e4
959ba93
032f123
2fa5aa2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,8 @@ | ||
| """Reusable Discord cogs.""" | ||
| __all__ = [] | ||
| from pydis_core.exts import source | ||
|
|
||
| __all__ = [ | ||
| source, | ||
| ] | ||
|
|
||
| __all__ = [module.__name__ for module in __all__] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| """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 | ||
|
|
||
| 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 | ||
|
|
||
|
|
||
| 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.""" | ||
|
|
||
| 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() | ||
| text_command = enum.auto() | ||
| core_command = enum.auto() | ||
| cog = enum.auto() | ||
| core_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.rstrip("/") | ||
|
|
||
| @commands.command(name="source", aliases=("src",)) | ||
| async def source_command( | ||
| self, | ||
| ctx: "commands.Context[Bot]", | ||
| *, | ||
| 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=GITHUB_AVATAR) | ||
| await ctx.send(embed=embed) | ||
| return | ||
|
Comment on lines
+70
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thoughts on adding a link to bot core as well here?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noise for most use cases, pydis_core is never really relevant to those outside PyDis (hence we don't really widely promote it). Supporting commands from pydis_core is about the most we should support I think. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most of time someone looking at the source command is trying to contribute and looking at how commands are implemented. Giving a link to where other commands are implemented is a benefit and intent of the source command, IMO. If a user is using the bot's source command, they're already interested in how it works, and surfacing bot-core is an important part of how the pydis bots work.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see a nice way to include this without fundamentally complicating what is a simple feature that hundreds of bots have. If someone else feels passionately that we should include it, then sure, but the whole point here is to try and transparently carry across functionality from the current command, which obviously does not mention pydis_core. |
||
|
|
||
| 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[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 | ||
|
|
||
| cog = ctx.bot.get_cog(argument) | ||
| if cog: | ||
| if inspect.getmodule(cog).__name__.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") | ||
| 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.text_command or source_type == _SourceType.core_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 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 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: | ||
| first_line_no = None | ||
| lines_extension = "" | ||
|
|
||
| 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: | ||
| # 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 | ||
|
|
||
| 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"): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this could be provided as a setup variable with a default of "main"
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could, but this works with our current bots without change on their side. Just seems easier and is guaranteed to work for our use cases. Setup for the bot core instance is already massive. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair enough, I think. Unified configuration should help this some. |
||
| version = sha | ||
| else: | ||
| version = "main" | ||
jb3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| url = f"{repo}/blob/{version}/{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.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 = "" | ||
| 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 "" | ||
|
|
||
| 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 | ||
Uh oh!
There was an error while loading. Please reload this page.