aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/exts/moderation/infraction/infractions.py4
-rw-r--r--bot/exts/utils/snekbox/__init__.py4
-rw-r--r--bot/exts/utils/snekbox/_cog.py51
-rw-r--r--bot/exts/utils/snekbox/_eval.py9
-rw-r--r--bot/exts/utils/utils.py65
-rw-r--r--bot/resources/tags/kindling-projects.md2
-rw-r--r--tests/bot/exts/utils/snekbox/test_snekbox.py38
-rw-r--r--tests/bot/exts/utils/test_utils.py14
8 files changed, 115 insertions, 72 deletions
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/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):