plain-parser 0.1.3.dev8__tar.gz → 0.1.4.dev9__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 (152) hide show
  1. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/PKG-INFO +1 -1
  2. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/src/plain_parser/plain_file.py +62 -28
  3. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/test_requires.py +104 -3
  4. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/.flake8 +0 -0
  5. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/.github/dependabot.yml +0 -0
  6. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/.github/workflows/lint-and-test.yml +0 -0
  7. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/.github/workflows/publish-dev-to-pypi.yml +0 -0
  8. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/.github/workflows/publish-to-pypi.yml +0 -0
  9. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/.gitignore +0 -0
  10. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/LICENSE +0 -0
  11. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/README.md +0 -0
  12. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/pyproject.toml +0 -0
  13. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/src/plain_parser/__init__.py +0 -0
  14. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/src/plain_parser/cli.py +0 -0
  15. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/src/plain_parser/concept_utils.py +0 -0
  16. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/src/plain_parser/exceptions.py +0 -0
  17. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/src/plain_parser/graph.py +0 -0
  18. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/src/plain_parser/liquid_nodes.py +0 -0
  19. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/src/plain_parser/loaders.py +0 -0
  20. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/src/plain_parser/plain_spec.py +0 -0
  21. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/__init__.py +0 -0
  22. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/conftest.py +0 -0
  23. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/cli/acceptance_test_binary_resource.plain +0 -0
  24. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/cli/acceptance_test_resource.plain +0 -0
  25. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/cli/base64_resource.plain +0 -0
  26. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/cli/binary.bin +0 -0
  27. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/cli/binary_resource.plain +0 -0
  28. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/cli/code_variable_conflict.plain +0 -0
  29. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/cli/code_variable_template.plain +0 -0
  30. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/cli/missing_resource.plain +0 -0
  31. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/cli/not_plain.md +0 -0
  32. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/cli/notes.md +0 -0
  33. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/cli/requires_binary_resource_base.plain +0 -0
  34. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/cli/requires_binary_resource_top.plain +0 -0
  35. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/cli/subdir/nested.plain +0 -0
  36. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/cli/undefined_concept.plain +0 -0
  37. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/cli/valid.plain +0 -0
  38. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/imports/circular_imports_1.plain +0 -0
  39. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/imports/circular_imports_2.plain +0 -0
  40. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/imports/circular_imports_main.plain +0 -0
  41. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/imports/diamond_import_1.plain +0 -0
  42. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/imports/diamond_import_2.plain +0 -0
  43. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/imports/diamond_import_common.plain +0 -0
  44. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/imports/diamond_imports_main.plain +0 -0
  45. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/imports/import_with_functionalities.plain +0 -0
  46. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/imports/imports_module_with_functionalities.plain +0 -0
  47. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/imports/non_existent_import.plain +0 -0
  48. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfile/add_new_task_modal_specification.yaml +0 -0
  49. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfile/duplicate_specification_heading.plain +0 -0
  50. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfile/empty_functional_specs_section.plain +0 -0
  51. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfile/invalid_specification_order.plain +0 -0
  52. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfile/missing_non_functional_requirements.plain +0 -0
  53. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfile/no_functional_specs_section.plain +0 -0
  54. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfile/plain_source_with_absolute_link.plain +0 -0
  55. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfile/plain_source_with_missing_link.plain +0 -0
  56. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfile/plain_source_with_url_link.plain +0 -0
  57. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfile/task_list_ui_specification.yaml +0 -0
  58. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfile/task_manager_with_reference_links.plain +0 -0
  59. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfile/without_non_functional_requirement.plain +0 -0
  60. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/concept_validation_acceptance_tests.plain +0 -0
  61. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/concept_validation_acceptance_tests_nondefined.plain +0 -0
  62. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/concept_validation_defined_nondefined.plain +0 -0
  63. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/concept_validation_defined_nondefined_2.plain +0 -0
  64. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/concept_validation_definition.plain +0 -0
  65. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/concept_validation_noconcepts.plain +0 -0
  66. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/concept_validation_nonconcept.plain +0 -0
  67. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/concept_validation_nondefined.plain +0 -0
  68. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/concept_validation_redefinition.plain +0 -0
  69. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/concept_validation_several_concepts.plain +0 -0
  70. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/concept_validation_valid.plain +0 -0
  71. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/cyclic_definitions.plain +0 -0
  72. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/cyclic_definitions_across_import.plain +0 -0
  73. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/cyclic_definitions_long.plain +0 -0
  74. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/cyclic_definitions_sub_bullet.plain +0 -0
  75. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts.plain +0 -0
  76. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_base.plain +0 -0
  77. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_defaults_exported.plain +0 -0
  78. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_example.plain +0 -0
  79. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_imported_exported.plain +0 -0
  80. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_inherited_reexported.plain +0 -0
  81. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_malformed.plain +0 -0
  82. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_missing_definition.plain +0 -0
  83. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_missing_definition_example.plain +0 -0
  84. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet.plain +0 -0
  85. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet_undefined.plain +0 -0
  86. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_nested.plain +0 -0
  87. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_nested_example.plain +0 -0
  88. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_no_definitions.plain +0 -0
  89. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_not_declared.plain +0 -0
  90. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_not_declared_example.plain +0 -0
  91. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_transitive_example.plain +0 -0
  92. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_transitive_l1.plain +0 -0
  93. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/exported_concepts_transitive_l2.plain +0 -0
  94. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/plain_file_parser_with_comments.plain +0 -0
  95. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/plain_file_with_comments_indented.plain +0 -0
  96. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/regular_plain_source.plain +0 -0
  97. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/required_concepts.plain +0 -0
  98. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/required_concepts_defs.plain +0 -0
  99. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/required_concepts_example.plain +0 -0
  100. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/required_concepts_l1.plain +0 -0
  101. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/required_concepts_l2.plain +0 -0
  102. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/required_concepts_missing.plain +0 -0
  103. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/required_concepts_module.plain +0 -0
  104. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/required_concepts_partial.plain +0 -0
  105. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/required_concepts_partial_duplicate.plain +0 -0
  106. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/requires_without_definitions_example.plain +0 -0
  107. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/topological_sort.plain +0 -0
  108. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/plainfileparser/topological_sort_not_referenced.plain +0 -0
  109. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/chain_base.plain +0 -0
  110. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/chain_fork_top.plain +0 -0
  111. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/chain_middle.plain +0 -0
  112. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/chain_top.plain +0 -0
  113. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/circular_requires_main.plain +0 -0
  114. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/circular_requires_sub.plain +0 -0
  115. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/diamond_requires_1.plain +0 -0
  116. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/diamond_requires_2.plain +0 -0
  117. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/diamond_requires_common.plain +0 -0
  118. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/diamond_requires_main.plain +0 -0
  119. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/independent_requires_1.plain +0 -0
  120. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/independent_requires_2.plain +0 -0
  121. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/independent_requires_main.plain +0 -0
  122. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/invalid_ancestor_base.plain +0 -0
  123. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/invalid_ancestor_top.plain +0 -0
  124. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/non_existent_require.plain +0 -0
  125. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/normal_requires_1.plain +0 -0
  126. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/normal_requires_2.plain +0 -0
  127. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/normal_requires_common.plain +0 -0
  128. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/normal_requires_main.plain +0 -0
  129. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/shared_export_above.plain +0 -0
  130. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/shared_export_base.plain +0 -0
  131. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/shared_export_middle.plain +0 -0
  132. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/requires/shared_export_top.plain +0 -0
  133. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/sample_base64_image.txt +0 -0
  134. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/simple.plain +0 -0
  135. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/templates/block_level_include.plain +0 -0
  136. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/templates/code_variables.plain +0 -0
  137. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/templates/header.plain +0 -0
  138. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/templates/implement.plain +0 -0
  139. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/templates/implement_2.plain +0 -0
  140. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/templates/malformed_tag.plain +0 -0
  141. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/templates/template_include.plain +0 -0
  142. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/templates/test_hardest_problem.plain +0 -0
  143. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/data/templates/unterminated_output.plain +0 -0
  144. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/test_cli.py +0 -0
  145. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/test_concept_utils.py +0 -0
  146. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/test_exceptions.py +0 -0
  147. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/test_graph.py +0 -0
  148. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/test_imports.py +0 -0
  149. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/test_loaders.py +0 -0
  150. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/test_plainfile.py +0 -0
  151. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/test_plainfileparser.py +0 -0
  152. {plain_parser-0.1.3.dev8 → plain_parser-0.1.4.dev9}/tests/test_plainspec.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: plain-parser
3
- Version: 0.1.3.dev8
3
+ Version: 0.1.4.dev9
4
4
  Summary: Parser for ***plain specification files
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -709,19 +709,31 @@ def parse_plain_file(
709
709
  )
710
710
 
711
711
 
712
+ @dataclass
713
+ class RequiredModule:
714
+ """A required module as parsed by the requires walk; validated once the top module is complete."""
715
+
716
+ parse_result: PlainFileParseResult
717
+ # Definitions exported to this module by the modules it requires.
718
+ ancestor_exported_definitions: list[mistletoe.block_token.token]
719
+ code_variables: dict
720
+ # Definitions this module exports to the modules that require it.
721
+ exported_definitions: list[mistletoe.block_token.token]
722
+ # Marshalled plain source tree, set once the module is validated.
723
+ tree: dict | None = None
724
+
725
+
712
726
  def process_required_modules(
713
727
  required_modules: list[str],
714
728
  template_dirs: list[str],
715
729
  all_required_modules: list[str],
716
730
  modules_trace: list[str],
717
- chain: list[tuple[str, dict]],
718
- module_exports: dict[str, list[mistletoe.block_token.token]],
731
+ required: dict[str, RequiredModule],
719
732
  ) -> list[mistletoe.block_token.token]:
720
- """Parse and fully validate every required module, deepest first.
733
+ """Parse every required module, deepest first, without validating it.
721
734
 
722
- Appends ``(module name, marshalled plain source tree)`` to ``chain`` for each module not already in it,
723
- records each module's exported definitions in ``module_exports``, and returns the exported definitions
724
- of the directly required modules.
735
+ Records each module in ``required`` the first time it is reached (deepest ancestors first) and returns
736
+ the exported definitions of the directly required modules.
725
737
  """
726
738
  exported_definitions = list[mistletoe.block_token.token]()
727
739
  for module_name in required_modules:
@@ -730,7 +742,7 @@ def process_required_modules(
730
742
 
731
743
  if len(all_required_modules) > 0 and module_name == all_required_modules[-1]:
732
744
  # Already processed as an earlier entry; its exports are still visible to this module.
733
- exported_definitions.extend(module_exports[module_name])
745
+ exported_definitions.extend(required[module_name].exported_definitions)
734
746
  continue
735
747
 
736
748
  code_variables: dict = {}
@@ -753,8 +765,7 @@ def process_required_modules(
753
765
  template_dirs,
754
766
  all_required_modules,
755
767
  modules_trace + [module_name],
756
- chain,
757
- module_exports,
768
+ required,
758
769
  )
759
770
 
760
771
  own_exported_definitions = list[mistletoe.block_token.token]()
@@ -770,20 +781,32 @@ def process_required_modules(
770
781
  exported_concept, plain_file_parse_result.plain_source, renderer
771
782
  )
772
783
  )
773
- module_exports[module_name] = own_exported_definitions
774
- exported_definitions.extend(own_exported_definitions)
775
-
776
- marshalled_plain_source = validate_and_marshall_module(
777
- plain_file_parse_result, module_name, ancestor_exported_definitions, code_variables, template_dirs
784
+ required[module_name] = RequiredModule(
785
+ plain_file_parse_result, ancestor_exported_definitions, code_variables, own_exported_definitions
778
786
  )
779
- if module_name not in (chain_module_name for chain_module_name, _ in chain):
780
- chain.append((module_name, marshalled_plain_source))
787
+ exported_definitions.extend(own_exported_definitions)
781
788
 
782
789
  all_required_modules.append(module_name)
783
790
 
784
791
  return exported_definitions
785
792
 
786
793
 
794
+ def validate_required_modules(
795
+ required_modules: list[str], required: dict[str, RequiredModule], template_dirs: list[str]
796
+ ) -> None:
797
+ """Validate recorded required modules top-down, in listed order, each module once."""
798
+ for module_name in required_modules:
799
+ module = required[module_name]
800
+ if module.tree is not None:
801
+ continue
802
+
803
+ validate_module_structure(module.parse_result, module_name)
804
+ module.tree = validate_and_build_module_tree(
805
+ module.parse_result, module_name, module.ancestor_exported_definitions, module.code_variables, template_dirs
806
+ )
807
+ validate_required_modules(module.parse_result.required_modules, required, template_dirs)
808
+
809
+
787
810
  def process_exported_definitions(plain_source: dict, exported_definitions: list[mistletoe.block_token.token]) -> None:
788
811
  if len(exported_definitions) == 0:
789
812
  return
@@ -806,14 +829,8 @@ def process_exported_definitions(plain_source: dict, exported_definitions: list[
806
829
  plain_source[plain_spec.DEFINITIONS].children.append(exported_definition)
807
830
 
808
831
 
809
- def validate_and_marshall_module(
810
- plain_file_parse_result: PlainFileParseResult,
811
- module_name: str,
812
- exported_definitions: list[mistletoe.block_token.token],
813
- code_variables: dict,
814
- template_dirs: list[str],
815
- ) -> dict:
816
- """Every check a module must pass after parsing, and its marshalled plain source tree."""
832
+ def validate_module_structure(plain_file_parse_result: PlainFileParseResult, module_name: str) -> None:
833
+ """Required concepts defined, functional specs present and non-empty, implementation reqs present."""
817
834
  if len(plain_file_parse_result.required_concepts) > 0:
818
835
  missing_required_concepts_msg = "Missing required concepts: "
819
836
  missing_required_concepts_msg += ", ".join(plain_file_parse_result.required_concepts)
@@ -841,6 +858,16 @@ def validate_and_marshall_module(
841
858
  has_requires=bool(plain_file_parse_result.required_modules),
842
859
  )
843
860
 
861
+
862
+ def validate_and_build_module_tree(
863
+ plain_file_parse_result: PlainFileParseResult,
864
+ module_name: str,
865
+ exported_definitions: list[mistletoe.block_token.token],
866
+ code_variables: dict,
867
+ template_dirs: list[str],
868
+ ) -> dict:
869
+ """Merge exported definitions, validate acceptance tests, linked resources and concepts, and return the
870
+ marshalled plain source tree."""
844
871
  process_exported_definitions(plain_file_parse_result.plain_source, exported_definitions)
845
872
 
846
873
  process_acceptance_tests(plain_file_parse_result.plain_source)
@@ -890,20 +917,27 @@ def _parse_module(
890
917
  modules_trace=[],
891
918
  )
892
919
 
893
- chain: list[tuple[str, dict]] = []
920
+ # Check the top module's structure, walk its requires, finish the top module's checks, then validate
921
+ # every required module top-down.
922
+ validate_module_structure(plain_file_parse_result, module_name)
923
+
924
+ required: dict[str, RequiredModule] = {}
894
925
  exported_definitions = process_required_modules(
895
926
  plain_file_parse_result.required_modules,
896
927
  template_dirs=template_dirs,
897
928
  all_required_modules=[],
898
929
  modules_trace=[],
899
- chain=chain,
900
- module_exports={},
930
+ required=required,
901
931
  )
902
932
 
903
- marshalled_plain_source = validate_and_marshall_module(
933
+ marshalled_plain_source = validate_and_build_module_tree(
904
934
  plain_file_parse_result, module_name, exported_definitions, code_variables, template_dirs
905
935
  )
906
936
 
937
+ validate_required_modules(plain_file_parse_result.required_modules, required, template_dirs)
938
+ # The walk records modules deepest first, which is the chain order.
939
+ chain = [(chain_module_name, module.tree) for chain_module_name, module in required.items()]
940
+
907
941
  return module_name, marshalled_plain_source, plain_file_parse_result.required_modules, chain
908
942
 
909
943
 
@@ -1,7 +1,7 @@
1
1
  import pytest
2
2
 
3
3
  from plain_parser import plain_file
4
- from plain_parser.exceptions import ModuleDoesNotExistError, PlainSyntaxError
4
+ from plain_parser.exceptions import MissingFunctionalitiesError, ModuleDoesNotExistError, PlainSyntaxError
5
5
 
6
6
 
7
7
  def test_non_existent_require(get_test_data_path):
@@ -61,6 +61,7 @@ def test_plain_file_parser_validates_required_modules(get_test_data_path):
61
61
 
62
62
 
63
63
  def write_module(directory, module_name, exported_concepts, required_modules, definitions, functional_spec):
64
+ # functional_spec=None writes a module without a ***functional specs*** section.
64
65
  exports = "".join(f" - {concept}\n" for concept in exported_concepts)
65
66
  requires = "".join(f" - {required_module}\n" for required_module in required_modules)
66
67
  (directory / f"{module_name}.plain").write_text(
@@ -70,8 +71,8 @@ def write_module(directory, module_name, exported_concepts, required_modules, de
70
71
  + "---\n\n"
71
72
  "***definitions***\n\n"
72
73
  + "".join(f"- {definition}\n\n" for definition in definitions)
73
- + "***implementation reqs***\n\n- An implementation requirement.\n\n"
74
- f"***functional specs***\n\n- {functional_spec}\n"
74
+ + "***implementation reqs***\n\n- An implementation requirement.\n"
75
+ + (f"\n***functional specs***\n\n- {functional_spec}\n" if functional_spec is not None else "")
75
76
  )
76
77
 
77
78
 
@@ -154,6 +155,14 @@ ACCEPTED_CHAINS = [
154
155
  marks=pytest.mark.xfail(strict=True, reason="an ancestor must be listed before its dependent"),
155
156
  ),
156
157
  # Top
158
+ # || Top lists Base twice
159
+ # Base
160
+ pytest.param(
161
+ {"Top": ["Base", "Base"], "Base": []},
162
+ ["Base", "Top"],
163
+ id="top-lists-base-twice",
164
+ ),
165
+ # Top
157
166
  # |
158
167
  # Upper every module also requires every module below it
159
168
  # |
@@ -267,3 +276,95 @@ def test_requires_independent_branches_are_rejected(tmp_path, requires_by_module
267
276
 
268
277
  with pytest.raises(PlainSyntaxError, match="There must be a fixed order how required modules are dependent"):
269
278
  plain_file.parse_module_chain("Top.plain", [str(tmp_path)])
279
+
280
+
281
+ # Two broken modules in one chain: each test pins which error is reported first.
282
+ def test_top_module_shape_is_checked_before_its_requires_are_read(tmp_path):
283
+ write_module(tmp_path, "Top", [], ["Missing"], [], None)
284
+
285
+ with pytest.raises(MissingFunctionalitiesError):
286
+ plain_file.plain_file_parser("Top.plain", [str(tmp_path)])
287
+
288
+
289
+ def test_top_module_shape_is_checked_before_required_modules_are_validated(tmp_path):
290
+ write_module(tmp_path, "Base", [], [], [], "Base uses :BaseMissing:.")
291
+ write_module(tmp_path, "Top", [], ["Base"], [], None)
292
+
293
+ with pytest.raises(MissingFunctionalitiesError):
294
+ plain_file.plain_file_parser("Top.plain", [str(tmp_path)])
295
+
296
+
297
+ def test_parse_errors_of_required_modules_come_before_top_module_concept_errors(tmp_path):
298
+ (tmp_path / "Base.plain").write_text(
299
+ "***implementation reqs***\n\n- {% if %}\n\n***functional specs***\n\n- Base works.\n"
300
+ )
301
+ write_module(tmp_path, "Top", [], ["Base"], [], "Top uses :TopMissing:.")
302
+
303
+ with pytest.raises(PlainSyntaxError, match="Invalid Liquid"):
304
+ plain_file.plain_file_parser("Top.plain", [str(tmp_path)])
305
+
306
+
307
+ def test_top_module_concept_errors_come_before_required_module_concept_errors(tmp_path):
308
+ write_module(tmp_path, "Base", [], [], [], "Base uses :BaseMissing:.")
309
+ write_module(tmp_path, "Top", [], ["Base"], [], "Top uses :TopMissing:.")
310
+
311
+ with pytest.raises(PlainSyntaxError, match="Concept :TopMissing: is not defined"):
312
+ plain_file.plain_file_parser("Top.plain", [str(tmp_path)])
313
+
314
+
315
+ def test_required_modules_are_validated_top_down(tmp_path):
316
+ write_module(tmp_path, "Base", [], [], [], "Base uses :BaseMissing:.")
317
+ write_module(tmp_path, "Middle", [], ["Base"], [], "Middle uses :MiddleMissing:.")
318
+ write_module(tmp_path, "Top", [], ["Middle"], [], "Top works.")
319
+
320
+ with pytest.raises(PlainSyntaxError, match="Concept :MiddleMissing: is not defined"):
321
+ plain_file.plain_file_parser("Top.plain", [str(tmp_path)])
322
+
323
+
324
+ def test_module_listed_twice_around_its_dependent_is_rejected(tmp_path):
325
+ write_modules_using_required_exports(tmp_path, {"Top": ["Base", "Middle", "Base"], "Middle": ["Base"], "Base": []})
326
+
327
+ with pytest.raises(PlainSyntaxError, match="There must be a fixed order how required modules are dependent"):
328
+ plain_file.parse_module_chain("Top.plain", [str(tmp_path)])
329
+
330
+
331
+ def test_chain_has_a_tree_for_every_module_reached_by_the_walk(tmp_path):
332
+ write_modules_using_required_exports(tmp_path, {"Top": ["Base", "Middle"], "Middle": ["Base"], "Base": []})
333
+
334
+ chain = plain_file.parse_module_chain("Top.plain", [str(tmp_path)])
335
+
336
+ assert [module_name for module_name, _ in chain] == ["Base", "Middle", "Top"]
337
+ assert all("functional specs" in tree for _, tree in chain)
338
+
339
+
340
+ def test_required_module_without_functional_specs_is_rejected(tmp_path):
341
+ write_module(tmp_path, "Base", [], [], [], None)
342
+ write_module(tmp_path, "Top", [], ["Base"], [], "Top works.")
343
+
344
+ with pytest.raises(MissingFunctionalitiesError, match="Module 'Base'"):
345
+ plain_file.plain_file_parser("Top.plain", [str(tmp_path)])
346
+
347
+
348
+ def test_top_module_concept_errors_come_before_required_module_structure_errors(tmp_path):
349
+ write_module(tmp_path, "Base", [], [], [], None)
350
+ write_module(tmp_path, "Top", [], ["Base"], [], "Top uses :TopMissing:.")
351
+
352
+ with pytest.raises(PlainSyntaxError, match="Concept :TopMissing: is not defined"):
353
+ plain_file.plain_file_parser("Top.plain", [str(tmp_path)])
354
+
355
+
356
+ def test_each_required_module_is_validated_once_top_down(tmp_path, monkeypatch):
357
+ write_modules_using_required_exports(tmp_path, {"Top": ["Base", "Middle"], "Middle": ["Base"], "Base": []})
358
+ # A second validation pass over a module would be idempotent, so only the call count can observe it.
359
+ built = []
360
+ build_tree = plain_file.validate_and_build_module_tree
361
+
362
+ def record_and_build(parse_result, module_name, *args):
363
+ built.append(module_name)
364
+ return build_tree(parse_result, module_name, *args)
365
+
366
+ monkeypatch.setattr(plain_file, "validate_and_build_module_tree", record_and_build)
367
+
368
+ plain_file.parse_module_chain("Top.plain", [str(tmp_path)])
369
+
370
+ assert built == ["Top", "Base", "Middle"]