diff options
author | 2025-05-22 23:07:38 +0100 | |
---|---|---|
committer | 2025-05-22 23:07:38 +0100 | |
commit | ba117c88eb04049b37733268e90a268efff85125 (patch) | |
tree | 3233b504b913f3b4e23ab89c755d4e9493892989 | |
parent | Merge pull request #3320 from python-discord/remove-cban-alias (diff) | |
parent | Update snekbox tests to use new default python version (diff) |
3.14 in snekbox (#3324)
* Replace 3.12 support in snekbox for 3.14-dev
* Add extra info about pre-release versions in snekbox output
* Dynamically get the default snekbox Python version by looking at the first supported version
* Move snekbox help docs to the @command decorator
This allows us to use a f-string to get the supported Python versions, instead of hard coding
* Update snekbox tests to use new default python version
-rw-r--r-- | bot/exts/utils/snekbox/__init__.py | 4 | ||||
-rw-r--r-- | bot/exts/utils/snekbox/_cog.py | 47 | ||||
-rw-r--r-- | bot/exts/utils/snekbox/_eval.py | 9 | ||||
-rw-r--r-- | tests/bot/exts/utils/snekbox/test_snekbox.py | 38 |
4 files changed, 57 insertions, 41 deletions
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..0afd3c408 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) 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/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) |