plain-parser 0.1.2.dev4__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.
Files changed (141) hide show
  1. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/PKG-INFO +1 -1
  2. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/src/plain_parser/concept_utils.py +2 -2
  3. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/src/plain_parser/loaders.py +19 -4
  4. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/src/plain_parser/plain_file.py +8 -6
  5. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/src/plain_parser/plain_spec.py +1 -1
  6. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_noconcepts.plain +0 -1
  7. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_redefinition.plain +0 -1
  8. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_example.plain +1 -1
  9. plain_parser-0.1.2.dev5/tests/data/templates/malformed_tag.plain +4 -0
  10. plain_parser-0.1.2.dev5/tests/data/templates/unterminated_output.plain +3 -0
  11. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/test_loaders.py +45 -0
  12. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/test_plainfileparser.py +11 -3
  13. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/.flake8 +0 -0
  14. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/.github/workflows/lint-and-test.yml +0 -0
  15. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/.github/workflows/publish-dev-to-pypi.yml +0 -0
  16. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/.github/workflows/publish-to-pypi.yml +0 -0
  17. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/.gitignore +0 -0
  18. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/LICENSE +0 -0
  19. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/README.md +0 -0
  20. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/pyproject.toml +0 -0
  21. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/src/plain_parser/__init__.py +0 -0
  22. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/src/plain_parser/cli.py +0 -0
  23. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/src/plain_parser/exceptions.py +0 -0
  24. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/src/plain_parser/liquid_nodes.py +0 -0
  25. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/__init__.py +0 -0
  26. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/conftest.py +0 -0
  27. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/cli/acceptance_test_binary_resource.plain +0 -0
  28. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/cli/acceptance_test_resource.plain +0 -0
  29. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/cli/base64_resource.plain +0 -0
  30. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/cli/binary.bin +0 -0
  31. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/cli/binary_resource.plain +0 -0
  32. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/cli/code_variable_conflict.plain +0 -0
  33. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/cli/code_variable_template.plain +0 -0
  34. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/cli/missing_resource.plain +0 -0
  35. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/cli/not_plain.md +0 -0
  36. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/cli/notes.md +0 -0
  37. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/cli/requires_binary_resource_base.plain +0 -0
  38. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/cli/requires_binary_resource_top.plain +0 -0
  39. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/cli/subdir/nested.plain +0 -0
  40. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/cli/undefined_concept.plain +0 -0
  41. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/cli/valid.plain +0 -0
  42. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/imports/circular_imports_1.plain +0 -0
  43. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/imports/circular_imports_2.plain +0 -0
  44. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/imports/circular_imports_main.plain +0 -0
  45. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/imports/diamond_import_1.plain +0 -0
  46. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/imports/diamond_import_2.plain +0 -0
  47. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/imports/diamond_import_common.plain +0 -0
  48. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/imports/diamond_imports_main.plain +0 -0
  49. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/imports/import_with_functionalities.plain +0 -0
  50. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/imports/imports_module_with_functionalities.plain +0 -0
  51. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/imports/non_existent_import.plain +0 -0
  52. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfile/add_new_task_modal_specification.yaml +0 -0
  53. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfile/duplicate_specification_heading.plain +0 -0
  54. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfile/empty_functional_specs_section.plain +0 -0
  55. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfile/invalid_specification_order.plain +0 -0
  56. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfile/missing_non_functional_requirements.plain +0 -0
  57. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfile/no_functional_specs_section.plain +0 -0
  58. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfile/plain_source_with_absolute_link.plain +0 -0
  59. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfile/plain_source_with_missing_link.plain +0 -0
  60. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfile/plain_source_with_url_link.plain +0 -0
  61. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfile/task_list_ui_specification.yaml +0 -0
  62. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfile/task_manager_with_reference_links.plain +0 -0
  63. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfile/without_non_functional_requirement.plain +0 -0
  64. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_acceptance_tests.plain +0 -0
  65. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_acceptance_tests_nondefined.plain +0 -0
  66. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_defined_nondefined.plain +0 -0
  67. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_defined_nondefined_2.plain +0 -0
  68. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_definition.plain +0 -0
  69. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_nonconcept.plain +0 -0
  70. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_nondefined.plain +0 -0
  71. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_several_concepts.plain +0 -0
  72. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/concept_validation_valid.plain +0 -0
  73. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/cyclic_definitions.plain +0 -0
  74. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts.plain +0 -0
  75. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_base.plain +0 -0
  76. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_defaults_exported.plain +0 -0
  77. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_example.plain +0 -0
  78. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_imported_exported.plain +0 -0
  79. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_inherited_reexported.plain +0 -0
  80. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_malformed.plain +0 -0
  81. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_missing_definition.plain +0 -0
  82. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_missing_definition_example.plain +0 -0
  83. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet.plain +0 -0
  84. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet_undefined.plain +0 -0
  85. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_nested.plain +0 -0
  86. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_nested_example.plain +0 -0
  87. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_no_definitions.plain +0 -0
  88. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_not_declared.plain +0 -0
  89. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_not_declared_example.plain +0 -0
  90. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_transitive_example.plain +0 -0
  91. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_transitive_l1.plain +0 -0
  92. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/exported_concepts_transitive_l2.plain +0 -0
  93. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/plain_file_parser_with_comments.plain +0 -0
  94. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/plain_file_with_comments_indented.plain +0 -0
  95. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/regular_plain_source.plain +0 -0
  96. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts.plain +0 -0
  97. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_defs.plain +0 -0
  98. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_l1.plain +0 -0
  99. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_l2.plain +0 -0
  100. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_missing.plain +0 -0
  101. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_module.plain +0 -0
  102. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_partial.plain +0 -0
  103. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/required_concepts_partial_duplicate.plain +0 -0
  104. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/requires_without_definitions_example.plain +0 -0
  105. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/topological_sort.plain +0 -0
  106. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/plainfileparser/topological_sort_not_referenced.plain +0 -0
  107. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/chain_base.plain +0 -0
  108. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/chain_fork_top.plain +0 -0
  109. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/chain_middle.plain +0 -0
  110. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/chain_top.plain +0 -0
  111. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/circular_requires_main.plain +0 -0
  112. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/circular_requires_sub.plain +0 -0
  113. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/diamond_requires_1.plain +0 -0
  114. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/diamond_requires_2.plain +0 -0
  115. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/diamond_requires_common.plain +0 -0
  116. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/diamond_requires_main.plain +0 -0
  117. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/independent_requires_1.plain +0 -0
  118. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/independent_requires_2.plain +0 -0
  119. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/independent_requires_main.plain +0 -0
  120. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/invalid_ancestor_base.plain +0 -0
  121. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/invalid_ancestor_top.plain +0 -0
  122. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/non_existent_require.plain +0 -0
  123. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/normal_requires_1.plain +0 -0
  124. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/normal_requires_2.plain +0 -0
  125. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/normal_requires_common.plain +0 -0
  126. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/requires/normal_requires_main.plain +0 -0
  127. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/sample_base64_image.txt +0 -0
  128. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/simple.plain +0 -0
  129. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/templates/block_level_include.plain +0 -0
  130. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/templates/code_variables.plain +0 -0
  131. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/templates/header.plain +0 -0
  132. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/templates/implement.plain +0 -0
  133. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/templates/implement_2.plain +0 -0
  134. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/templates/template_include.plain +0 -0
  135. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/data/templates/test_hardest_problem.plain +0 -0
  136. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/test_cli.py +0 -0
  137. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/test_exceptions.py +0 -0
  138. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/test_imports.py +0 -0
  139. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/test_plainfile.py +0 -0
  140. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/test_plainspec.py +0 -0
  141. {plain_parser-0.1.2.dev4 → plain_parser-0.1.2.dev5}/tests/test_requires.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: plain-parser
3
- Version: 0.1.2.dev4
3
+ Version: 0.1.2.dev5
4
4
  Summary: Parser for ***plain specification files
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -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 definitons:\n"
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
@@ -95,7 +95,20 @@ class TrackingFileSystemLoader(Plain2CodeLoaderMixin, FileSystemLoader):
95
95
  return source
96
96
 
97
97
 
98
- def get_loaded_templates(source_path, plain_source):
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):
99
112
  # Render the plain source with Liquid templating engine
100
113
  # to identify the templates that are being loaded
101
114
 
@@ -106,9 +119,11 @@ def get_loaded_templates(source_path, plain_source):
106
119
  liquid_env.filters["code_variable"] = plain_spec.code_variable_liquid_filter
107
120
  liquid_env.filters["prohibited_chars"] = plain_spec.prohibited_chars_liquid_filter
108
121
 
109
- plain_source_template = liquid_env.from_string(plain_source)
110
122
  try:
123
+ plain_source_template = liquid_env.from_string(plain_source, name=template_name or "")
111
124
  plain_source = plain_source_template.render()
125
+ except LiquidSyntaxError as e:
126
+ raise PlainSyntaxError(liquid_error_message(e))
112
127
  except UndefinedError as e:
113
128
  raise Exception(f"Undefined liquid variable: {str(e)}")
114
129
 
@@ -240,7 +240,7 @@ def _is_acceptance_test_heading(token) -> tuple[bool, str | None]:
240
240
  content = strong_children[0].content.strip()
241
241
  if content == plain_spec.ACCEPTANCE_TEST_HEADING:
242
242
  return True, None
243
- 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}`."
244
244
  return False, problem
245
245
 
246
246
 
@@ -508,6 +508,7 @@ def parse_plain_source( # noqa: C901
508
508
  template_dirs: list[str],
509
509
  imported_modules: list[str],
510
510
  modules_trace: list[str],
511
+ module_name: str | None = None,
511
512
  ) -> PlainFileParseResult:
512
513
  plain_source_text = normalize_line_endings(plain_source_text)
513
514
 
@@ -527,7 +528,7 @@ def parse_plain_source( # noqa: C901
527
528
  else:
528
529
  required_concepts = list[str]()
529
530
 
530
- [_, 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)
531
532
 
532
533
  plain_source_content = restore_stripped_lines(plain_source_text, plain_source_obj.content)
533
534
 
@@ -585,7 +586,7 @@ def parse_plain_source( # noqa: C901
585
586
 
586
587
  if specification_heading == plain_spec.DEFINITIONS and current_specification_heading is not None:
587
588
  raise PlainSyntaxError(
588
- f"Plain syntax error: Syntax error at line {token.line_number}: Definitions specification must be the first specification in the section (`{token_text}`)"
589
+ f"Plain syntax error: Syntax error at line {token.line_number}: Definitions specification must be the first specification in the section (`{token_text}`)"
589
590
  )
590
591
 
591
592
  current_specification_heading = specification_heading
@@ -602,7 +603,7 @@ def parse_plain_source( # noqa: C901
602
603
 
603
604
  else:
604
605
  raise PlainSyntaxError(
605
- f"Plain syntax error: Syntax error at line {token.line_number}: Invalid source structure (`{token_text}`)"
606
+ f"Plain syntax error: Syntax error at line {token.line_number}: Invalid source structure (`{token_text}`)"
606
607
  )
607
608
 
608
609
  # Acceptance tests are only allowed nested under functional specs. Reject them when
@@ -681,8 +682,8 @@ def read_module_plain_source(module_name: str, template_dirs: list[str]) -> str:
681
682
  if blob is not None:
682
683
  raise UnsupportedBase64Content(
683
684
  f"Module '{module_name}' contains a base64-encoded blob ({len(blob)} characters) "
684
- "inlined in the specification. This is not supported."
685
- "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, "
686
687
  "include the binary file path in the specification."
687
688
  )
688
689
 
@@ -704,6 +705,7 @@ def parse_plain_file(
704
705
  template_dirs,
705
706
  imported_modules,
706
707
  modules_trace + [module_name],
708
+ module_name=module_name,
707
709
  )
708
710
 
709
711
 
@@ -126,7 +126,7 @@ def collect_linked_resources(
126
126
  ):
127
127
 
128
128
  if not isinstance(plain_source_tree, dict):
129
- raise ValueError("[plain_source_tree must be a dictionary.")
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)
@@ -1,4 +1,3 @@
1
- # "hello, world" in Plain (Python version)
2
1
 
3
2
  ***definitions***
4
3
 
@@ -1,4 +1,3 @@
1
- # "hello, world" in Plain (Python version)
2
1
 
3
2
  ***definitions***
4
3
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- import
2
+ import:
3
3
  - required_concepts
4
4
  ---
5
5
 
@@ -0,0 +1,4 @@
1
+ ***functional specs***
2
+
3
+ {% if %}
4
+ - A :User: can add a :Task:.
@@ -0,0 +1,3 @@
1
+ ***functional specs***
2
+
3
+ - A :User: can add a {{ task_name
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import re
2
3
  import tempfile
3
4
 
4
5
  import pytest
@@ -137,3 +138,47 @@ def test_resolve_linked_resource_directory_in_first_dir_shadows_file_in_second()
137
138
  f.write("x")
138
139
 
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"
@@ -62,7 +62,7 @@ code block
62
62
  """
63
63
  with pytest.raises(
64
64
  Exception,
65
- match=re.escape("Syntax error at line 3: Invalid source structure (`code block`)"),
65
+ match=re.escape("Syntax error at line 3: Invalid source structure (`code block`)"),
66
66
  ):
67
67
  plain_file.parse_plain_source(plain_source, {}, [], [], [])
68
68
 
@@ -602,7 +602,7 @@ def test_concept_validation_definitions(get_test_data_path):
602
602
  [get_test_data_path("data/plainfileparser")],
603
603
  )
604
604
 
605
- with pytest.raises(PlainSyntaxError):
605
+ with pytest.raises(PlainSyntaxError, match=re.escape("Invalid definition specification text")):
606
606
  plain_file.plain_file_parser(
607
607
  "concept_validation_noconcepts.plain",
608
608
  [get_test_data_path("data/plainfileparser")],
@@ -635,7 +635,7 @@ def test_concept_validation_usage(get_test_data_path):
635
635
 
636
636
 
637
637
  def test_concept_validation_redefinition(get_test_data_path):
638
- with pytest.raises(PlainSyntaxError):
638
+ with pytest.raises(PlainSyntaxError, match=re.escape("Concepts were defined multiple times")):
639
639
  plain_file.plain_file_parser(
640
640
  "concept_validation_redefinition.plain",
641
641
  [get_test_data_path("data/plainfileparser")],
@@ -771,3 +771,11 @@ def test_topological_sort(get_test_data_path):
771
771
  {"markdown": "- :Concept4: is a concept that depends on the :Concept3: concept."},
772
772
  {"markdown": "- :Concept6: is a concept that depends on the :Concept1: and :Concept4: concepts."},
773
773
  ]
774
+
775
+
776
+ def test_liquid_syntax_error_names_module(get_test_data_path):
777
+ with pytest.raises(
778
+ PlainSyntaxError,
779
+ match=re.escape("Plain syntax error: Invalid Liquid at unterminated_output:4:0: "),
780
+ ):
781
+ plain_file.plain_file_parser("unterminated_output.plain", [get_test_data_path("data/templates")])