Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 1 addition & 17 deletions bot/exts/info/codeblock/_instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,23 +148,7 @@ def get_instructions(content: str) -> str | None:
instructions = _get_no_ticks_message(content)
else:
log.trace("Searching results for a code block with invalid ticks.")
bad_ticks = [block for block in blocks if block.tick != _parsing.BACKTICK]
block = None
if bad_ticks:
block = next(
(
bad_tick
for bad_tick in bad_ticks
if any(
block
for block in blocks
if block != bad_tick
and bad_tick.content != block.content
and bad_tick.content not in block.content
)
),
None,
)
block = _parsing._get_block_with_invalid_ticks(blocks)

if block:
log.trace("A code block exists but has invalid ticks.")
Expand Down
19 changes: 14 additions & 5 deletions bot/exts/info/codeblock/_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
from collections.abc import Sequence
from typing import NamedTuple

import regex

from bot import constants
from bot.log import get_logger
from bot.utils import has_lines
Expand Down Expand Up @@ -47,8 +45,6 @@
""",
re.DOTALL | re.VERBOSE
)
# copy of _RE_CODE_BLOCK. Done like this for highlighting reasons (regex.compile doesn't properly highlight)
_REGEX_CODE_BLOCK = regex.compile(_RE_CODE_BLOCK.pattern, regex.DOTALL | regex.VERBOSE)

_RE_LANGUAGE = re.compile(
fr"""
Expand Down Expand Up @@ -91,7 +87,7 @@ def find_faulty_code_blocks(message: str) -> Sequence[CodeBlock] | None:
log.trace("Finding all code blocks in a message.")

code_blocks = []
for match in _REGEX_CODE_BLOCK.finditer(message, overlapped=True):
for match in _RE_CODE_BLOCK.finditer(message):
# Used to ensure non-matched groups have an empty string as the default value.
groups = match.groupdict("")
language = groups["lang"].strip() # Strip the whitespace cause it's included in the group.
Expand Down Expand Up @@ -249,3 +245,16 @@ def _fix_indentation(content: str) -> str:
content = first_line + "".join(line[first_indent:] for line in lines[1:])

return content


def _get_block_with_invalid_ticks(blocks: Sequence[CodeBlock]) -> CodeBlock | None:
"""
Find a block with invalid ticks and return it.

Return `None` if there are no blocks with invalid ticks.
"""
for block in blocks:
if block.tick != BACKTICK:
return block

return None
113 changes: 113 additions & 0 deletions tests/bot/exts/info/codeblock/test_instructions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import unittest
from textwrap import dedent

from bot.exts.info.codeblock import _instructions as instructions


class ProvideBadTicksInstructionsTest(unittest.TestCase):
def __assert_instructions_are_bad_ticks_ones(
self,
instructions_text: str,
wrong_ticks: str,
) -> None:
self.assertIn("\\`\\`\\`", instructions_text)
self.assertIn(wrong_ticks, instructions_text)

def __assert_instructions_contain_no_land_instructions(
self,
instructions_text: str,
) -> None:
self.assertIn("py", instructions_text)

def test_should_provide_bad_ticks_and_no_land_instructions_when_no_lang_spec_and_bad_ticks_are_used(self) -> None:
message = dedent("""
'''
\"\"\"A script that iterates and prints the numbers\"\"\"
numbs = [1, 2, 3]

for numb in numbs:
print(numb)
'''
""").strip()

instructions_text = instructions.get_instructions(message)

self.assertIsNotNone(instructions_text)

# Type narrowing
if instructions_text is None:
return

self.__assert_instructions_are_bad_ticks_ones(instructions_text, "'''")
self.__assert_instructions_contain_no_land_instructions(instructions_text)

def test_should_provide_bad_ticks_instructions_when_correct_lang_spec_and_bad_ticks_are_used(self) -> None:
message = dedent("""
'''py
\"\"\"A script that iterates and prints the numbers\"\"\"
numbs = [1, 2, 3]

for numb in numbs:
print(numb)
'''
""").strip()

instructions_text = instructions.get_instructions(message)

self.assertIsNotNone(instructions_text)

# Type narrowing
if instructions_text is None:
return

self.__assert_instructions_are_bad_ticks_ones(instructions_text, "'''")

def test_should_provide_bad_ticks_instructions_when_wrong_lang_spec_and_bad_ticks_are_used(self) -> None:
message = dedent("""
'''c
\"\"\"A script that iterates and prints the numbers\"\"\"
numbs = [1, 2, 3]

for numb in numbs:
print(numb)
'''
""").strip()

instructions_text = instructions.get_instructions(message)

self.assertIsNotNone(instructions_text)

# Type narrowing
if instructions_text is None:
return

self.__assert_instructions_are_bad_ticks_ones(instructions_text, "'''")

def test_should_provide_bad_ticks_instructions_when_bad_ticks_are_used_in_two_identical_codeblocks(self) -> None:
message = dedent("""
'''py
\"\"\"Docstring\"\"\"
numbs = [1, 2, 3]

for numb in numbs:
print(numb)
'''

'''py
\"\"\"Docstring\"\"\"
numbs = [1, 2, 3]

for numb in numbs:
print(numb)
'''
""").strip()

instructions_text = instructions.get_instructions(message)

self.assertIsNotNone(instructions_text)

# Type narrowing
if instructions_text is None:
return

self.__assert_instructions_are_bad_ticks_ones(instructions_text, "'''")
79 changes: 78 additions & 1 deletion tests/bot/exts/info/codeblock/test_parsing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest
from textwrap import dedent

from bot.exts.info.codeblock import _parsing as parsing

Expand Down Expand Up @@ -30,6 +31,7 @@ def test_should_recognize_contained_codeblock(self):
faulty_code_blocks = parsing.find_faulty_code_blocks(message)
self.assertIsNone(faulty_code_blocks)

@unittest.expectedFailure
def test_should_recognize_contained_codeblock_even_if_that_breaks_formatting(self):
Comment on lines +34 to 35

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the faulty code block here? I think this test shouldn't e an expected failure. Though if this is an edge case that's hard to fix, then I'm fine with letting this be a failure.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's an edge case, but I don't really know. I tried to figure out how to fix it, but it seems adding a lot of code and, therefore, an overwhelm.

message = """```
```py
Expand Down Expand Up @@ -67,7 +69,6 @@ def test_should_not_recognize_quoting_single_quotes(self):
self.assertIsNotNone(faulty_code_blocks)
self.assertEqual(len(faulty_code_blocks), 0)


def test_should_not_recognize_normal_double_quotes(self):
"""normal double quotes refer to double quotes that appear normally in text to quote something"""
message = """ "I am doing a long quote.
Expand All @@ -92,6 +93,36 @@ def test_should_not_recognize_normal_double_quotes_python_text(self):
self.assertIsNotNone(faulty_code_blocks)
self.assertEqual(len(faulty_code_blocks), 0)

def test_should_not_recognize_multiline_single_quote_python_text(self) -> None:
message = dedent("""
```py
\'\'\'A script that iterates and prints the numbers\'\'\'
numbs = [1, 2, 3]

for numb in numbs:
print(numb)
```
""").strip()

faulty_code_blocks = parsing.find_faulty_code_blocks(message)

self.assertIsNone(faulty_code_blocks)

def test_should_not_recognize_multiline_double_quote_python_text(self) -> None:
message = dedent('''
```py
"""A script that iterates and prints the numbers"""
numbs = [1, 2, 3]

for numb in numbs:
print(numb)
```
''').strip()

faulty_code_blocks = parsing.find_faulty_code_blocks(message)

self.assertIsNone(faulty_code_blocks)

def test_should_recognize_single_backtick_no_language(self):
message = """`
x = 4
Expand Down Expand Up @@ -151,3 +182,49 @@ def test_should_recognize_wrong_number_of_backticks(self):
faulty_code_blocks = parsing.find_faulty_code_blocks(message)
self.assertIsNotNone(faulty_code_blocks)
self.assertEqual(len(faulty_code_blocks), 1)


class FindCodeblockWithInvalidTicksTest(unittest.TestCase):
def test_should_return_codeblock_with_single_quotes(self) -> None:
first_block_content = dedent('''
"""A script that iterates and prints the numbers"""
numbs = [1, 2, 3]

for numb in numbs:
print(numb)
''').strip()
first_block_language = "py"
first_block_tick = parsing.BACKTICK
first_block_ticks = first_block_tick * 3

second_block_content = dedent('''
"""A script that iterates and prints the letters"""
letters = ["a", "b", "c"]

for letter in letters:
print(letter)
''').strip()
second_block_language = "py"
second_block_tick = "'"
second_block_ticks = second_block_tick * 3

first_block = parsing.CodeBlock(
first_block_content,
first_block_language,
first_block_ticks,
first_block_tick,
True,
)
second_block = parsing.CodeBlock(
second_block_content,
second_block_language,
second_block_ticks,
second_block_tick,
True,
)
blocks = (first_block, second_block)
expected_block = second_block

block_with_invalid_ticks = parsing._get_block_with_invalid_ticks(blocks)

self.assertEqual(block_with_invalid_ticks, expected_block)