Skip to content

Commit fc001e5

Browse files
Separate parse-time vs usage errors; reclassify import check
Split the misnamed check_if_functional_requirements_are_specified (which mutated a caller-passed buffer, raised a validation error, and returned header presence) into three single-purpose helpers: - has_functional_specs_section: pure presence predicate - count_functionalities: functionality count - validate_functionalities_have_implementation_reqs: the impl-reqs validation Reclassify 'imported module must not contain functionalities' from a PlainSyntaxError to the new usage error ImportedModuleWithFunctionalitiesError. The file is syntactically valid and would be fine as a render target; it is only wrong for the import role, so it is a usage error, not a syntax error. This also fixes the prior mislabeling where such a module could surface the misleading 'no implementation reqs' syntax error first. Behavior preserved: missing functional specs (usage), empty functional specs section (syntax), and functionalities-without-impl-reqs (syntax) are unchanged.
1 parent 5f103a0 commit fc001e5

6 files changed

Lines changed: 73 additions & 19 deletions

File tree

plain2code.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from plain2code_exceptions import (
2626
ConflictingRequirements,
2727
GitNotInstalledError,
28+
ImportedModuleWithFunctionalitiesError,
2829
InvalidAPIKey,
2930
InvalidFridArgument,
3031
MissingAPIKey,
@@ -66,6 +67,7 @@
6667
MissingResource,
6768
TemplateNotFoundError,
6869
PlainSyntaxError,
70+
ImportedModuleWithFunctionalitiesError,
6971
MissingFunctionalitiesError,
7072
MissingPreviousFunctionalitiesError,
7173
MissingAPIKey,

plain2code_exceptions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ class MissingFunctionalitiesError(Exception):
8484
pass
8585

8686

87+
class ImportedModuleWithFunctionalitiesError(Exception):
88+
"""Raised when a module brought in via ``import`` contains functional specs.
89+
90+
This is a usage error, not a syntax error: the module is syntactically valid
91+
and would be fine as a render target, but functionalities are not allowed in
92+
the import role.
93+
"""
94+
95+
pass
96+
97+
8798
class NetworkConnectionError(Exception):
8899
"""Raised when there is a network connectivity issue with the API server."""
89100

plain_file.py

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import file_utils
1919
import plain_spec
2020
from plain2code_exceptions import (
21+
ImportedModuleWithFunctionalitiesError,
2122
MissingFunctionalitiesError,
2223
ModuleDoesNotExistError,
2324
PlainSyntaxError,
@@ -156,17 +157,27 @@ def process_code_variables(plain_source, code_variables):
156157
process_section_code_variables(requirement, code_variables)
157158

158159

159-
def check_if_functional_requirements_are_specified(plain_source, non_functional_requirements):
160-
if plain_source[plain_spec.NON_FUNCTIONAL_REQUIREMENTS] is not None and hasattr(
161-
plain_source[plain_spec.NON_FUNCTIONAL_REQUIREMENTS], "children"
162-
):
163-
non_functional_requirements.extend(plain_source[plain_spec.NON_FUNCTIONAL_REQUIREMENTS].children)
160+
def has_functional_specs_section(plain_source) -> bool:
161+
"""Whether the module declares a ***functional specs*** section (even if it is empty)."""
162+
return plain_spec.FUNCTIONAL_REQUIREMENTS in plain_source
163+
164+
165+
def count_functionalities(plain_source) -> int:
166+
"""Number of functionalities under the ***functional specs*** section (0 if absent or empty)."""
167+
section = plain_source.get(plain_spec.FUNCTIONAL_REQUIREMENTS)
168+
return len(section.children) if section is not None else 0
164169

165-
found_functional_requirements = plain_spec.FUNCTIONAL_REQUIREMENTS in plain_source
166-
if found_functional_requirements and len(non_functional_requirements) == 0:
167-
raise PlainSyntaxError("Plain syntax error: functionality with no implementation reqs specified.")
168170

169-
return found_functional_requirements
171+
def validate_functionalities_have_implementation_reqs(plain_source) -> None:
172+
"""Raise if the module has functionalities but no implementation reqs are specified."""
173+
implementation_reqs = plain_source[plain_spec.NON_FUNCTIONAL_REQUIREMENTS]
174+
has_implementation_reqs = (
175+
implementation_reqs is not None
176+
and hasattr(implementation_reqs, "children")
177+
and len(implementation_reqs.children) > 0
178+
)
179+
if not has_implementation_reqs:
180+
raise PlainSyntaxError("Plain syntax error: functionality with no implementation reqs specified.")
170181

171182

172183
def _is_acceptance_test_heading(token) -> tuple[bool, str | None]:
@@ -390,8 +401,11 @@ def process_imports(
390401
module_name, code_variables, template_dirs, imported_modules, modules_trace
391402
)
392403

393-
if check_if_functional_requirements_are_specified(plain_file_parse_result.plain_source, []):
394-
raise PlainSyntaxError("Plain syntax error: Imported module must not contain functionalities.")
404+
if has_functional_specs_section(plain_file_parse_result.plain_source):
405+
raise ImportedModuleWithFunctionalitiesError(
406+
f"Module {module_name} is imported but contains functional specs. "
407+
f"Imported modules may only provide definitions, implementation reqs, and test reqs."
408+
)
395409

396410
for specification_heading in plain_file_parse_result.plain_source:
397411
if specification_heading not in plain_spec.ALLOWED_IMPORT_SPECIFICATION_HEADINGS:
@@ -731,24 +745,21 @@ def plain_file_parser( # noqa: C901
731745
f"Plain syntax error: Not all required concepts were defined. {missing_required_concepts_msg}."
732746
)
733747

734-
fr_section = plain_file_parse_result.plain_source.get(plain_spec.FUNCTIONAL_REQUIREMENTS)
735-
736-
if fr_section is None:
737-
# Case 1: no ***functional specs*** header at all. Not a syntax error.
748+
if not has_functional_specs_section(plain_file_parse_result.plain_source):
749+
# No ***functional specs*** section at all: valid as an import, but not renderable. Usage error.
738750
raise MissingFunctionalitiesError(
739751
f"Module {module_name} does not have any functionality specified. "
740752
f"At least one functionality is required for rendering."
741753
)
742754

743-
if len(fr_section.children) == 0:
744-
# Case 2: ***functional specs*** header present but empty. Treated as a syntax error.
755+
if count_functionalities(plain_file_parse_result.plain_source) == 0:
756+
# ***functional specs*** section present but empty: invalid in every role. Syntax error.
745757
raise PlainSyntaxError(
746758
f"Plain syntax error: Module '{module_name}' has an empty "
747759
f"'{plain_spec.FUNCTIONAL_REQUIREMENTS}' section. At least one functionality must be specified."
748760
)
749761

750-
# Functionalities exist: keep the existing "functionality with no implementation reqs specified" check.
751-
check_if_functional_requirements_are_specified(plain_file_parse_result.plain_source, [])
762+
validate_functionalities_have_implementation_reqs(plain_file_parse_result.plain_source)
752763

753764
exported_definitions = process_required_modules(
754765
plain_file_parse_result.required_modules,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
***implementation reqs***
2+
3+
- :SomeDef: is used in import_with_functionalities.
4+
5+
***functional specs***
6+
7+
- Display "hello, world"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
import:
3+
- import_with_functionalities
4+
---
5+
6+
***implementation reqs***
7+
8+
- :MainExecutableFile: of :App: should be called "hello_world.py".
9+
10+
***functional specs***
11+
12+
- Display "hello, world"

tests/test_imports.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import pytest
22

33
import plain_file
4+
from plain2code_exceptions import ImportedModuleWithFunctionalitiesError
5+
6+
7+
def test_imported_module_with_functionalities(load_test_data, get_test_data_path):
8+
plain_source_text = load_test_data("data/imports/imports_module_with_functionalities.plain")
9+
template_dirs = [get_test_data_path("data/imports")]
10+
with pytest.raises(
11+
ImportedModuleWithFunctionalitiesError,
12+
match="is imported but contains functional specs",
13+
):
14+
plain_file.parse_plain_source(plain_source_text, {}, template_dirs, [], [])
415

516

617
def test_non_existent_import(load_test_data, get_test_data_path):

0 commit comments

Comments
 (0)