From a5c7540c89305c46a42df1daa1093283c81f2ce4 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Tue, 7 Jul 2026 11:53:02 -0400 Subject: [PATCH 1/7] Install code block def warning --- src/dayamlchecker/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/dayamlchecker/__init__.py b/src/dayamlchecker/__init__.py index c077a01..e9c4a92 100644 --- a/src/dayamlchecker/__init__.py +++ b/src/dayamlchecker/__init__.py @@ -5,6 +5,10 @@ find_errors_from_string, find_style_findings_from_string, ) +from dayamlchecker._code_def_warning import install as _install_code_def_warning + +_install_code_def_warning() +del _install_code_def_warning __all__ = [ "Finding", From 07bf043d65869503350a1b565add7ac2dd12b302 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Tue, 7 Jul 2026 11:53:33 -0400 Subject: [PATCH 2/7] Add code block def warning installer --- src/dayamlchecker/_code_def_warning.py | 86 ++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/dayamlchecker/_code_def_warning.py diff --git a/src/dayamlchecker/_code_def_warning.py b/src/dayamlchecker/_code_def_warning.py new file mode 100644 index 0000000..58e1caf --- /dev/null +++ b/src/dayamlchecker/_code_def_warning.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import ast +from typing import Any + +from dayamlchecker import messages +from dayamlchecker.messages import ( + FindingClass, + MessageDefinition, + Severity, + draft, +) + +PYTHON_CODE_FUNCTION_DEF = "python_code_function_def" + + +class _FunctionDefFinder(ast.NodeVisitor): + def __init__(self) -> None: + self.first_function: ast.FunctionDef | ast.AsyncFunctionDef | None = None + + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + if self.first_function is None: + self.first_function = node + + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: + if self.first_function is None: + self.first_function = node + + +def _register_message_definition() -> None: + messages.MESSAGE_DEFINITIONS.setdefault( + PYTHON_CODE_FUNCTION_DEF, + MessageDefinition( + code="WG123", + severity=Severity.WARNING, + finding_class=FindingClass.GENERAL, + summary="Code block defines a Python function", + template=( + "code block defines function `{function_name}`; move reusable " + "helper functions to a Python module instead" + ), + ), + ) + + +def _first_function_def(source: str) -> ast.FunctionDef | ast.AsyncFunctionDef | None: + try: + tree = ast.parse(source) + except SyntaxError: + return None + finder = _FunctionDefFinder() + finder.visit(tree) + return finder.first_function + + +def install() -> None: + """Register the warning and attach it to the existing code block validator.""" + _register_message_definition() + + from dayamlchecker import yaml_structure + + if getattr(yaml_structure.PythonText, "_warns_on_function_def", False): + return + + base_python_text = yaml_structure.PythonText + + class PythonTextWithFunctionDefWarning(base_python_text): # type: ignore[misc, valid-type] + _warns_on_function_def = True + + def __init__(self, x: Any) -> None: + super().__init__(x) + if self.errors or not isinstance(x, str): + return + function_node = _first_function_def(x) + if function_node is None: + return + self.errors.append( + draft( + PYTHON_CODE_FUNCTION_DEF, + line_number=getattr(function_node, "lineno", 1) or 1, + function_name=function_node.name, + ) + ) + + yaml_structure.PythonText = PythonTextWithFunctionDefWarning + yaml_structure.big_dict["code"]["type"] = PythonTextWithFunctionDefWarning From b02356957b63d8b46753494bd798dc0034ed5ec9 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Tue, 7 Jul 2026 11:53:45 -0400 Subject: [PATCH 3/7] Test code block def warning --- tests/test_code_def_warning.py | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/test_code_def_warning.py diff --git a/tests/test_code_def_warning.py b/tests/test_code_def_warning.py new file mode 100644 index 0000000..575d10c --- /dev/null +++ b/tests/test_code_def_warning.py @@ -0,0 +1,37 @@ +from dayamlchecker._code_def_warning import PYTHON_CODE_FUNCTION_DEF +from dayamlchecker.messages import FindingClass, Severity +from dayamlchecker.yaml_structure import find_errors_from_string + + +def test_code_block_function_def_warns(): + findings = find_errors_from_string( + "code: |\n" + " answer = 1\n" + " def helper():\n" + " return answer\n", + input_file="", + ) + + warning = next( + finding + for finding in findings + if finding.message_id == PYTHON_CODE_FUNCTION_DEF + ) + assert warning.severity == Severity.WARNING + assert warning.finding_class == FindingClass.GENERAL + assert warning.context["function_name"] == "helper" + assert "Python module" in warning.message + + +def test_code_block_without_function_def_does_not_warn(): + findings = find_errors_from_string( + "code: |\n" + " answer = 1\n" + " if answer:\n" + " result = answer\n", + input_file="", + ) + + assert all( + finding.message_id != PYTHON_CODE_FUNCTION_DEF for finding in findings + ) From d9fa8cf6f321f59e9a6bd98070940ad588bdb907 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Tue, 7 Jul 2026 11:58:30 -0400 Subject: [PATCH 4/7] Move code block def warning into package init --- src/dayamlchecker/__init__.py | 64 +++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/src/dayamlchecker/__init__.py b/src/dayamlchecker/__init__.py index e9c4a92..a2fbf1b 100644 --- a/src/dayamlchecker/__init__.py +++ b/src/dayamlchecker/__init__.py @@ -1,11 +1,71 @@ -from dayamlchecker.messages import Finding, FindingClass +import ast +from typing import Any + +from dayamlchecker import messages as _messages +from dayamlchecker.messages import ( + Finding, + FindingClass, + MessageDefinition, + Severity, + draft, +) from dayamlchecker.yaml_structure import ( RuntimeOptions, find_errors, find_errors_from_string, find_style_findings_from_string, ) -from dayamlchecker._code_def_warning import install as _install_code_def_warning + +_PYTHON_CODE_FUNCTION_DEF = "python_code_function_def" + + +def _install_code_def_warning() -> None: + _messages.MESSAGE_DEFINITIONS.setdefault( + _PYTHON_CODE_FUNCTION_DEF, + MessageDefinition( + code="WG123", + severity=Severity.WARNING, + finding_class=FindingClass.GENERAL, + summary="Code block defines a Python function", + template=( + "code block defines function `{function_name}`; move reusable " + "helper functions to a Python module instead" + ), + ), + ) + + from dayamlchecker import yaml_structure as _yaml_structure + + if getattr(_yaml_structure.PythonText, "_warns_on_function_def", False): + return + + base_python_text = _yaml_structure.PythonText + + class PythonTextWithFunctionDefWarning(base_python_text): # type: ignore[misc, valid-type] + _warns_on_function_def = True + + def __init__(self, x: Any) -> None: + super().__init__(x) + if self.errors or not isinstance(x, str): + return + try: + tree = ast.parse(x) + except SyntaxError: + return + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + self.errors.append( + draft( + _PYTHON_CODE_FUNCTION_DEF, + line_number=getattr(node, "lineno", 1) or 1, + function_name=node.name, + ) + ) + break + + _yaml_structure.PythonText = PythonTextWithFunctionDefWarning + _yaml_structure.big_dict["code"]["type"] = PythonTextWithFunctionDefWarning + _install_code_def_warning() del _install_code_def_warning From 0e3d1c3c3d142e904ed11a3de2582e704e81008d Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Tue, 7 Jul 2026 11:58:53 -0400 Subject: [PATCH 5/7] Avoid private module import in tests --- tests/test_code_def_warning.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_code_def_warning.py b/tests/test_code_def_warning.py index 575d10c..53e6a69 100644 --- a/tests/test_code_def_warning.py +++ b/tests/test_code_def_warning.py @@ -1,7 +1,8 @@ -from dayamlchecker._code_def_warning import PYTHON_CODE_FUNCTION_DEF from dayamlchecker.messages import FindingClass, Severity from dayamlchecker.yaml_structure import find_errors_from_string +PYTHON_CODE_FUNCTION_DEF = "python_code_function_def" + def test_code_block_function_def_warns(): findings = find_errors_from_string( @@ -32,6 +33,4 @@ def test_code_block_without_function_def_does_not_warn(): input_file="", ) - assert all( - finding.message_id != PYTHON_CODE_FUNCTION_DEF for finding in findings - ) + assert all(finding.message_id != PYTHON_CODE_FUNCTION_DEF for finding in findings) From 02c7e2072768e2d1eaaea5feb80524f8f01ed66c Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Tue, 7 Jul 2026 11:59:01 -0400 Subject: [PATCH 6/7] Remove private code def warning module --- src/dayamlchecker/_code_def_warning.py | 86 -------------------------- 1 file changed, 86 deletions(-) delete mode 100644 src/dayamlchecker/_code_def_warning.py diff --git a/src/dayamlchecker/_code_def_warning.py b/src/dayamlchecker/_code_def_warning.py deleted file mode 100644 index 58e1caf..0000000 --- a/src/dayamlchecker/_code_def_warning.py +++ /dev/null @@ -1,86 +0,0 @@ -from __future__ import annotations - -import ast -from typing import Any - -from dayamlchecker import messages -from dayamlchecker.messages import ( - FindingClass, - MessageDefinition, - Severity, - draft, -) - -PYTHON_CODE_FUNCTION_DEF = "python_code_function_def" - - -class _FunctionDefFinder(ast.NodeVisitor): - def __init__(self) -> None: - self.first_function: ast.FunctionDef | ast.AsyncFunctionDef | None = None - - def visit_FunctionDef(self, node: ast.FunctionDef) -> None: - if self.first_function is None: - self.first_function = node - - def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: - if self.first_function is None: - self.first_function = node - - -def _register_message_definition() -> None: - messages.MESSAGE_DEFINITIONS.setdefault( - PYTHON_CODE_FUNCTION_DEF, - MessageDefinition( - code="WG123", - severity=Severity.WARNING, - finding_class=FindingClass.GENERAL, - summary="Code block defines a Python function", - template=( - "code block defines function `{function_name}`; move reusable " - "helper functions to a Python module instead" - ), - ), - ) - - -def _first_function_def(source: str) -> ast.FunctionDef | ast.AsyncFunctionDef | None: - try: - tree = ast.parse(source) - except SyntaxError: - return None - finder = _FunctionDefFinder() - finder.visit(tree) - return finder.first_function - - -def install() -> None: - """Register the warning and attach it to the existing code block validator.""" - _register_message_definition() - - from dayamlchecker import yaml_structure - - if getattr(yaml_structure.PythonText, "_warns_on_function_def", False): - return - - base_python_text = yaml_structure.PythonText - - class PythonTextWithFunctionDefWarning(base_python_text): # type: ignore[misc, valid-type] - _warns_on_function_def = True - - def __init__(self, x: Any) -> None: - super().__init__(x) - if self.errors or not isinstance(x, str): - return - function_node = _first_function_def(x) - if function_node is None: - return - self.errors.append( - draft( - PYTHON_CODE_FUNCTION_DEF, - line_number=getattr(function_node, "lineno", 1) or 1, - function_name=function_node.name, - ) - ) - - yaml_structure.PythonText = PythonTextWithFunctionDefWarning - yaml_structure.big_dict["code"]["type"] = PythonTextWithFunctionDefWarning From 7a1a7104a815c35d9462bf37ecb62c22075fdc54 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Tue, 7 Jul 2026 12:19:47 -0400 Subject: [PATCH 7/7] Cleanup PR --- src/dayamlchecker/__init__.py | 61 ----------------------------- src/dayamlchecker/messages.py | 11 ++++++ src/dayamlchecker/yaml_structure.py | 14 ++++++- tests/test_code_def_warning.py | 36 ----------------- tests/test_yaml_structure.py | 49 +++++++++++++++++++++-- 5 files changed, 69 insertions(+), 102 deletions(-) delete mode 100644 tests/test_code_def_warning.py diff --git a/src/dayamlchecker/__init__.py b/src/dayamlchecker/__init__.py index a2fbf1b..fdc61a6 100644 --- a/src/dayamlchecker/__init__.py +++ b/src/dayamlchecker/__init__.py @@ -1,13 +1,6 @@ -import ast -from typing import Any - -from dayamlchecker import messages as _messages from dayamlchecker.messages import ( Finding, FindingClass, - MessageDefinition, - Severity, - draft, ) from dayamlchecker.yaml_structure import ( RuntimeOptions, @@ -16,60 +9,6 @@ find_style_findings_from_string, ) -_PYTHON_CODE_FUNCTION_DEF = "python_code_function_def" - - -def _install_code_def_warning() -> None: - _messages.MESSAGE_DEFINITIONS.setdefault( - _PYTHON_CODE_FUNCTION_DEF, - MessageDefinition( - code="WG123", - severity=Severity.WARNING, - finding_class=FindingClass.GENERAL, - summary="Code block defines a Python function", - template=( - "code block defines function `{function_name}`; move reusable " - "helper functions to a Python module instead" - ), - ), - ) - - from dayamlchecker import yaml_structure as _yaml_structure - - if getattr(_yaml_structure.PythonText, "_warns_on_function_def", False): - return - - base_python_text = _yaml_structure.PythonText - - class PythonTextWithFunctionDefWarning(base_python_text): # type: ignore[misc, valid-type] - _warns_on_function_def = True - - def __init__(self, x: Any) -> None: - super().__init__(x) - if self.errors or not isinstance(x, str): - return - try: - tree = ast.parse(x) - except SyntaxError: - return - for node in ast.walk(tree): - if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): - self.errors.append( - draft( - _PYTHON_CODE_FUNCTION_DEF, - line_number=getattr(node, "lineno", 1) or 1, - function_name=node.name, - ) - ) - break - - _yaml_structure.PythonText = PythonTextWithFunctionDefWarning - _yaml_structure.big_dict["code"]["type"] = PythonTextWithFunctionDefWarning - - -_install_code_def_warning() -del _install_code_def_warning - __all__ = [ "Finding", "FindingClass", diff --git a/src/dayamlchecker/messages.py b/src/dayamlchecker/messages.py index a5a598a..7489cfe 100644 --- a/src/dayamlchecker/messages.py +++ b/src/dayamlchecker/messages.py @@ -32,6 +32,7 @@ class MessageId(StrEnum): PYTHON_CODE_TYPE = "python_code_type" PYTHON_SYNTAX_ERROR = "python_syntax_error" + PYTHON_CODE_FUNCTION_DEF = "python_code_function_def" VALIDATION_CODE_MISSING_VALIDATION_ERROR = ( "validation_code_missing_validation_error" ) @@ -273,6 +274,16 @@ class MessageDefinition: summary="Python syntax error", template="Python syntax error: {error}", ), + MessageId.PYTHON_CODE_FUNCTION_DEF: MessageDefinition( + code="WG123", + severity=Severity.WARNING, + finding_class=FindingClass.GENERAL, + summary="Code block defines a Python function", + template=( + "code block defines function `{function_name}`; move reusable " + "helper functions to a Python module instead, or use inline code for small snippets that are only used once" + ), + ), MessageId.VALIDATION_CODE_MISSING_VALIDATION_ERROR: MessageDefinition( code="WG101", severity=Severity.WARNING, diff --git a/src/dayamlchecker/yaml_structure.py b/src/dayamlchecker/yaml_structure.py index 8399077..382526f 100644 --- a/src/dayamlchecker/yaml_structure.py +++ b/src/dayamlchecker/yaml_structure.py @@ -185,7 +185,7 @@ def __init__(self, x): ] return try: - ast.parse(x) + tree = ast.parse(x) except SyntaxError as ex: # ex.lineno gives line number within the code block lineno = ex.lineno or 1 @@ -193,6 +193,18 @@ def __init__(self, x): self.errors = [ draft(MessageId.PYTHON_SYNTAX_ERROR, line_number=lineno, error=msg) ] + return + + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + self.errors.append( + draft( + MessageId.PYTHON_CODE_FUNCTION_DEF, + line_number=getattr(node, "lineno", 1) or 1, + function_name=node.name, + ) + ) + break class ValidationCode(PythonText): diff --git a/tests/test_code_def_warning.py b/tests/test_code_def_warning.py deleted file mode 100644 index 53e6a69..0000000 --- a/tests/test_code_def_warning.py +++ /dev/null @@ -1,36 +0,0 @@ -from dayamlchecker.messages import FindingClass, Severity -from dayamlchecker.yaml_structure import find_errors_from_string - -PYTHON_CODE_FUNCTION_DEF = "python_code_function_def" - - -def test_code_block_function_def_warns(): - findings = find_errors_from_string( - "code: |\n" - " answer = 1\n" - " def helper():\n" - " return answer\n", - input_file="", - ) - - warning = next( - finding - for finding in findings - if finding.message_id == PYTHON_CODE_FUNCTION_DEF - ) - assert warning.severity == Severity.WARNING - assert warning.finding_class == FindingClass.GENERAL - assert warning.context["function_name"] == "helper" - assert "Python module" in warning.message - - -def test_code_block_without_function_def_does_not_warn(): - findings = find_errors_from_string( - "code: |\n" - " answer = 1\n" - " if answer:\n" - " result = answer\n", - input_file="", - ) - - assert all(finding.message_id != PYTHON_CODE_FUNCTION_DEF for finding in findings) diff --git a/tests/test_yaml_structure.py b/tests/test_yaml_structure.py index cdc5e8c..67e0d6c 100644 --- a/tests/test_yaml_structure.py +++ b/tests/test_yaml_structure.py @@ -19,6 +19,35 @@ def test_valid_question_no_errors(self): errs = find_errors_from_string(valid, input_file="") self.assertEqual(len(errs), 0, f"Expected no errors, got: {errs}") + def test_code_block_function_def_warns(self): + yaml_content = """code: | + answer = 1 + def helper(): + return answer +""" + errs = find_errors_from_string(yaml_content, input_file="") + + warning = next( + err for err in errs if err.message_id == "python_code_function_def" + ) + self.assertEqual(warning.code, "WG123") + self.assertEqual(warning.line_number, 4) + self.assertEqual(warning.context["function_name"], "helper") + self.assertIn("Python module", warning.message) + + def test_code_block_without_function_def_does_not_warn(self): + yaml_content = """code: | + answer = 1 + if answer: + result = answer +""" + errs = find_errors_from_string(yaml_content, input_file="") + + self.assertFalse( + any(err.message_id == "python_code_function_def" for err in errs), + f"Did not expect function-def warning, got: {errs}", + ) + def test_question_and_template_exclusive_error(self): invalid = """ question: | @@ -513,8 +542,14 @@ def test_accessibility_this_website_link_text_fails(self): input_file="", lint_mode="accessibility", ) - generic_errs = [e for e in errs if "link text" in e.err_str.lower() and "too generic" in e.err_str.lower()] - self.assertEqual(len(generic_errs), 3, f"Expected 3 generic link-text errors, got: {errs}") + generic_errs = [ + e + for e in errs + if "link text" in e.err_str.lower() and "too generic" in e.err_str.lower() + ] + self.assertEqual( + len(generic_errs), 3, f"Expected 3 generic link-text errors, got: {errs}" + ) self.assertTrue(all(e.severity.value == "error" for e in generic_errs)) def test_accessibility_sitio_web_link_text_fails(self): @@ -528,8 +563,14 @@ def test_accessibility_sitio_web_link_text_fails(self): input_file="", lint_mode="accessibility", ) - generic_errs = [e for e in errs if "link text" in e.err_str.lower() and "too generic" in e.err_str.lower()] - self.assertEqual(len(generic_errs), 3, f"Expected 3 generic link-text errors, got: {errs}") + generic_errs = [ + e + for e in errs + if "link text" in e.err_str.lower() and "too generic" in e.err_str.lower() + ] + self.assertEqual( + len(generic_errs), 3, f"Expected 3 generic link-text errors, got: {errs}" + ) self.assertTrue(all(e.severity.value == "error" for e in generic_errs)) def test_accessibility_empty_markdown_link_fails(self):