Skip to content
Merged
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
19 changes: 18 additions & 1 deletion plain_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,19 @@ def process_imports(
return required_concepts


def normalize_line_endings(plain_source_text: str) -> str:
return plain_source_text.replace("\r\n", "\n").replace("\r", "\n")


def restore_stripped_lines(plain_source_text: str, content: str) -> str:
stripped_source = plain_source_text.rstrip()
if not content or not stripped_source.endswith(content):
return content

stripped_line_count = stripped_source[: len(stripped_source) - len(content)].count("\n")
return "\n" * stripped_line_count + content


def read_plain_source_metadata(plain_source_text):
try:
plain_source_obj = frontmatter.loads(plain_source_text)
Expand Down Expand Up @@ -490,6 +503,8 @@ def parse_plain_source( # noqa: C901
imported_modules: list[str],
modules_trace: list[str],
) -> PlainFileParseResult:
plain_source_text = normalize_line_endings(plain_source_text)

plain_source_obj = read_plain_source_metadata(plain_source_text)

plain_source = PLAIN_SOURCE_TEMPLATE.copy()
Expand All @@ -508,7 +523,9 @@ def parse_plain_source( # noqa: C901

[_, loaded_templates] = file_utils.get_loaded_templates(template_dirs, plain_source_text)

plain_source_full_text = render_plain_source(plain_source_obj.content, loaded_templates, code_variables)
plain_source_content = restore_stripped_lines(plain_source_text, plain_source_obj.content)

plain_source_full_text = render_plain_source(plain_source_content, loaded_templates, code_variables)

plain_file = mistletoe.Document(io.StringIO(plain_source_full_text))

Expand Down
45 changes: 41 additions & 4 deletions tests/test_plainfileparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def test_unknown_section():
with pytest.raises(
Exception,
match=re.escape(
"Syntax error at line 3: Invalid specification heading (`Unknown Section:`). Allowed headings: definitions, implementation reqs, test reqs, functional specs, acceptance tests"
"Syntax error at line 4: Invalid specification heading (`Unknown Section:`). Allowed headings: definitions, implementation reqs, test reqs, functional specs, acceptance tests"
),
):
plain_file.parse_plain_source(plain_source, {}, [], [], [])
Expand All @@ -50,7 +50,7 @@ def test_duplicate_section():
"""
with pytest.raises(
Exception,
match=re.escape("Syntax error at line 3: Duplicate specification heading (`definitions`)"),
match=re.escape("Syntax error at line 4: Duplicate specification heading (`definitions`)"),
):
plain_file.parse_plain_source(plain_source, {}, [], [], [])

Expand All @@ -64,11 +64,48 @@ def test_invalid_top_level_element():
"""
with pytest.raises(
Exception,
match=re.escape("Syntax error at line 2: Invalid source structure (`code block`)"),
match=re.escape("Syntax error at line 3: Invalid source structure (`code block`)"),
):
plain_file.parse_plain_source(plain_source, {}, [], [], [])


def test_syntax_error_line_number_accounts_for_frontmatter():
plain_source = """---
description: 'Plain file with frontmatter'
---

***definitions***

***Unknown Section:***
"""
with pytest.raises(
Exception,
match=re.escape("Syntax error at line 7: Invalid specification heading (`Unknown Section:`)"),
):
plain_file.parse_plain_source(plain_source, {}, [], [], [])


def test_syntax_error_line_number_with_windows_line_endings():
plain_source = (
"---\r\n"
"description: 'Plain file with frontmatter'\r\n"
"---\r\n"
"\r\n"
"***definitions***\r\n"
"\r\n"
"***Unknown Section:***\r\n"
)
with pytest.raises(
Exception,
match=re.escape("Syntax error at line 7: Invalid specification heading (`Unknown Section:`)"),
):
plain_file.parse_plain_source(plain_source, {}, [], [], [])


def test_normalize_line_endings_does_not_duplicate_newlines():
assert plain_file.normalize_line_endings("a\r\nb\rc\nd") == "a\nb\nc\nd"


def test_plain_file_parser_with_comments(get_test_data_path):
_, plain_sections, _ = plain_file.plain_file_parser(
"plain_file_parser_with_comments.plain",
Expand Down Expand Up @@ -423,7 +460,7 @@ def test_acceptance_tests_top_level_rejected():
with pytest.raises(
PlainSyntaxError,
match=re.escape(
"Syntax error at line 1: acceptance tests heading should be nested under specific functional spec."
"Syntax error at line 2: acceptance tests heading should be nested under specific functional spec."
),
):
plain_file.parse_plain_source(plain_source, {}, [], [], [])
Expand Down
Loading