From d00125c87632efd275d46552362e8dc334a51280 Mon Sep 17 00:00:00 2001 From: roli2py Date: Mon, 17 Aug 2026 13:37:44 +0300 Subject: [PATCH 1/3] Fix a get of bad ticks instructions for codeblock --- bot/exts/info/codeblock/_instructions.py | 18 +--- bot/exts/info/codeblock/_parsing.py | 6 +- .../exts/info/codeblock/test_instructions.py | 93 +++++++++++++++++++ tests/bot/exts/info/codeblock/test_parsing.py | 2 +- 4 files changed, 96 insertions(+), 23 deletions(-) create mode 100644 tests/bot/exts/info/codeblock/test_instructions.py diff --git a/bot/exts/info/codeblock/_instructions.py b/bot/exts/info/codeblock/_instructions.py index 57c4b7ad95..7204dbeae4 100644 --- a/bot/exts/info/codeblock/_instructions.py +++ b/bot/exts/info/codeblock/_instructions.py @@ -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 = next((block for block in blocks if block.tick != _parsing.BACKTICK), None) if block: log.trace("A code block exists but has invalid ticks.") diff --git a/bot/exts/info/codeblock/_parsing.py b/bot/exts/info/codeblock/_parsing.py index 22cbe4344e..021f2f4a26 100644 --- a/bot/exts/info/codeblock/_parsing.py +++ b/bot/exts/info/codeblock/_parsing.py @@ -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 @@ -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""" @@ -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. diff --git a/tests/bot/exts/info/codeblock/test_instructions.py b/tests/bot/exts/info/codeblock/test_instructions.py new file mode 100644 index 0000000000..04b265d519 --- /dev/null +++ b/tests/bot/exts/info/codeblock/test_instructions.py @@ -0,0 +1,93 @@ +import unittest +from textwrap import dedent + +from bot.exts.info.codeblock import _instructions as instructions, _parsing as parsing + + +class ProvideBadTicksInstructionsTest(unittest.TestCase): + def __assert_is_instructions_for_message_bad_ticks_one(self, message: str) -> None: + code_blocks = parsing.find_faulty_code_blocks(message) + self.assertIsNotNone(code_blocks) + + # Type narrowing + if code_blocks is None: + return + + code_block = next((block for block in code_blocks if block.tick != parsing.BACKTICK), None) + self.assertIsNotNone(code_block) + + # Type narrowing + if code_block is None: + return + + expected_instructions_text = instructions._get_bad_ticks_message(code_block) + self.assertIsInstance(expected_instructions_text, str) + + # Type narrowing + if not isinstance(expected_instructions_text, str): + return + + instructions_text = instructions.get_instructions(message) + self.assertIsInstance(instructions_text, str) + + # Type narrowing + if not isinstance(instructions_text, str): + return + + self.assertEqual(instructions_text, expected_instructions_text) + + def test_should_provide_when_no_lang_spec_and_bad_ticks_are_used(self) -> None: + message = dedent(""" + ''' + \"\"\"Docstring\"\"\" + numbs = [1, 2, 3] + + for numb in numbs: + print(numb) + ''' + """).strip() + self.__assert_is_instructions_for_message_bad_ticks_one(message) + + def test_should_provide_when_correct_lang_spec_and_bad_ticks_are_used(self) -> None: + message = dedent(""" + '''py + \"\"\"Docstring\"\"\" + numbs = [1, 2, 3] + + for numb in numbs: + print(numb) + ''' + """).strip() + self.__assert_is_instructions_for_message_bad_ticks_one(message) + + def test_should_provide_when_wrong_lang_spec_and_bad_ticks_are_used(self) -> None: + message = dedent(""" + '''c + \"\"\"Docstring\"\"\" + numbs = [1, 2, 3] + + for numb in numbs: + print(numb) + ''' + """).strip() + self.__assert_is_instructions_for_message_bad_ticks_one(message) + + def test_should_provide_bad_ticks_are_used_in_two_identical_codeblocks(self) -> None: + message = dedent(""" + ''' + \"\"\"Docstring\"\"\" + numbs = [1, 2, 3] + + for numb in numbs: + print(numb) + ''' + + ''' + \"\"\"Docstring\"\"\" + numbs = [1, 2, 3] + + for numb in numbs: + print(numb) + ''' + """).strip() + self.__assert_is_instructions_for_message_bad_ticks_one(message) diff --git a/tests/bot/exts/info/codeblock/test_parsing.py b/tests/bot/exts/info/codeblock/test_parsing.py index 4507fcaa54..47892e8e57 100644 --- a/tests/bot/exts/info/codeblock/test_parsing.py +++ b/tests/bot/exts/info/codeblock/test_parsing.py @@ -30,6 +30,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): message = """``` ```py @@ -67,7 +68,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. From 9b4162b861febdb610c3b319af4e24d3a93aeb08 Mon Sep 17 00:00:00 2001 From: roli2py Date: Sat, 29 Aug 2026 18:10:50 +0300 Subject: [PATCH 2/3] Create tests for the faulty code blocks finder Create the unit tests that check whether the finder recognizes Python strings as codeblocks. --- bot/exts/info/codeblock/_instructions.py | 2 +- bot/exts/info/codeblock/_parsing.py | 13 ++++ tests/bot/exts/info/codeblock/test_parsing.py | 77 +++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/codeblock/_instructions.py b/bot/exts/info/codeblock/_instructions.py index 7204dbeae4..2038748978 100644 --- a/bot/exts/info/codeblock/_instructions.py +++ b/bot/exts/info/codeblock/_instructions.py @@ -148,7 +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.") - block = next((block for block in blocks if block.tick != _parsing.BACKTICK), None) + block = _parsing._get_block_with_invalid_ticks(blocks) if block: log.trace("A code block exists but has invalid ticks.") diff --git a/bot/exts/info/codeblock/_parsing.py b/bot/exts/info/codeblock/_parsing.py index 021f2f4a26..3edfb37a14 100644 --- a/bot/exts/info/codeblock/_parsing.py +++ b/bot/exts/info/codeblock/_parsing.py @@ -245,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 diff --git a/tests/bot/exts/info/codeblock/test_parsing.py b/tests/bot/exts/info/codeblock/test_parsing.py index 47892e8e57..286558f816 100644 --- a/tests/bot/exts/info/codeblock/test_parsing.py +++ b/tests/bot/exts/info/codeblock/test_parsing.py @@ -1,4 +1,5 @@ import unittest +from textwrap import dedent from bot.exts.info.codeblock import _parsing as parsing @@ -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 @@ -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) From 71dd92c1d24278215b7f1213c4703258853b8b28 Mon Sep 17 00:00:00 2001 From: roli2py Date: Sat, 29 Aug 2026 19:05:38 +0300 Subject: [PATCH 3/3] De-couple the instructions getter unit tests De-coupling helps to isolate the tests and make the tests more stabler and independent from external objects. --- .../exts/info/codeblock/test_instructions.py | 110 +++++++++++------- 1 file changed, 65 insertions(+), 45 deletions(-) diff --git a/tests/bot/exts/info/codeblock/test_instructions.py b/tests/bot/exts/info/codeblock/test_instructions.py index 04b265d519..6d6ca478c1 100644 --- a/tests/bot/exts/info/codeblock/test_instructions.py +++ b/tests/bot/exts/info/codeblock/test_instructions.py @@ -1,80 +1,91 @@ import unittest from textwrap import dedent -from bot.exts.info.codeblock import _instructions as instructions, _parsing as parsing +from bot.exts.info.codeblock import _instructions as instructions class ProvideBadTicksInstructionsTest(unittest.TestCase): - def __assert_is_instructions_for_message_bad_ticks_one(self, message: str) -> None: - code_blocks = parsing.find_faulty_code_blocks(message) - self.assertIsNotNone(code_blocks) - - # Type narrowing - if code_blocks is None: - return - - code_block = next((block for block in code_blocks if block.tick != parsing.BACKTICK), None) - self.assertIsNotNone(code_block) - - # Type narrowing - if code_block is None: - return - - expected_instructions_text = instructions._get_bad_ticks_message(code_block) - self.assertIsInstance(expected_instructions_text, str) - - # Type narrowing - if not isinstance(expected_instructions_text, str): - return - - instructions_text = instructions.get_instructions(message) - self.assertIsInstance(instructions_text, str) - - # Type narrowing - if not isinstance(instructions_text, str): - return - - self.assertEqual(instructions_text, expected_instructions_text) - - def test_should_provide_when_no_lang_spec_and_bad_ticks_are_used(self) -> None: + 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(""" ''' - \"\"\"Docstring\"\"\" + \"\"\"A script that iterates and prints the numbers\"\"\" numbs = [1, 2, 3] for numb in numbs: print(numb) ''' """).strip() - self.__assert_is_instructions_for_message_bad_ticks_one(message) - def test_should_provide_when_correct_lang_spec_and_bad_ticks_are_used(self) -> None: + 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 - \"\"\"Docstring\"\"\" + \"\"\"A script that iterates and prints the numbers\"\"\" numbs = [1, 2, 3] for numb in numbs: print(numb) ''' """).strip() - self.__assert_is_instructions_for_message_bad_ticks_one(message) - def test_should_provide_when_wrong_lang_spec_and_bad_ticks_are_used(self) -> None: + 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 - \"\"\"Docstring\"\"\" + \"\"\"A script that iterates and prints the numbers\"\"\" numbs = [1, 2, 3] for numb in numbs: print(numb) ''' """).strip() - self.__assert_is_instructions_for_message_bad_ticks_one(message) - def test_should_provide_bad_ticks_are_used_in_two_identical_codeblocks(self) -> None: + 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] @@ -82,7 +93,7 @@ def test_should_provide_bad_ticks_are_used_in_two_identical_codeblocks(self) -> print(numb) ''' - ''' + '''py \"\"\"Docstring\"\"\" numbs = [1, 2, 3] @@ -90,4 +101,13 @@ def test_should_provide_bad_ticks_are_used_in_two_identical_codeblocks(self) -> print(numb) ''' """).strip() - self.__assert_is_instructions_for_message_bad_ticks_one(message) + + 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, "'''")