diff options
| -rw-r--r-- | bot/exts/info/code_snippets.py | 78 | ||||
| -rw-r--r-- | bot/exts/info/doc/_markdown.py | 31 | ||||
| -rw-r--r-- | bot/exts/info/doc/_parsing.py | 2 | ||||
| -rw-r--r-- | bot/exts/moderation/infraction/infractions.py | 4 | ||||
| -rw-r--r-- | bot/exts/utils/snekbox/__init__.py | 4 | ||||
| -rw-r--r-- | bot/exts/utils/snekbox/_cog.py | 51 | ||||
| -rw-r--r-- | bot/exts/utils/snekbox/_eval.py | 9 | ||||
| -rw-r--r-- | bot/exts/utils/utils.py | 65 | ||||
| -rw-r--r-- | bot/resources/tags/kindling-projects.md | 2 | ||||
| -rw-r--r-- | poetry.lock | 8 | ||||
| -rw-r--r-- | pyproject.toml | 2 | ||||
| -rw-r--r-- | tests/bot/exts/utils/snekbox/test_snekbox.py | 38 | ||||
| -rw-r--r-- | tests/bot/exts/utils/test_utils.py | 14 |
13 files changed, 200 insertions, 108 deletions
diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index a44b0c475..eba15e825 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -38,6 +38,13 @@ BITBUCKET_RE = re.compile( r"/(?P<file_path>[^#>]+)(\?[^#>]+)?(#lines-(?P<start_line>\d+)(:(?P<end_line>\d+))?)" ) +PYDIS_PASTEBIN_RE = re.compile( + r"https://paste\.(?:pythondiscord\.com|pydis\.wtf)/(?P<paste_id>[a-zA-Z0-9]+)" + r"#(?P<selections>(?:\d+L\d+-L\d+)(?:,\d+L\d+-L\d+)*)" +) + +PASTEBIN_LINE_SELECTION_RE = re.compile(r"(\d+)L(\d+)-L(\d+)") + class CodeSnippets(Cog): """ @@ -54,7 +61,8 @@ class CodeSnippets(Cog): (GITHUB_RE, self._fetch_github_snippet), (GITHUB_GIST_RE, self._fetch_github_gist_snippet), (GITLAB_RE, self._fetch_gitlab_snippet), - (BITBUCKET_RE, self._fetch_bitbucket_snippet) + (BITBUCKET_RE, self._fetch_bitbucket_snippet), + (PYDIS_PASTEBIN_RE, self._fetch_pastebin_snippets), ] async def _fetch_response(self, url: str, response_format: str, **kwargs) -> Any: @@ -170,7 +178,40 @@ class CodeSnippets(Cog): ) return self._snippet_to_codeblock(file_contents, file_path, start_line, end_line) - def _snippet_to_codeblock(self, file_contents: str, file_path: str, start_line: str, end_line: str) -> str: + async def _fetch_pastebin_snippets(self, paste_id: str, selections: str) -> list[str]: + """Fetches snippets from paste.pythondiscord.com.""" + paste_data = await self._fetch_response( + f"https://paste.pythondiscord.com/api/v1/paste/{paste_id}", + "json" + ) + + snippets = [] + for match in PASTEBIN_LINE_SELECTION_RE.finditer(selections): + file_num, start, end = match.groups() + file_num = int(file_num) - 1 + + file = paste_data["files"][file_num] + file_name = file.get("name") or f"file {file_num + 1}" + snippet = self._snippet_to_codeblock( + file["content"], + file_name, + start, + end, + language=file["lexer"], + ) + + snippets.append(snippet) + + return snippets + + def _snippet_to_codeblock( + self, + file_contents: str, + file_path: str, + start_line: str, + end_line: str|None, + language: str|None = None + ) -> str: """ Given the entire file contents and target lines, creates a code block. @@ -203,15 +244,16 @@ class CodeSnippets(Cog): required = "\n".join(split_file_contents[start_line - 1:end_line]) required = textwrap.dedent(required).rstrip().replace("`", "`\u200b") - # Extracts the code language and checks whether it's a "valid" language - language = file_path.split("/")[-1].split(".")[-1] - trimmed_language = language.replace("-", "").replace("+", "").replace("_", "") - is_valid_language = trimmed_language.isalnum() - if not is_valid_language: - language = "" + if language is None: + # Extracts the code language and checks whether it's a "valid" language + language = file_path.split("/")[-1].split(".")[-1] + trimmed_language = language.replace("-", "").replace("+", "").replace("_", "") + is_valid_language = trimmed_language.isalnum() + if not is_valid_language: + language = "" - if language == "pyi": - language = "py" + if language == "pyi": + language = "py" # Adds a label showing the file path to the snippet if start_line == end_line: @@ -231,8 +273,7 @@ class CodeSnippets(Cog): for pattern, handler in self.pattern_handlers: for match in pattern.finditer(content): try: - snippet = await handler(**match.groupdict()) - all_snippets.append((match.start(), snippet)) + result = await handler(**match.groupdict()) except ClientResponseError as error: error_message = error.message log.log( @@ -241,8 +282,17 @@ class CodeSnippets(Cog): f"{error_message} for GET {error.request_info.real_url.human_repr()}" ) - # Sorts the list of snippets by their match index and joins them into a single message - return "\n".join(x[1] for x in sorted(all_snippets)) + if isinstance(result, list): + # The handler returned multiple snippets (currently only possible with our pastebin) + all_snippets.extend((match.start(), snippet) for snippet in result) + else: + all_snippets.append((match.start(), result)) + + # Sort the list of snippets by ONLY their match index + all_snippets.sort(key=lambda item: item[0]) + + # Join them into a single message + return "\n".join(x[1] for x in all_snippets) @Cog.listener() async def on_message(self, message: discord.Message) -> None: diff --git a/bot/exts/info/doc/_markdown.py b/bot/exts/info/doc/_markdown.py index a030903ed..52e00c2f3 100644 --- a/bot/exts/info/doc/_markdown.py +++ b/bot/exts/info/doc/_markdown.py @@ -1,21 +1,20 @@ -import re from urllib.parse import urljoin import markdownify from bs4.element import PageElement -# See https://github.com/matthewwithanm/python-markdownify/issues/31 -markdownify.whitespace_re = re.compile(r"[\r\n\s\t ]+") - class DocMarkdownConverter(markdownify.MarkdownConverter): """Subclass markdownify's MarkdownCoverter to provide custom conversion methods.""" def __init__(self, *, page_url: str, **options): - super().__init__(**options) + # Reflow text to avoid unwanted line breaks. + default_options = {"wrap": True, "wrap_width": None} + + super().__init__(**default_options | options) self.page_url = page_url - def convert_li(self, el: PageElement, text: str, convert_as_inline: bool) -> str: + def convert_li(self, el: PageElement, text: str, parent_tags: set[str]) -> str: """Fix markdownify's erroneous indexing in ol tags.""" parent = el.parent if parent is not None and parent.name == "ol": @@ -31,38 +30,38 @@ class DocMarkdownConverter(markdownify.MarkdownConverter): bullet = bullets[depth % len(bullets)] return f"{bullet} {text}\n" - def _convert_hn(self, _n: int, el: PageElement, text: str, convert_as_inline: bool) -> str: + def _convert_hn(self, _n: int, el: PageElement, text: str, parent_tags: set[str]) -> str: """Convert h tags to bold text with ** instead of adding #.""" - if convert_as_inline: + if "_inline" in parent_tags: return text return f"**{text}**\n\n" - def convert_code(self, el: PageElement, text: str, convert_as_inline: bool) -> str: + def convert_code(self, el: PageElement, text: str, parent_tags: set[str]) -> str: """Undo `markdownify`s underscore escaping.""" return f"`{text}`".replace("\\", "") - def convert_pre(self, el: PageElement, text: str, convert_as_inline: bool) -> str: + def convert_pre(self, el: PageElement, text: str, parent_tags: set[str]) -> str: """Wrap any codeblocks in `py` for syntax highlighting.""" code = "".join(el.strings) return f"```py\n{code}```" - def convert_a(self, el: PageElement, text: str, convert_as_inline: bool) -> str: + def convert_a(self, el: PageElement, text: str, parent_tags: set[str]) -> str: """Resolve relative URLs to `self.page_url`.""" el["href"] = urljoin(self.page_url, el["href"]) # Discord doesn't handle titles properly, showing links with them as raw text. el["title"] = None - return super().convert_a(el, text, convert_as_inline) + return super().convert_a(el, text, parent_tags) - def convert_p(self, el: PageElement, text: str, convert_as_inline: bool) -> str: + def convert_p(self, el: PageElement, text: str, parent_tags: set[str]) -> str: """Include only one newline instead of two when the parent is a li tag.""" - if convert_as_inline: + if "_inline" in parent_tags: return text parent = el.parent if parent is not None and parent.name == "li": return f"{text}\n" - return super().convert_p(el, text, convert_as_inline) + return super().convert_p(el, text, parent_tags) - def convert_hr(self, el: PageElement, text: str, convert_as_inline: bool) -> str: + def convert_hr(self, el: PageElement, text: str, parent_tags: set[str]) -> str: """Ignore `hr` tag.""" return "" diff --git a/bot/exts/info/doc/_parsing.py b/bot/exts/info/doc/_parsing.py index bc5a5bd31..0f5734a15 100644 --- a/bot/exts/info/doc/_parsing.py +++ b/bot/exts/info/doc/_parsing.py @@ -159,7 +159,7 @@ def _get_truncated_description( if rendered_length + element_length < max_length: if is_tag: - element_markdown = markdown_converter.process_tag(element, convert_as_inline=False) + element_markdown = markdown_converter.process_tag(element) else: element_markdown = markdown_converter.process_text(element) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index a09568b4f..efe70d021 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -102,7 +102,7 @@ class Infractions(InfractionScheduler, commands.Cog): """ await self.apply_ban(ctx, user, reason, duration_or_expiry=duration_or_expiry) - @command(aliases=("cban", "purgeban", "pban")) + @command(aliases=("clban", "purgeban", "pban")) @ensure_future_timestamp(timestamp_arg=3) async def cleanban( self, @@ -154,7 +154,7 @@ class Infractions(InfractionScheduler, commands.Cog): ctx.send = send await infr_manage_cog.infraction_append(ctx, infraction, None, reason=f"[Clean log]({log_url})") - @command() + @command(aliases=("cpban",)) async def compban(self, ctx: Context, user: UnambiguousMemberOrUser) -> None: """Same as cleanban, but specifically with the ban reason and duration used for compromised accounts.""" await self.cleanban(ctx, user, duration=(arrow.utcnow() + COMP_BAN_DURATION).datetime, reason=COMP_BAN_REASON) diff --git a/bot/exts/utils/snekbox/__init__.py b/bot/exts/utils/snekbox/__init__.py index 92bf366be..fa91d0d6f 100644 --- a/bot/exts/utils/snekbox/__init__.py +++ b/bot/exts/utils/snekbox/__init__.py @@ -1,8 +1,8 @@ from bot.bot import Bot -from bot.exts.utils.snekbox._cog import CodeblockConverter, Snekbox +from bot.exts.utils.snekbox._cog import CodeblockConverter, Snekbox, SupportedPythonVersions from bot.exts.utils.snekbox._eval import EvalJob, EvalResult -__all__ = ("CodeblockConverter", "EvalJob", "EvalResult", "Snekbox") +__all__ = ("CodeblockConverter", "EvalJob", "EvalResult", "Snekbox", "SupportedPythonVersions") async def setup(bot: Bot) -> None: diff --git a/bot/exts/utils/snekbox/_cog.py b/bot/exts/utils/snekbox/_cog.py index 39f61c6e2..7ff21d2e6 100644 --- a/bot/exts/utils/snekbox/_cog.py +++ b/bot/exts/utils/snekbox/_cog.py @@ -87,7 +87,7 @@ SNEKBOX_ROLES = (Roles.helpers, Roles.moderators, Roles.admins, Roles.owners, Ro REDO_EMOJI = "\U0001f501" # :repeat: REDO_TIMEOUT = 30 -SupportedPythonVersions = Literal["3.12", "3.13", "3.13t"] +SupportedPythonVersions = Literal["3.13", "3.13t", "3.14"] class FilteredFiles(NamedTuple): allowed: list[FileAttachment] @@ -569,7 +569,29 @@ class Snekbox(Cog): break log.info(f"Re-evaluating code from message {ctx.message.id}:\n{job}") - @command(name="eval", aliases=("e",), usage="[python_version] <code, ...>") + @command( + name="eval", + aliases=("e",), + usage="[python_version] <code, ...>", + help=f""" + Run Python code and get the results. + + This command supports multiple lines of code, including formatted code blocks. + Code can be re-evaluated by editing the original message within 10 seconds and + clicking the reaction that subsequently appears. + + The starting working directory `/home`, is a writeable temporary file system. + Files created, excluding names with leading underscores, will be uploaded in the response. + + If multiple codeblocks are in a message, all of them will be joined and evaluated, + ignoring the text outside them. + + The currently supported versions are {", ".join(get_args(SupportedPythonVersions))}. + + We've done our best to make this sandboxed, but do let us know if you manage to find an + issue with it! + """ + ) @guild_only() @redirect_output( destination_channel=Channels.bot_commands, @@ -585,26 +607,9 @@ class Snekbox(Cog): *, code: CodeblockConverter ) -> None: - """ - Run Python code and get the results. - - This command supports multiple lines of code, including formatted code blocks. - Code can be re-evaluated by editing the original message within 10 seconds and - clicking the reaction that subsequently appears. - - The starting working directory `/home`, is a writeable temporary file system. - Files created, excluding names with leading underscores, will be uploaded in the response. - - If multiple codeblocks are in a message, all of them will be joined and evaluated, - ignoring the text outside them. - - The currently supported verisons are 3.12, 3.13, and 3.13t. - - We've done our best to make this sandboxed, but do let us know if you manage to find an - issue with it! - """ + """Run Python code and get the results.""" code: list[str] - python_version = python_version or "3.12" + python_version = python_version or get_args(SupportedPythonVersions)[0] job = EvalJob.from_code("\n".join(code)).as_version(python_version) await self.run_job(ctx, job) @@ -634,13 +639,13 @@ class Snekbox(Cog): If multiple formatted codeblocks are provided, the first one will be the setup code, which will not be timed. The remaining codeblocks will be joined together and timed. - The currently supported verisons are 3.12, 3.13, and 3.13t. + The currently supported verisons are 3.13, 3.13t, and 3.14. We've done our best to make this sandboxed, but do let us know if you manage to find an issue with it! """ code: list[str] - python_version = python_version or "3.12" + python_version = python_version or "3.13" args = self.prepare_timeit_input(code) job = EvalJob(args, version=python_version, name="timeit") diff --git a/bot/exts/utils/snekbox/_eval.py b/bot/exts/utils/snekbox/_eval.py index ac67d1ed7..6136b6a81 100644 --- a/bot/exts/utils/snekbox/_eval.py +++ b/bot/exts/utils/snekbox/_eval.py @@ -26,7 +26,7 @@ class EvalJob: args: list[str] files: list[FileAttachment] = field(default_factory=list) name: str = "eval" - version: SupportedPythonVersions = "3.12" + version: SupportedPythonVersions = "3.13" @classmethod def from_code(cls, code: str, path: str = "main.py") -> EvalJob: @@ -144,7 +144,12 @@ class EvalResult: def get_status_message(self, job: EvalJob) -> str: """Return a user-friendly message corresponding to the process's return code.""" - version_text = job.version.replace("t", " [free threaded](<https://docs.python.org/3.13/whatsnew/3.13.html#free-threaded-cpython>)") + if job.version == "3.13t": + version_text = job.version.replace("t", " [free threaded](<https://docs.python.org/3.13/whatsnew/3.13.html#free-threaded-cpython>)") + elif job.version == "3.14": + version_text = "3.14 [pre-release](<https://docs.python.org/3.14/whatsnew/3.14.html#development>)" + else: + version_text = job.version msg = f"Your {version_text} {job.name} job" if self.returncode is None: diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index 1bd5c500b..68019b143 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -16,7 +16,7 @@ from bot.utils import messages, time log = get_logger(__name__) -ZEN_OF_PYTHON = """\ +ZEN_OF_PYTHON = """ Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. @@ -36,7 +36,7 @@ Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those! -""" +""".strip() LEADS_AND_COMMUNITY = (Roles.project_leads, Roles.domain_leads, Roles.partners, Roles.python_community) @@ -111,31 +111,54 @@ class Utils(Cog): zen_lines = ZEN_OF_PYTHON.splitlines() # Prioritize checking for an index or slice - match = re.match(r"(-?\d+)(:(-?\d+)?)?", search_value.split(" ")[0]) + match = re.match( + r"(?P<index>-?\d++(?!:))|(?P<start>(?:-\d+)|\d*):(?:(?P<end>(?:-\d+)|\d*)(?::(?P<step>(?:-\d+)|\d*))?)?", + search_value.split(" ")[0], + ) if match: - upper_bound = len(zen_lines) - 1 - lower_bound = -1 * len(zen_lines) - - start_index = int(match.group(1)) - - if not match.group(2): - if not (lower_bound <= start_index <= upper_bound): - raise BadArgument(f"Please provide an index between {lower_bound} and {upper_bound}.") - embed.title += f" (line {start_index % len(zen_lines)}):" - embed.description = zen_lines[start_index] + if match.group("index"): + index = int(match.group("index")) + if not (-19 <= index <= 18): + raise BadArgument("Please provide an index between -19 and 18.") + embed.title += f" (line {index % 19}):" + embed.description = zen_lines[index] await ctx.send(embed=embed) return - end_index= int(match.group(3)) if match.group(3) else len(zen_lines) + start_index = int(match.group("start")) if match.group("start") else None + end_index = int(match.group("end")) if match.group("end") else None + step_size = int(match.group("step")) if match.group("step") else 1 - if not ((lower_bound <= start_index <= upper_bound) and (lower_bound <= end_index <= len(zen_lines))): - raise BadArgument(f"Please provide valid indices between {lower_bound} and {upper_bound}.") - if not (start_index % len(zen_lines) < end_index % (len(zen_lines) + 1)): - raise BadArgument("The start index for the slice must be smaller than the end index.") + if step_size == 0: + raise BadArgument("Step size must not be 0.") - embed.title += f" (lines {start_index%len(zen_lines)}-{(end_index-1)%len(zen_lines)}):" - embed.description = "\n".join(zen_lines[start_index:end_index]) - await ctx.send(embed=embed) + lines = zen_lines[start_index:end_index:step_size] + if not lines: + raise BadArgument("Slice returned 0 lines.") + + if len(lines) == 1: + embed.title += f" (line {zen_lines.index(lines[0])}):" + embed.description = lines[0] + await ctx.send(embed=embed) + elif lines == zen_lines: + embed.title += ", by Tim Peters" + await ctx.send(embed=embed) + elif len(lines) == 19: + embed.title += f" (step size {step_size}):" + embed.description = "\n".join(lines) + await ctx.send(embed=embed) + else: + if step_size != 1: + step_message = f", step size {step_size}" + else: + step_message = "" + first_position = zen_lines.index(lines[0]) + second_position = zen_lines.index(lines[-1]) + if first_position > second_position: + (first_position, second_position) = (second_position, first_position) + embed.title += f" (lines {first_position}-{second_position}{step_message}):" + embed.description = "\n".join(lines) + await ctx.send(embed=embed) return # Try to handle first exact word due difflib.SequenceMatched may use some other similar word instead diff --git a/bot/resources/tags/kindling-projects.md b/bot/resources/tags/kindling-projects.md index 00ab95513..3317c6a2f 100644 --- a/bot/resources/tags/kindling-projects.md +++ b/bot/resources/tags/kindling-projects.md @@ -2,4 +2,4 @@ embed: title: "Kindling Projects" --- -The [Kindling projects page](https://nedbatchelder.com/text/kindling.html) on Ned Batchelder's website contains a list of projects and ideas programmers can tackle to build their skills and knowledge. +The [Kindling projects page](https://nedbatchelder.com/text/kindling.html) contains a list of projects and ideas programmers can tackle to build their skills and knowledge. diff --git a/poetry.lock b/poetry.lock index 88bbc1904..1977ede8e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1191,14 +1191,14 @@ source = ["Cython (>=3.0.11,<3.1.0)"] [[package]] name = "markdownify" -version = "0.14.1" +version = "1.1.0" description = "Convert HTML to markdown." optional = false python-versions = "*" groups = ["main"] files = [ - {file = "markdownify-0.14.1-py3-none-any.whl", hash = "sha256:4c46a6c0c12c6005ddcd49b45a5a890398b002ef51380cd319db62df5e09bc2a"}, - {file = "markdownify-0.14.1.tar.gz", hash = "sha256:a62a7a216947ed0b8dafb95b99b2ef4a0edd1e18d5653c656f68f03db2bfb2f1"}, + {file = "markdownify-1.1.0-py3-none-any.whl", hash = "sha256:32a5a08e9af02c8a6528942224c91b933b4bd2c7d078f9012943776fc313eeef"}, + {file = "markdownify-1.1.0.tar.gz", hash = "sha256:449c0bbbf1401c5112379619524f33b63490a8fa479456d41de9dc9e37560ebd"}, ] [package.dependencies] @@ -2758,4 +2758,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" python-versions = "3.12.*" -content-hash = "ab706b41230a6e46d4aaa098f0fc65de8802889995d047a6f8df61cacdceb5df" +content-hash = "fba27b9411ee45b438bdecca84881f55f0d72f9933ecee31469e516dcfbe8d45" diff --git a/pyproject.toml b/pyproject.toml index c8858057d..6b4b44a0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ deepdiff = "7.0.1" emoji = "2.14.1" feedparser = "6.0.11" lxml = "5.3.1" -markdownify = "0.14.1" +markdownify = "1.1.0" pydantic = "2.10.6" pydantic-settings = "2.8.1" python-dateutil = "2.9.0.post0" diff --git a/tests/bot/exts/utils/snekbox/test_snekbox.py b/tests/bot/exts/utils/snekbox/test_snekbox.py index 9cfd75df8..69262bf61 100644 --- a/tests/bot/exts/utils/snekbox/test_snekbox.py +++ b/tests/bot/exts/utils/snekbox/test_snekbox.py @@ -1,6 +1,7 @@ import asyncio import unittest from base64 import b64encode +from typing import get_args from unittest.mock import AsyncMock, MagicMock, Mock, call, create_autospec, patch from discord import AllowedMentions @@ -10,7 +11,7 @@ from pydis_core.utils.paste_service import MAX_PASTE_SIZE from bot import constants from bot.errors import LockedResourceError from bot.exts.utils import snekbox -from bot.exts.utils.snekbox import EvalJob, EvalResult, Snekbox +from bot.exts.utils.snekbox import EvalJob, EvalResult, Snekbox, SupportedPythonVersions from bot.exts.utils.snekbox._io import FileAttachment from tests.helpers import MockBot, MockContext, MockMember, MockMessage, MockReaction, MockUser @@ -21,6 +22,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): self.bot = MockBot() self.cog = Snekbox(bot=self.bot) self.job = EvalJob.from_code("import random") + self.default_version = get_args(SupportedPythonVersions)[0] @staticmethod def code_args(code: str) -> tuple[EvalJob]: @@ -35,7 +37,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): context_manager = MagicMock() context_manager.__aenter__.return_value = resp self.bot.http_session.post.return_value = context_manager - py_version = "3.12" + py_version = self.default_version job = EvalJob.from_code("import random").as_version(py_version) self.assertEqual(await self.cog.post_job(job), EvalResult("Hi", 137)) @@ -104,9 +106,13 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): def test_eval_result_message(self): """EvalResult.get_message(), should return message.""" cases = ( - ("ERROR", None, ("Your 3.12 eval job has failed", "ERROR", "")), - ("", 128 + snekbox._eval.SIGKILL, ("Your 3.12 eval job timed out or ran out of memory", "", "")), - ("", 255, ("Your 3.12 eval job has failed", "A fatal NsJail error occurred", "")) + ("ERROR", None, (f"Your {self.default_version} eval job has failed", "ERROR", "")), + ( + "", + 128 + snekbox._eval.SIGKILL, + (f"Your {self.default_version} eval job timed out or ran out of memory", "", "") + ), + ("", 255, (f"Your {self.default_version} eval job has failed", "A fatal NsJail error occurred", "")) ) for stdout, returncode, expected in cases: exp_msg, exp_err, exp_files_err = expected @@ -178,8 +184,8 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): mock_signals.return_value.name = "SIGTEST" result = EvalResult(stdout="", returncode=127) self.assertEqual( - result.get_status_message(EvalJob([], version="3.12")), - "Your 3.12 eval job has completed with return code 127 (SIGTEST)" + result.get_status_message(EvalJob([])), + f"Your {self.default_version} eval job has completed with return code 127 (SIGTEST)" ) def test_eval_result_status_emoji(self): @@ -253,7 +259,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): self.cog.send_job = AsyncMock(return_value=response) self.cog.continue_job = AsyncMock(return_value=None) - await self.cog.eval_command(self.cog, ctx=ctx, python_version="3.12", code=["MyAwesomeCode"]) + await self.cog.eval_command(self.cog, ctx=ctx, python_version=self.default_version, code=["MyAwesomeCode"]) job = EvalJob.from_code("MyAwesomeCode") self.cog.send_job.assert_called_once_with(ctx, job) self.cog.continue_job.assert_called_once_with(ctx, response, "eval") @@ -267,7 +273,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): self.cog.continue_job = AsyncMock() self.cog.continue_job.side_effect = (EvalJob.from_code("MyAwesomeFormattedCode"), None) - await self.cog.eval_command(self.cog, ctx=ctx, python_version="3.12", code=["MyAwesomeCode"]) + await self.cog.eval_command(self.cog, ctx=ctx, python_version=self.default_version, code=["MyAwesomeCode"]) expected_job = EvalJob.from_code("MyAwesomeFormattedCode") self.cog.send_job.assert_called_with(ctx, expected_job) @@ -311,7 +317,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): ctx.send.assert_called_once() self.assertEqual( ctx.send.call_args.args[0], - ":warning: Your 3.12 eval job has completed " + f":warning: Your {self.default_version} eval job has completed " "with return code 0.\n\n```ansi\n[No output]\n```" ) allowed_mentions = ctx.send.call_args.kwargs["allowed_mentions"] @@ -335,13 +341,13 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=(False, [])) self.bot.get_cog.return_value = mocked_filter_cog - job = EvalJob.from_code("MyAwesomeCode").as_version("3.12") + job = EvalJob.from_code("MyAwesomeCode").as_version(self.default_version) await self.cog.send_job(ctx, job), ctx.send.assert_called_once() self.assertEqual( ctx.send.call_args.args[0], - ":white_check_mark: Your 3.12 eval job " + f":white_check_mark: Your {self.default_version} eval job " "has completed with return code 0." "\n\n```ansi\nWay too long beard\n```\nFull output: lookatmybeard.com" ) @@ -362,13 +368,13 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=(False, [])) self.bot.get_cog.return_value = mocked_filter_cog - job = EvalJob.from_code("MyAwesomeCode").as_version("3.12") + job = EvalJob.from_code("MyAwesomeCode").as_version(self.default_version) await self.cog.send_job(ctx, job), ctx.send.assert_called_once() self.assertEqual( ctx.send.call_args.args[0], - ":x: Your 3.12 eval job has completed with return code 127." + f":x: Your {self.default_version} eval job has completed with return code 127." "\n\n```ansi\nERROR\n```" ) @@ -395,13 +401,13 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=(False, disallowed_exts)) self.bot.get_cog.return_value = mocked_filter_cog - job = EvalJob.from_code("MyAwesomeCode").as_version("3.12") + job = EvalJob.from_code("MyAwesomeCode").as_version(self.default_version) await self.cog.send_job(ctx, job), ctx.send.assert_called_once() res = ctx.send.call_args.args[0] self.assertTrue( - res.startswith(":white_check_mark: Your 3.12 eval job has completed with return code 0.") + res.startswith(f":white_check_mark: Your {self.default_version} eval job has completed with return code 0.") ) self.assertIn("Files with disallowed extensions can't be uploaded: **.disallowed, .disallowed2, ...**", res) diff --git a/tests/bot/exts/utils/test_utils.py b/tests/bot/exts/utils/test_utils.py index 5392e3512..9b8ea4ade 100644 --- a/tests/bot/exts/utils/test_utils.py +++ b/tests/bot/exts/utils/test_utils.py @@ -65,11 +65,15 @@ class ZenTests(unittest.IsolatedAsyncioTestCase): """ Tests if the `!zen` command reacts properly to valid slices for indexing as an argument. """ expected_results = { - "0:19": ("The Zen of Python (lines 0-18):", "\n".join(self.zen_list[0:19])), - "0:": ("The Zen of Python (lines 0-18):", "\n".join(self.zen_list[0:])), - "-2:-1": ("The Zen of Python (lines 17-17):", self.zen_list[17]), + "0:19": ("The Zen of Python, by Tim Peters", "\n".join(self.zen_list)), + ":": ("The Zen of Python, by Tim Peters", "\n".join(self.zen_list)), + "::": ("The Zen of Python, by Tim Peters", "\n".join(self.zen_list)), + "1:": ("The Zen of Python (lines 1-18):", "\n".join(self.zen_list[1:])), + "-2:-1": ("The Zen of Python (line 17):", self.zen_list[17]), "0:-1": ("The Zen of Python (lines 0-17):", "\n".join(self.zen_list[0:-1])), - "10:13": ("The Zen of Python (lines 10-12):", "\n".join(self.zen_list[10:13])) + "10:13": ("The Zen of Python (lines 10-12):", "\n".join(self.zen_list[10:13])), + "::-1": ("The Zen of Python (step size -1):", "\n".join(self.zen_list[::-1])), + "10:5:-1": ("The Zen of Python (lines 6-10, step size -1):", "\n".join(self.zen_list[10:5:-1])), } for input_slice, (title, description) in expected_results.items(): @@ -83,7 +87,7 @@ class ZenTests(unittest.IsolatedAsyncioTestCase): async def test_zen_with_invalid_slices(self): """ Tests if the `!zen` command reacts properly to invalid slices for indexing as an argument. """ - slices= ["19:", "10:9", "-1:-2", "0:20", "-100:", "0:-100"] + slices= ["19:18", "10:9", "-1:-2", "0:-100", "::0", "1:2:-1", "-5:-4:-1"] for input_slice in slices: with self.subTest(input_slice = input_slice), self.assertRaises(BadArgument): |