Skip to content

Commit 6c90ab7

Browse files
Zen slicing (#3297)
* Added the possibility of slicing for the zen command * Fixed off-by-one error, as the end index can be equal to the length of the zen * Added support for negative signs and replaced re.search by re.match * Allows for end_index == len(zen_lines). Previously, in that case, end_index % zen_lines would be 0, even though it's the last line * Allows for slicing without a specified end index (e.g. "1:" will return all lines from the second to the last) * Update end index display when slicing in the zen command Co-authored-by: Vivek Ashokkumar <[email protected]> * Added tests for the zen command --------- Co-authored-by: Vivek Ashokkumar <[email protected]> Co-authored-by: ChrisJL <[email protected]>
2 parents 2101946 + 816ca0a commit 6c90ab7

File tree

2 files changed

+116
-13
lines changed

2 files changed

+116
-13
lines changed

Diff for: bot/exts/utils/utils.py

+26-13
Original file line numberDiff line numberDiff line change
@@ -88,40 +88,53 @@ def get_info(char: str) -> tuple[str, str]:
8888
async def zen(
8989
self,
9090
ctx: Context,
91-
zen_rule_index: int | None,
92-
*,
93-
search_value: str | None = None
91+
search_value: str | None,
9492
) -> None:
9593
"""
9694
Show the Zen of Python.
9795
9896
Without any arguments, the full Zen will be produced.
99-
If zen_rule_index is provided, the line with that index will be produced.
100-
If only a string is provided, the line which matches best will be produced.
97+
If an index or a slice is provided, the corresponding lines will be produced.
98+
Otherwise, the line which matches best will be produced.
10199
"""
102100
embed = Embed(
103101
colour=Colour.og_blurple(),
104102
title="The Zen of Python",
105103
description=ZEN_OF_PYTHON
106104
)
107105

108-
if zen_rule_index is None and search_value is None:
106+
if search_value is None:
109107
embed.title += ", by Tim Peters"
110108
await ctx.send(embed=embed)
111109
return
112110

113111
zen_lines = ZEN_OF_PYTHON.splitlines()
114112

115-
# Prioritize passing the zen rule index
116-
if zen_rule_index is not None:
117-
113+
# Prioritize checking for an index or slice
114+
match = re.match(r"(-?\d+)(:(-?\d+)?)?", search_value.split(" ")[0])
115+
if match:
118116
upper_bound = len(zen_lines) - 1
119117
lower_bound = -1 * len(zen_lines)
120-
if not (lower_bound <= zen_rule_index <= upper_bound):
121-
raise BadArgument(f"Please provide an index between {lower_bound} and {upper_bound}.")
122118

123-
embed.title += f" (line {zen_rule_index % len(zen_lines)}):"
124-
embed.description = zen_lines[zen_rule_index]
119+
start_index = int(match.group(1))
120+
121+
if not match.group(2):
122+
if not (lower_bound <= start_index <= upper_bound):
123+
raise BadArgument(f"Please provide an index between {lower_bound} and {upper_bound}.")
124+
embed.title += f" (line {start_index % len(zen_lines)}):"
125+
embed.description = zen_lines[start_index]
126+
await ctx.send(embed=embed)
127+
return
128+
129+
end_index= int(match.group(3)) if match.group(3) else len(zen_lines)
130+
131+
if not ((lower_bound <= start_index <= upper_bound) and (lower_bound <= end_index <= len(zen_lines))):
132+
raise BadArgument(f"Please provide valid indices between {lower_bound} and {upper_bound}.")
133+
if not (start_index % len(zen_lines) < end_index % (len(zen_lines) + 1)):
134+
raise BadArgument("The start index for the slice must be smaller than the end index.")
135+
136+
embed.title += f" (lines {start_index%len(zen_lines)}-{(end_index-1)%len(zen_lines)}):"
137+
embed.description = "\n".join(zen_lines[start_index:end_index])
125138
await ctx.send(embed=embed)
126139
return
127140

Diff for: tests/bot/exts/utils/test_utils.py

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import unittest
2+
3+
from discord import Colour, Embed
4+
from discord.ext.commands import BadArgument
5+
6+
from bot.exts.utils.utils import Utils, ZEN_OF_PYTHON
7+
from tests.helpers import MockBot, MockContext
8+
9+
10+
class ZenTests(unittest.IsolatedAsyncioTestCase):
11+
""" Tests for the `!zen` command. """
12+
13+
14+
def setUp(self):
15+
self.bot = MockBot()
16+
self.cog = Utils(self.bot)
17+
self.ctx = MockContext()
18+
19+
self.zen_list = ZEN_OF_PYTHON.splitlines()
20+
self.template_embed = Embed(colour=Colour.og_blurple(), title="The Zen of Python", description=ZEN_OF_PYTHON)
21+
22+
23+
24+
async def test_zen_without_arguments(self):
25+
""" Tests if the `!zen` command reacts properly to no arguments. """
26+
self.template_embed.title += ", by Tim Peters"
27+
28+
29+
await self.cog.zen.callback(self.cog,self.ctx, search_value = None)
30+
self.ctx.send.assert_called_once_with(embed=self.template_embed)
31+
32+
async def test_zen_with_valid_index(self):
33+
""" Tests if the `!zen` command reacts properly to a valid index as an argument. """
34+
expected_results = {
35+
0: ("The Zen of Python (line 0):", "Beautiful is better than ugly."),
36+
10: ("The Zen of Python (line 10):", "Unless explicitly silenced."),
37+
18: ("The Zen of Python (line 18):", "Namespaces are one honking great idea -- let's do more of those!"),
38+
-1: ("The Zen of Python (line 18):", "Namespaces are one honking great idea -- let's do more of those!"),
39+
-10: ("The Zen of Python (line 9):", "Errors should never pass silently."),
40+
-19: ("The Zen of Python (line 0):", "Beautiful is better than ugly.")
41+
42+
}
43+
44+
for index, (title, description) in expected_results.items():
45+
self.template_embed.title = title
46+
self.template_embed.description = description
47+
ctx = MockContext()
48+
with self.subTest(index = index, expected_title=title, expected_description = description):
49+
await self.cog.zen.callback(self.cog, ctx, search_value = str(index))
50+
ctx.send.assert_called_once_with(embed = self.template_embed)
51+
52+
53+
54+
async def test_zen_with_invalid_index(self):
55+
""" Tests if the `!zen` command reacts properly to an out-of-bounds index as an argument. """
56+
# Negative index
57+
with self.subTest(index = -20), self.assertRaises(BadArgument):
58+
await self.cog.zen.callback(self.cog, self.ctx, search_value="-20")
59+
60+
# Positive index
61+
with self.subTest(index = len(ZEN_OF_PYTHON)), self.assertRaises(BadArgument):
62+
await self.cog.zen.callback(self.cog, self.ctx, search_value=str(len(ZEN_OF_PYTHON)))
63+
64+
async def test_zen_with_valid_slices(self):
65+
""" Tests if the `!zen` command reacts properly to valid slices for indexing as an argument. """
66+
67+
expected_results = {
68+
"0:19": ("The Zen of Python (lines 0-18):", "\n".join(self.zen_list[0:19])),
69+
"0:": ("The Zen of Python (lines 0-18):", "\n".join(self.zen_list[0:])),
70+
"-2:-1": ("The Zen of Python (lines 17-17):", self.zen_list[17]),
71+
"0:-1": ("The Zen of Python (lines 0-17):", "\n".join(self.zen_list[0:-1])),
72+
"10:13": ("The Zen of Python (lines 10-12):", "\n".join(self.zen_list[10:13]))
73+
}
74+
75+
for input_slice, (title, description) in expected_results.items():
76+
self.template_embed.title = title
77+
self.template_embed.description = description
78+
79+
ctx = MockContext()
80+
with self.subTest(input_slice=input_slice, expected_title=title, expected_description=description):
81+
await self.cog.zen.callback(self.cog, ctx, search_value=input_slice)
82+
ctx.send.assert_called_once_with(embed = self.template_embed)
83+
84+
async def test_zen_with_invalid_slices(self):
85+
""" Tests if the `!zen` command reacts properly to invalid slices for indexing as an argument. """
86+
slices= ["19:", "10:9", "-1:-2", "0:20", "-100:", "0:-100"]
87+
88+
for input_slice in slices:
89+
with self.subTest(input_slice = input_slice), self.assertRaises(BadArgument):
90+
await self.cog.zen.callback(self.cog, self.ctx, search_value=input_slice)

0 commit comments

Comments
 (0)