aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar ChrisJL <[email protected]>2025-05-22 23:07:38 +0100
committerGravatar GitHub <[email protected]>2025-05-22 23:07:38 +0100
commitba117c88eb04049b37733268e90a268efff85125 (patch)
tree3233b504b913f3b4e23ab89c755d4e9493892989
parentMerge pull request #3320 from python-discord/remove-cban-alias (diff)
parentUpdate 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__.py4
-rw-r--r--bot/exts/utils/snekbox/_cog.py47
-rw-r--r--bot/exts/utils/snekbox/_eval.py9
-rw-r--r--tests/bot/exts/utils/snekbox/test_snekbox.py38
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)