diff --git a/bot/exts/info/pep.py b/bot/exts/info/pep.py index 002522d042..de266146a1 100644 --- a/bot/exts/info/pep.py +++ b/bot/exts/info/pep.py @@ -1,8 +1,9 @@ from datetime import UTC, datetime, timedelta from typing import TypedDict -from discord import Colour, Embed +from discord import Colour, Embed, Interaction, app_commands from discord.ext.commands import Cog, Context, command +from rapidfuzz import process from bot.bot import Bot from bot.log import get_logger @@ -13,6 +14,7 @@ ICON_URL = "https://www.python.org/static/opengraph-icon-200x200.png" PEP_API_URL = "https://peps.python.org/api/peps.json" + class PEPInfo(TypedDict): """ Useful subset of the PEP API response. @@ -35,6 +37,7 @@ class PythonEnhancementProposals(Cog): def __init__(self, bot: Bot): self.bot = bot self.peps: dict[int, PEPInfo] = {} + self.pep_autocomplete_choices: dict[int, str] = {} self.last_refreshed_peps: datetime | None = None async def refresh_pep_data(self) -> None: @@ -45,14 +48,14 @@ async def refresh_pep_data(self) -> None: log.trace("Started refreshing PEP data.") async with self.bot.http_session.get(PEP_API_URL) as resp: if resp.status != 200: - log.warning( - "Fetching PEP data from PEP API failed with code %s", - resp.status) + log.warning("Fetching PEP data from PEP API failed with code %s", resp.status) return listing = await resp.json() - for pep_num, pep_info in listing.items(): - self.peps[int(pep_num)] = pep_info + for pep_num_str, pep_info in listing.items(): + pep_num = int(pep_num_str) + self.peps[pep_num] = pep_info + self.pep_autocomplete_choices[pep_num] = pep_info["title"] log.info("Successfully refreshed PEP data.") @@ -72,18 +75,17 @@ def generate_pep_embed(self, pep: PEPInfo) -> Embed: return embed - @command(name="pep", aliases=("get_pep", "p")) - async def pep_command(self, ctx: Context, pep_number: int) -> None: - """Fetches information about a PEP and sends it to the channel.""" - # Refresh the PEP data up to every hour, as e.g. the PEP status might have changed. - if ( - self.last_refreshed_peps is None or ( - (self.last_refreshed_peps + timedelta(hours=1)) <= datetime.now(tz=UTC) - and len(str(pep_number)) < 5 - ) - ): + async def refresh_pep_data_if_needed(self, *, pep_number: int | None = None) -> None: + """Refreshes the PEP data only when a certain criteria is met.""" + if self.last_refreshed_peps is None or (self.last_refreshed_peps + timedelta(hours=1)) <= datetime.now(tz=UTC): + if pep_number is not None and len(str(pep_number)) >= 5: + return await self.refresh_pep_data() + async def get_pep_embed(self, pep_number: int) -> Embed: + """Refreshes the PEP data if needed and generates the PEP embed.""" + await self.refresh_pep_data_if_needed(pep_number=pep_number) + if pep := self.peps.get(pep_number): embed = self.generate_pep_embed(pep) else: @@ -93,9 +95,33 @@ async def pep_command(self, ctx: Context, pep_number: int) -> None: description=f"PEP {pep_number} does not exist.", colour=Colour.red(), ) + return embed + @command(name="pep", aliases=("get_pep", "p")) + async def pep_command(self, ctx: Context, pep_number: int) -> None: + """Fetches information about a PEP and sends it to the channel.""" + # Refresh the PEP data up to every hour, as e.g. the PEP status might have changed. + embed = await self.get_pep_embed(pep_number) await send_or_reply(ctx, embed) + @app_commands.command(name="pep") + @app_commands.guild_only() + @app_commands.describe(pep_number="The pep number or the autocompleted pep") + async def pep_slash_command(self, interaction: Interaction, pep_number: int) -> None: + """Fetches information about a PEP and sends it to the channel.""" + embed = await self.get_pep_embed(pep_number) + await interaction.response.send_message(embed=embed) + + @pep_slash_command.autocomplete("pep_number") + async def pep_slash_command_autocomplete(self, interaction: Interaction, query: str) -> list[app_commands.Choice]: + """Returns a list of PEPs that matches `query`.""" + await self.refresh_pep_data_if_needed() + + # list[('pep_title', similarity, pep_number)] + result = process.extract(query=query, choices=self.pep_autocomplete_choices, limit=10) + return [app_commands.Choice(name=f"{pep[2]} - {pep[0]}", value=pep[2]) for pep in result] + + async def setup(bot: Bot) -> None: """Load the PEP cog.""" await bot.add_cog(PythonEnhancementProposals(bot))