plain-parser 0.1.2.dev3__tar.gz → 0.1.2.dev5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/PKG-INFO +1 -1
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/src/plain_parser/concept_utils.py +2 -2
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/src/plain_parser/loaders.py +33 -4
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/src/plain_parser/plain_file.py +28 -17
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/src/plain_parser/plain_spec.py +1 -1
- plain_parser-0.1.2.dev5/tests/data/plainfile/plain_source_with_missing_link.plain +9 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfile/task_manager_with_reference_links.plain +2 -2
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_noconcepts.plain +0 -1
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_redefinition.plain +0 -1
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_example.plain +1 -1
- plain_parser-0.1.2.dev5/tests/data/templates/malformed_tag.plain +4 -0
- plain_parser-0.1.2.dev5/tests/data/templates/unterminated_output.plain +3 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/test_cli.py +44 -40
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/test_loaders.py +87 -1
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/test_plainfileparser.py +54 -7
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/.flake8 +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/.github/workflows/lint-and-test.yml +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/.github/workflows/publish-dev-to-pypi.yml +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/.github/workflows/publish-to-pypi.yml +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/.gitignore +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/LICENSE +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/README.md +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/pyproject.toml +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/src/plain_parser/__init__.py +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/src/plain_parser/cli.py +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/src/plain_parser/exceptions.py +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/src/plain_parser/liquid_nodes.py +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/__init__.py +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/conftest.py +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/cli/acceptance_test_binary_resource.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/cli/acceptance_test_resource.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/cli/base64_resource.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/cli/binary.bin +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/cli/binary_resource.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/cli/code_variable_conflict.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/cli/code_variable_template.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/cli/missing_resource.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/cli/not_plain.md +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/cli/notes.md +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/cli/requires_binary_resource_base.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/cli/requires_binary_resource_top.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/cli/subdir/nested.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/cli/undefined_concept.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/cli/valid.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/imports/circular_imports_1.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/imports/circular_imports_2.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/imports/circular_imports_main.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/imports/diamond_import_1.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/imports/diamond_import_2.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/imports/diamond_import_common.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/imports/diamond_imports_main.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/imports/import_with_functionalities.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/imports/imports_module_with_functionalities.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/imports/non_existent_import.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfile/add_new_task_modal_specification.yaml +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfile/duplicate_specification_heading.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfile/empty_functional_specs_section.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfile/invalid_specification_order.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfile/missing_non_functional_requirements.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfile/no_functional_specs_section.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfile/plain_source_with_absolute_link.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfile/plain_source_with_url_link.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfile/task_list_ui_specification.yaml +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfile/without_non_functional_requirement.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_acceptance_tests.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_acceptance_tests_nondefined.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_defined_nondefined.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_defined_nondefined_2.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_definition.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_nonconcept.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_nondefined.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_several_concepts.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_valid.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/cyclic_definitions.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_base.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_defaults_exported.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_example.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_imported_exported.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_inherited_reexported.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_malformed.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_missing_definition.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_missing_definition_example.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet_undefined.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_nested.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_nested_example.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_no_definitions.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_not_declared.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_not_declared_example.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_transitive_example.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_transitive_l1.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_transitive_l2.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/plain_file_parser_with_comments.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/plain_file_with_comments_indented.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/regular_plain_source.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_defs.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_l1.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_l2.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_missing.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_module.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_partial.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_partial_duplicate.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/requires_without_definitions_example.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/topological_sort.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/topological_sort_not_referenced.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/chain_base.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/chain_fork_top.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/chain_middle.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/chain_top.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/circular_requires_main.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/circular_requires_sub.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/diamond_requires_1.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/diamond_requires_2.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/diamond_requires_common.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/diamond_requires_main.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/independent_requires_1.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/independent_requires_2.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/independent_requires_main.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/invalid_ancestor_base.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/invalid_ancestor_top.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/non_existent_require.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/normal_requires_1.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/normal_requires_2.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/normal_requires_common.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/requires/normal_requires_main.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/sample_base64_image.txt +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/simple.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/templates/block_level_include.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/templates/code_variables.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/templates/header.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/templates/implement.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/templates/implement_2.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/templates/template_include.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/data/templates/test_hardest_problem.plain +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/test_exceptions.py +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/test_imports.py +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/test_plainfile.py +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/test_plainspec.py +0 -0
- {plain_parser-0.1.2.dev3 → plain_parser-0.1.2.dev5}/tests/test_requires.py +0 -0
|
@@ -200,14 +200,14 @@ def sort_definitions(definitions: list[dict]) -> list[dict]:
|
|
|
200
200
|
concept_graph.add_node(node)
|
|
201
201
|
|
|
202
202
|
if not nx.is_directed_acyclic_graph(concept_graph):
|
|
203
|
-
msg = "Found cycles in the concept graph. Cycles are not allowed."
|
|
203
|
+
msg = "Found cycles in the concept graph. Cycles are not allowed. "
|
|
204
204
|
all_cycles = list(nx.simple_cycles(concept_graph))
|
|
205
205
|
|
|
206
206
|
for cycle in all_cycles:
|
|
207
207
|
cyclic_definitions = []
|
|
208
208
|
cyclic_definitions.append(concept_definitions[cycle[0]]["markdown"])
|
|
209
209
|
cyclic_definitions.append(concept_definitions[cycle[1]]["markdown"])
|
|
210
|
-
msg += "Cyclic
|
|
210
|
+
msg += "Cyclic definitions:\n"
|
|
211
211
|
msg += "\n".join(cyclic_definitions)
|
|
212
212
|
msg += "\n"
|
|
213
213
|
|
|
@@ -3,10 +3,10 @@ import re
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
5
|
from liquid2 import Environment, FileSystemLoader, StrictUndefined
|
|
6
|
-
from liquid2.exceptions import UndefinedError
|
|
6
|
+
from liquid2.exceptions import LiquidSyntaxError, UndefinedError
|
|
7
7
|
|
|
8
8
|
from plain_parser import plain_spec
|
|
9
|
-
from plain_parser.exceptions import UnsupportedBase64Content, UnsupportedResourceType
|
|
9
|
+
from plain_parser.exceptions import PlainSyntaxError, UnsupportedBase64Content, UnsupportedResourceType
|
|
10
10
|
from plain_parser.liquid_nodes import Plain2CodeIncludeTag, Plain2CodeLoaderMixin
|
|
11
11
|
|
|
12
12
|
MAX_BASE64_BLOB_LENGTH = 8192
|
|
@@ -36,6 +36,20 @@ def open_from(dirs, file_name):
|
|
|
36
36
|
return None
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
def resolve_linked_resource(dirs, file_name):
|
|
40
|
+
"""Return the first existing path for file_name across dirs, or None if not found.
|
|
41
|
+
|
|
42
|
+
Mirrors the search order used by open_from so that validating a linked resource and
|
|
43
|
+
loading it can never disagree.
|
|
44
|
+
"""
|
|
45
|
+
for dir in dirs:
|
|
46
|
+
full_file_name = os.path.join(dir, file_name)
|
|
47
|
+
if os.path.exists(full_file_name):
|
|
48
|
+
return full_file_name
|
|
49
|
+
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
39
53
|
def load_linked_resources(template_dirs: list[str], resources_list, module_name: str):
|
|
40
54
|
linked_resources = {}
|
|
41
55
|
|
|
@@ -81,7 +95,20 @@ class TrackingFileSystemLoader(Plain2CodeLoaderMixin, FileSystemLoader):
|
|
|
81
95
|
return source
|
|
82
96
|
|
|
83
97
|
|
|
84
|
-
def
|
|
98
|
+
def liquid_error_message(error: LiquidSyntaxError) -> str:
|
|
99
|
+
# Built from attributes: str() of some liquid2 syntax errors (e.g. an unterminated `{{ x`) raises ValueError.
|
|
100
|
+
template_name = error.template_name or None
|
|
101
|
+
token = error.token
|
|
102
|
+
if token is None or token.start < 0:
|
|
103
|
+
where = f" in {template_name}" if template_name else ""
|
|
104
|
+
else:
|
|
105
|
+
line = token.source[: token.start].count("\n") + 1
|
|
106
|
+
column = token.start - (token.source.rfind("\n", 0, token.start) + 1)
|
|
107
|
+
where = f" at {template_name}:{line}:{column}" if template_name else f" at {line}:{column}"
|
|
108
|
+
return f"Plain syntax error: Invalid Liquid{where}: {error.message}"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_loaded_templates(source_path, plain_source, template_name=None):
|
|
85
112
|
# Render the plain source with Liquid templating engine
|
|
86
113
|
# to identify the templates that are being loaded
|
|
87
114
|
|
|
@@ -92,9 +119,11 @@ def get_loaded_templates(source_path, plain_source):
|
|
|
92
119
|
liquid_env.filters["code_variable"] = plain_spec.code_variable_liquid_filter
|
|
93
120
|
liquid_env.filters["prohibited_chars"] = plain_spec.prohibited_chars_liquid_filter
|
|
94
121
|
|
|
95
|
-
plain_source_template = liquid_env.from_string(plain_source)
|
|
96
122
|
try:
|
|
123
|
+
plain_source_template = liquid_env.from_string(plain_source, name=template_name or "")
|
|
97
124
|
plain_source = plain_source_template.render()
|
|
125
|
+
except LiquidSyntaxError as e:
|
|
126
|
+
raise PlainSyntaxError(liquid_error_message(e))
|
|
98
127
|
except UndefinedError as e:
|
|
99
128
|
raise Exception(f"Undefined liquid variable: {str(e)}")
|
|
100
129
|
|
|
@@ -88,7 +88,7 @@ def remove_quotes(token):
|
|
|
88
88
|
token.children = tuple(new_children)
|
|
89
89
|
|
|
90
90
|
|
|
91
|
-
def check_section_for_linked_resources(section):
|
|
91
|
+
def check_section_for_linked_resources(section, template_dirs):
|
|
92
92
|
linked_resources = []
|
|
93
93
|
for link in traverse(section, klass=Link):
|
|
94
94
|
parsed_url = urlparse(link.node.target)
|
|
@@ -97,11 +97,19 @@ def check_section_for_linked_resources(section):
|
|
|
97
97
|
f"Plain syntax error: Only relative links are allowed (text: {link.node.children[0].content}, target: {link.node.target})."
|
|
98
98
|
)
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
resolved_target = loaders.resolve_linked_resource(template_dirs, link.node.target)
|
|
101
|
+
if resolved_target is None:
|
|
102
|
+
searched_dirs = "\n".join(f" {position}. {dir}" for position, dir in enumerate(template_dirs, start=1))
|
|
103
|
+
raise PlainSyntaxError(
|
|
104
|
+
f"Plain syntax error: Link {link.node.target} does not exist. "
|
|
105
|
+
f"Linked resources are looked up relative to the following directories "
|
|
106
|
+
f"(highest to lowest precedence):\n{searched_dirs}"
|
|
107
|
+
)
|
|
102
108
|
|
|
103
|
-
if not os.path.isfile(
|
|
104
|
-
raise PlainSyntaxError(
|
|
109
|
+
if not os.path.isfile(resolved_target):
|
|
110
|
+
raise PlainSyntaxError(
|
|
111
|
+
f"Plain syntax error: Link {link.node.target} must be a file (resolved to {resolved_target})."
|
|
112
|
+
)
|
|
105
113
|
|
|
106
114
|
if len(link.node.children) != 1:
|
|
107
115
|
raise PlainSyntaxError(f"Plain syntax error: Link must have text specified (link: {link.node.target}).")
|
|
@@ -120,15 +128,15 @@ def check_section_for_linked_resources(section):
|
|
|
120
128
|
section.linked_resources = linked_resources
|
|
121
129
|
|
|
122
130
|
|
|
123
|
-
def check_for_linked_resources(plain_source):
|
|
131
|
+
def check_for_linked_resources(plain_source, template_dirs):
|
|
124
132
|
for specification_heading in plain_spec.ALLOWED_SPECIFICATION_HEADINGS:
|
|
125
133
|
if specification_heading in plain_source and hasattr(plain_source[specification_heading], "children"):
|
|
126
134
|
for requirement in plain_source[specification_heading].children:
|
|
127
|
-
check_section_for_linked_resources(requirement)
|
|
135
|
+
check_section_for_linked_resources(requirement, template_dirs)
|
|
128
136
|
|
|
129
137
|
if hasattr(requirement, plain_spec.ACCEPTANCE_TESTS):
|
|
130
138
|
for acceptance_test in requirement.acceptance_tests:
|
|
131
|
-
check_section_for_linked_resources(acceptance_test)
|
|
139
|
+
check_section_for_linked_resources(acceptance_test, template_dirs)
|
|
132
140
|
|
|
133
141
|
|
|
134
142
|
def process_section_code_variables(section, code_variables):
|
|
@@ -232,7 +240,7 @@ def _is_acceptance_test_heading(token) -> tuple[bool, str | None]:
|
|
|
232
240
|
content = strong_children[0].content.strip()
|
|
233
241
|
if content == plain_spec.ACCEPTANCE_TEST_HEADING:
|
|
234
242
|
return True, None
|
|
235
|
-
problem = f"Syntax error at line {token.line_number}: Invalid acceptance test heading (`{content}`). Expected: `{plain_spec.ACCEPTANCE_TEST_HEADING}
|
|
243
|
+
problem = f"Syntax error at line {token.line_number}: Invalid acceptance test heading (`{content}`). Expected: `{plain_spec.ACCEPTANCE_TEST_HEADING}`."
|
|
236
244
|
return False, problem
|
|
237
245
|
|
|
238
246
|
|
|
@@ -500,6 +508,7 @@ def parse_plain_source( # noqa: C901
|
|
|
500
508
|
template_dirs: list[str],
|
|
501
509
|
imported_modules: list[str],
|
|
502
510
|
modules_trace: list[str],
|
|
511
|
+
module_name: str | None = None,
|
|
503
512
|
) -> PlainFileParseResult:
|
|
504
513
|
plain_source_text = normalize_line_endings(plain_source_text)
|
|
505
514
|
|
|
@@ -519,7 +528,7 @@ def parse_plain_source( # noqa: C901
|
|
|
519
528
|
else:
|
|
520
529
|
required_concepts = list[str]()
|
|
521
530
|
|
|
522
|
-
[_, loaded_templates] = loaders.get_loaded_templates(template_dirs, plain_source_text)
|
|
531
|
+
[_, loaded_templates] = loaders.get_loaded_templates(template_dirs, plain_source_text, module_name)
|
|
523
532
|
|
|
524
533
|
plain_source_content = restore_stripped_lines(plain_source_text, plain_source_obj.content)
|
|
525
534
|
|
|
@@ -577,7 +586,7 @@ def parse_plain_source( # noqa: C901
|
|
|
577
586
|
|
|
578
587
|
if specification_heading == plain_spec.DEFINITIONS and current_specification_heading is not None:
|
|
579
588
|
raise PlainSyntaxError(
|
|
580
|
-
f"Plain syntax error: Syntax error at line {token.line_number}: Definitions specification must be the first specification in the section
|
|
589
|
+
f"Plain syntax error: Syntax error at line {token.line_number}: Definitions specification must be the first specification in the section (`{token_text}`)"
|
|
581
590
|
)
|
|
582
591
|
|
|
583
592
|
current_specification_heading = specification_heading
|
|
@@ -594,7 +603,7 @@ def parse_plain_source( # noqa: C901
|
|
|
594
603
|
|
|
595
604
|
else:
|
|
596
605
|
raise PlainSyntaxError(
|
|
597
|
-
f"Plain syntax error: Syntax error at line {token.line_number}: Invalid source structure
|
|
606
|
+
f"Plain syntax error: Syntax error at line {token.line_number}: Invalid source structure (`{token_text}`)"
|
|
598
607
|
)
|
|
599
608
|
|
|
600
609
|
# Acceptance tests are only allowed nested under functional specs. Reject them when
|
|
@@ -673,8 +682,8 @@ def read_module_plain_source(module_name: str, template_dirs: list[str]) -> str:
|
|
|
673
682
|
if blob is not None:
|
|
674
683
|
raise UnsupportedBase64Content(
|
|
675
684
|
f"Module '{module_name}' contains a base64-encoded blob ({len(blob)} characters) "
|
|
676
|
-
"inlined in the specification. This is not supported."
|
|
677
|
-
"Remove the base64 data from the .plain file or if necessary,"
|
|
685
|
+
"inlined in the specification. This is not supported. "
|
|
686
|
+
"Remove the base64 data from the .plain file or if necessary, "
|
|
678
687
|
"include the binary file path in the specification."
|
|
679
688
|
)
|
|
680
689
|
|
|
@@ -696,6 +705,7 @@ def parse_plain_file(
|
|
|
696
705
|
template_dirs,
|
|
697
706
|
imported_modules,
|
|
698
707
|
modules_trace + [module_name],
|
|
708
|
+
module_name=module_name,
|
|
699
709
|
)
|
|
700
710
|
|
|
701
711
|
|
|
@@ -756,7 +766,7 @@ def process_required_modules(
|
|
|
756
766
|
)
|
|
757
767
|
|
|
758
768
|
marshalled_plain_source = validate_and_marshall_module(
|
|
759
|
-
plain_file_parse_result, module_name, ancestor_exported_definitions, code_variables
|
|
769
|
+
plain_file_parse_result, module_name, ancestor_exported_definitions, code_variables, template_dirs
|
|
760
770
|
)
|
|
761
771
|
if module_name not in (chain_module_name for chain_module_name, _ in chain):
|
|
762
772
|
chain.append((module_name, marshalled_plain_source))
|
|
@@ -793,6 +803,7 @@ def validate_and_marshall_module(
|
|
|
793
803
|
module_name: str,
|
|
794
804
|
exported_definitions: list[mistletoe.block_token.token],
|
|
795
805
|
code_variables: dict,
|
|
806
|
+
template_dirs: list[str],
|
|
796
807
|
) -> dict:
|
|
797
808
|
"""Every check a module must pass after parsing, and its marshalled plain source tree."""
|
|
798
809
|
if len(plain_file_parse_result.required_concepts) > 0:
|
|
@@ -826,7 +837,7 @@ def validate_and_marshall_module(
|
|
|
826
837
|
|
|
827
838
|
process_acceptance_tests(plain_file_parse_result.plain_source)
|
|
828
839
|
|
|
829
|
-
check_for_linked_resources(plain_file_parse_result.plain_source)
|
|
840
|
+
check_for_linked_resources(plain_file_parse_result.plain_source, template_dirs)
|
|
830
841
|
|
|
831
842
|
marshalled_plain_source = marshall_plain_source(plain_file_parse_result.plain_source)
|
|
832
843
|
|
|
@@ -881,7 +892,7 @@ def _parse_module(
|
|
|
881
892
|
)
|
|
882
893
|
|
|
883
894
|
marshalled_plain_source = validate_and_marshall_module(
|
|
884
|
-
plain_file_parse_result, module_name, exported_definitions, code_variables
|
|
895
|
+
plain_file_parse_result, module_name, exported_definitions, code_variables, template_dirs
|
|
885
896
|
)
|
|
886
897
|
|
|
887
898
|
return module_name, marshalled_plain_source, plain_file_parse_result.required_modules, chain
|
|
@@ -126,7 +126,7 @@ def collect_linked_resources(
|
|
|
126
126
|
):
|
|
127
127
|
|
|
128
128
|
if not isinstance(plain_source_tree, dict):
|
|
129
|
-
raise ValueError("
|
|
129
|
+
raise ValueError("plain_source_tree must be a dictionary.")
|
|
130
130
|
|
|
131
131
|
if frid is not None:
|
|
132
132
|
functional_requirements = get_frids(plain_source_tree)
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
|
|
30
30
|
- Implement the entry point for :App:.
|
|
31
31
|
|
|
32
|
-
- Show :TaskList:. The details of the user interface are provided in the file [task_list_ui_specification.yaml](
|
|
32
|
+
- Show :TaskList:. The details of the user interface are provided in the file [task_list_ui_specification.yaml](task_list_ui_specification.yaml).
|
|
33
33
|
|
|
34
|
-
- :User: should be able to add :Task:. Only valid :Task: items can be added. The details of the user interface are provided in the file [add_new_task_modal_specification.yaml](
|
|
34
|
+
- :User: should be able to add :Task:. Only valid :Task: items can be added. The details of the user interface are provided in the file [add_new_task_modal_specification.yaml](add_new_task_modal_specification.yaml).
|
|
35
35
|
|
|
36
36
|
- :User: should be able to delete :Task:.
|
|
37
37
|
|
|
@@ -9,12 +9,12 @@ from plain_parser import cli
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@pytest.fixture
|
|
12
|
-
def
|
|
12
|
+
def cli_specs_dir(get_test_data_path):
|
|
13
13
|
return get_test_data_path("data/cli")
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def test_check_valid_module_exits_zero(
|
|
17
|
-
exit_code = cli.main(["check", os.path.join(
|
|
16
|
+
def test_check_valid_module_exits_zero(cli_specs_dir, capsys):
|
|
17
|
+
exit_code = cli.main(["check", os.path.join(cli_specs_dir, "valid.plain")])
|
|
18
18
|
|
|
19
19
|
captured = capsys.readouterr()
|
|
20
20
|
assert exit_code == 0
|
|
@@ -36,7 +36,7 @@ def test_without_subcommand_is_usage_error(capsys):
|
|
|
36
36
|
assert exc_info.value.code == 2
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def test_console_script_is_installed_and_exits_zero(
|
|
39
|
+
def test_console_script_is_installed_and_exits_zero(cli_specs_dir):
|
|
40
40
|
# The venv's script dir may not be on PATH when pytest is run through an absolute interpreter path.
|
|
41
41
|
search_path = os.pathsep.join([os.path.dirname(sys.executable), os.environ.get("PATH", "")])
|
|
42
42
|
script = shutil.which("plain-parser", path=search_path)
|
|
@@ -46,7 +46,7 @@ def test_console_script_is_installed_and_exits_zero(cli_data_dir):
|
|
|
46
46
|
pytest.fail(message) if os.environ.get("CI") else pytest.skip(message)
|
|
47
47
|
|
|
48
48
|
result = subprocess.run(
|
|
49
|
-
[script, "check", os.path.join(
|
|
49
|
+
[script, "check", os.path.join(cli_specs_dir, "valid.plain")], capture_output=True, text=True
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
assert result.returncode == 0
|
|
@@ -66,32 +66,32 @@ def _assert_ok(result):
|
|
|
66
66
|
assert out.endswith(": OK\n"), out
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
def test_check_valid_module_in_subdirectory_from_parent(
|
|
70
|
-
monkeypatch.chdir(
|
|
69
|
+
def test_check_valid_module_in_subdirectory_from_parent(cli_specs_dir, capsys, monkeypatch):
|
|
70
|
+
monkeypatch.chdir(cli_specs_dir)
|
|
71
71
|
|
|
72
72
|
exit_code, out, err = _run_check(capsys, os.path.join("subdir", "nested.plain"))
|
|
73
73
|
|
|
74
74
|
_assert_ok((exit_code, out, err))
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
def test_check_valid_module_with_linked_resource_in_acceptance_test(
|
|
78
|
-
monkeypatch.chdir(
|
|
77
|
+
def test_check_valid_module_with_linked_resource_in_acceptance_test(cli_specs_dir, capsys, monkeypatch, tmp_path):
|
|
78
|
+
monkeypatch.chdir(tmp_path)
|
|
79
79
|
|
|
80
|
-
exit_code, out, err = _run_check(capsys, "acceptance_test_resource.plain")
|
|
80
|
+
exit_code, out, err = _run_check(capsys, os.path.join(cli_specs_dir, "acceptance_test_resource.plain"))
|
|
81
81
|
|
|
82
82
|
_assert_ok((exit_code, out, err))
|
|
83
83
|
|
|
84
84
|
|
|
85
|
-
def test_check_nonexistent_template_dir_is_ignored(
|
|
85
|
+
def test_check_nonexistent_template_dir_is_ignored(cli_specs_dir, capsys):
|
|
86
86
|
exit_code, out, err = _run_check(
|
|
87
|
-
capsys, os.path.join(
|
|
87
|
+
capsys, os.path.join(cli_specs_dir, "valid.plain"), "--template-dir", os.path.join(cli_specs_dir, "nope")
|
|
88
88
|
)
|
|
89
89
|
|
|
90
90
|
_assert_ok((exit_code, out, err))
|
|
91
91
|
|
|
92
92
|
|
|
93
|
-
def test_check_undefined_concept(
|
|
94
|
-
exit_code, out, err = _run_check(capsys, os.path.join(
|
|
93
|
+
def test_check_undefined_concept(cli_specs_dir, capsys):
|
|
94
|
+
exit_code, out, err = _run_check(capsys, os.path.join(cli_specs_dir, "undefined_concept.plain"))
|
|
95
95
|
|
|
96
96
|
assert exit_code == 1
|
|
97
97
|
assert out == ""
|
|
@@ -101,30 +101,34 @@ def test_check_undefined_concept(cli_data_dir, capsys):
|
|
|
101
101
|
assert "Traceback" not in err
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
def test_check_missing_linked_resource(
|
|
105
|
-
monkeypatch.chdir(
|
|
104
|
+
def test_check_missing_linked_resource(cli_specs_dir, capsys, monkeypatch, tmp_path):
|
|
105
|
+
monkeypatch.chdir(tmp_path)
|
|
106
106
|
|
|
107
|
-
exit_code, out, err = _run_check(capsys, "missing_resource.plain")
|
|
107
|
+
exit_code, out, err = _run_check(capsys, os.path.join(cli_specs_dir, "missing_resource.plain"))
|
|
108
108
|
|
|
109
109
|
assert exit_code == 1
|
|
110
110
|
assert out == ""
|
|
111
|
-
assert err ==
|
|
111
|
+
assert err == (
|
|
112
|
+
"Error: Plain syntax error: Link does_not_exist.md does not exist. "
|
|
113
|
+
"Linked resources are looked up relative to the following directories (highest to lowest precedence):\n"
|
|
114
|
+
f" 1. {cli_specs_dir}\n"
|
|
115
|
+
)
|
|
112
116
|
|
|
113
117
|
|
|
114
|
-
def test_check_binary_linked_resource(
|
|
115
|
-
monkeypatch.chdir(
|
|
118
|
+
def test_check_binary_linked_resource(cli_specs_dir, capsys, monkeypatch, tmp_path):
|
|
119
|
+
monkeypatch.chdir(tmp_path)
|
|
116
120
|
|
|
117
|
-
exit_code, out, err = _run_check(capsys, "binary_resource.plain")
|
|
121
|
+
exit_code, out, err = _run_check(capsys, os.path.join(cli_specs_dir, "binary_resource.plain"))
|
|
118
122
|
|
|
119
123
|
assert exit_code == 1
|
|
120
124
|
assert out == ""
|
|
121
125
|
assert err.startswith("Error: Referenced resource 'binary.bin' in module 'binary_resource' is a binary file.")
|
|
122
126
|
|
|
123
127
|
|
|
124
|
-
def test_check_base64_blob_linked_resource(
|
|
125
|
-
monkeypatch.chdir(
|
|
128
|
+
def test_check_base64_blob_linked_resource(cli_specs_dir, capsys, monkeypatch, tmp_path):
|
|
129
|
+
monkeypatch.chdir(tmp_path)
|
|
126
130
|
|
|
127
|
-
exit_code, out, err = _run_check(capsys, "base64_resource.plain")
|
|
131
|
+
exit_code, out, err = _run_check(capsys, os.path.join(cli_specs_dir, "base64_resource.plain"))
|
|
128
132
|
|
|
129
133
|
assert exit_code == 1
|
|
130
134
|
assert out == ""
|
|
@@ -134,10 +138,10 @@ def test_check_base64_blob_linked_resource(cli_data_dir, capsys, monkeypatch):
|
|
|
134
138
|
)
|
|
135
139
|
|
|
136
140
|
|
|
137
|
-
def test_check_invalid_linked_resource_in_required_module(
|
|
138
|
-
monkeypatch.chdir(
|
|
141
|
+
def test_check_invalid_linked_resource_in_required_module(cli_specs_dir, capsys, monkeypatch, tmp_path):
|
|
142
|
+
monkeypatch.chdir(tmp_path)
|
|
139
143
|
|
|
140
|
-
exit_code, out, err = _run_check(capsys, "requires_binary_resource_top.plain")
|
|
144
|
+
exit_code, out, err = _run_check(capsys, os.path.join(cli_specs_dir, "requires_binary_resource_top.plain"))
|
|
141
145
|
|
|
142
146
|
assert exit_code == 1
|
|
143
147
|
assert out == ""
|
|
@@ -146,24 +150,24 @@ def test_check_invalid_linked_resource_in_required_module(cli_data_dir, capsys,
|
|
|
146
150
|
)
|
|
147
151
|
|
|
148
152
|
|
|
149
|
-
def test_check_code_variable_with_two_values(
|
|
150
|
-
exit_code, out, err = _run_check(capsys, os.path.join(
|
|
153
|
+
def test_check_code_variable_with_two_values(cli_specs_dir, capsys):
|
|
154
|
+
exit_code, out, err = _run_check(capsys, os.path.join(cli_specs_dir, "code_variable_conflict.plain"))
|
|
151
155
|
|
|
152
156
|
assert exit_code == 1
|
|
153
157
|
assert out == ""
|
|
154
158
|
assert err == "Error: Code variable variable_name has multiple values: first and second\n"
|
|
155
159
|
|
|
156
160
|
|
|
157
|
-
def test_check_missing_plain_file(
|
|
158
|
-
exit_code, out, err = _run_check(capsys, os.path.join(
|
|
161
|
+
def test_check_missing_plain_file(cli_specs_dir, capsys):
|
|
162
|
+
exit_code, out, err = _run_check(capsys, os.path.join(cli_specs_dir, "does_not_exist.plain"))
|
|
159
163
|
|
|
160
164
|
assert exit_code == 1
|
|
161
165
|
assert out == ""
|
|
162
166
|
assert err == "Error: Module does not exist (does_not_exist).\n"
|
|
163
167
|
|
|
164
168
|
|
|
165
|
-
def test_check_wrong_extension(
|
|
166
|
-
exit_code, out, err = _run_check(capsys, os.path.join(
|
|
169
|
+
def test_check_wrong_extension(cli_specs_dir, capsys):
|
|
170
|
+
exit_code, out, err = _run_check(capsys, os.path.join(cli_specs_dir, "not_plain.md"))
|
|
167
171
|
|
|
168
172
|
assert exit_code == 1
|
|
169
173
|
assert out == ""
|
|
@@ -189,10 +193,10 @@ def test_check_empty_template_dir_is_ignored(tmp_path, capsys, monkeypatch):
|
|
|
189
193
|
assert err == "Error: Module does not exist (helper).\n"
|
|
190
194
|
|
|
191
195
|
|
|
192
|
-
def test_check_binary_linked_resource_in_acceptance_test(
|
|
193
|
-
monkeypatch.chdir(
|
|
196
|
+
def test_check_binary_linked_resource_in_acceptance_test(cli_specs_dir, capsys, monkeypatch, tmp_path):
|
|
197
|
+
monkeypatch.chdir(tmp_path)
|
|
194
198
|
|
|
195
|
-
exit_code, out, err = _run_check(capsys, "acceptance_test_binary_resource.plain")
|
|
199
|
+
exit_code, out, err = _run_check(capsys, os.path.join(cli_specs_dir, "acceptance_test_binary_resource.plain"))
|
|
196
200
|
|
|
197
201
|
assert exit_code == 1
|
|
198
202
|
assert out == ""
|
|
@@ -202,13 +206,13 @@ def test_check_binary_linked_resource_in_acceptance_test(cli_data_dir, capsys, m
|
|
|
202
206
|
|
|
203
207
|
|
|
204
208
|
@pytest.mark.parametrize("interrupted_function", ["check", "resolve_config_file"])
|
|
205
|
-
def test_check_interrupted_exits_130_without_traceback(
|
|
209
|
+
def test_check_interrupted_exits_130_without_traceback(cli_specs_dir, capsys, monkeypatch, interrupted_function):
|
|
206
210
|
def interrupt(*_):
|
|
207
211
|
raise KeyboardInterrupt
|
|
208
212
|
|
|
209
213
|
monkeypatch.setattr(cli, interrupted_function, interrupt)
|
|
210
214
|
|
|
211
|
-
exit_code = cli.main(["check", os.path.join(
|
|
215
|
+
exit_code = cli.main(["check", os.path.join(cli_specs_dir, "valid.plain")])
|
|
212
216
|
|
|
213
217
|
captured = capsys.readouterr()
|
|
214
218
|
assert exit_code == 130
|
|
@@ -317,8 +321,8 @@ def test_check_unreadable_config_is_usage_error(tmp_path, capsys):
|
|
|
317
321
|
assert "Error reading config file" in capsys.readouterr().err
|
|
318
322
|
|
|
319
323
|
|
|
320
|
-
def test_check_valid_module_prints_ok_line_with_file_as_given(
|
|
321
|
-
monkeypatch.chdir(
|
|
324
|
+
def test_check_valid_module_prints_ok_line_with_file_as_given(cli_specs_dir, capsys, monkeypatch):
|
|
325
|
+
monkeypatch.chdir(cli_specs_dir)
|
|
322
326
|
|
|
323
327
|
exit_code, out, err = _run_check(capsys, "valid.plain")
|
|
324
328
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import re
|
|
2
3
|
import tempfile
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
5
6
|
|
|
6
7
|
from plain_parser.exceptions import UnsupportedBase64Content, UnsupportedResourceType
|
|
7
|
-
from plain_parser.loaders import MAX_BASE64_BLOB_LENGTH, load_linked_resources
|
|
8
|
+
from plain_parser.loaders import MAX_BASE64_BLOB_LENGTH, load_linked_resources, resolve_linked_resource
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@pytest.fixture
|
|
@@ -96,3 +97,88 @@ def test_get_loaded_templates_missing_include_names_template(template_dir):
|
|
|
96
97
|
message = str(exc_info.value)
|
|
97
98
|
assert "nope.plain" in message
|
|
98
99
|
assert "--template-dir" not in message
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_resolve_linked_resource_returns_none_when_missing(template_dir):
|
|
103
|
+
assert resolve_linked_resource([template_dir], "missing.md") is None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_resolve_linked_resource_first_dir_wins():
|
|
107
|
+
with tempfile.TemporaryDirectory() as first, tempfile.TemporaryDirectory() as second:
|
|
108
|
+
for d in (first, second):
|
|
109
|
+
with open(os.path.join(d, "notes.md"), "w") as f:
|
|
110
|
+
f.write("x")
|
|
111
|
+
|
|
112
|
+
assert resolve_linked_resource([first, second], "notes.md") == os.path.join(first, "notes.md")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_resolve_linked_resource_falls_back_to_later_dir():
|
|
116
|
+
with tempfile.TemporaryDirectory() as first, tempfile.TemporaryDirectory() as second:
|
|
117
|
+
with open(os.path.join(second, "notes.md"), "w") as f:
|
|
118
|
+
f.write("x")
|
|
119
|
+
|
|
120
|
+
assert resolve_linked_resource([first, second], "notes.md") == os.path.join(second, "notes.md")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_resolve_linked_resource_parent_relative_path():
|
|
124
|
+
with tempfile.TemporaryDirectory() as root:
|
|
125
|
+
spec_dir = os.path.join(root, "plain")
|
|
126
|
+
os.mkdir(spec_dir)
|
|
127
|
+
with open(os.path.join(root, "shared.md"), "w") as f:
|
|
128
|
+
f.write("x")
|
|
129
|
+
|
|
130
|
+
assert resolve_linked_resource([spec_dir], "../shared.md") == os.path.join(spec_dir, "../shared.md")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_resolve_linked_resource_directory_in_first_dir_shadows_file_in_second():
|
|
134
|
+
# Parity with codeplain: existence, not file-ness, decides the match.
|
|
135
|
+
with tempfile.TemporaryDirectory() as first, tempfile.TemporaryDirectory() as second:
|
|
136
|
+
os.mkdir(os.path.join(first, "notes.md"))
|
|
137
|
+
with open(os.path.join(second, "notes.md"), "w") as f:
|
|
138
|
+
f.write("x")
|
|
139
|
+
|
|
140
|
+
assert resolve_linked_resource([first, second], "notes.md") == os.path.join(first, "notes.md")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _load_template_fixture(name):
|
|
144
|
+
path = os.path.join(os.path.dirname(__file__), "data", "templates", f"{name}.plain")
|
|
145
|
+
with open(path, encoding="utf-8") as f:
|
|
146
|
+
return os.path.dirname(path), f.read()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_get_loaded_templates_malformed_tag_raises_plain_syntax_error():
|
|
150
|
+
from plain_parser.exceptions import PlainSyntaxError
|
|
151
|
+
from plain_parser.loaders import get_loaded_templates
|
|
152
|
+
|
|
153
|
+
directory, source = _load_template_fixture("malformed_tag")
|
|
154
|
+
|
|
155
|
+
with pytest.raises(
|
|
156
|
+
PlainSyntaxError,
|
|
157
|
+
match=re.escape("Plain syntax error: Invalid Liquid at malformed_tag:3:0: "),
|
|
158
|
+
):
|
|
159
|
+
get_loaded_templates([directory], source, "malformed_tag")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_get_loaded_templates_unterminated_output_raises_plain_syntax_error():
|
|
163
|
+
from plain_parser.exceptions import PlainSyntaxError
|
|
164
|
+
from plain_parser.loaders import get_loaded_templates
|
|
165
|
+
|
|
166
|
+
directory, source = _load_template_fixture("unterminated_output")
|
|
167
|
+
|
|
168
|
+
with pytest.raises(
|
|
169
|
+
PlainSyntaxError,
|
|
170
|
+
match=re.escape("Plain syntax error: Invalid Liquid at unterminated_output:4:0: "),
|
|
171
|
+
):
|
|
172
|
+
get_loaded_templates([directory], source, "unterminated_output")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_liquid_error_message_without_position():
|
|
176
|
+
from liquid2.exceptions import LiquidSyntaxError
|
|
177
|
+
|
|
178
|
+
from plain_parser.loaders import liquid_error_message
|
|
179
|
+
|
|
180
|
+
error = LiquidSyntaxError("missing expression", token=None, template_name="header")
|
|
181
|
+
assert liquid_error_message(error) == "Plain syntax error: Invalid Liquid in header: missing expression"
|
|
182
|
+
|
|
183
|
+
error = LiquidSyntaxError("missing expression", token=None)
|
|
184
|
+
assert liquid_error_message(error) == "Plain syntax error: Invalid Liquid: missing expression"
|