plain-parser 0.1.4.dev9__tar.gz → 0.1.4.dev10__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 (166) hide show
  1. plain_parser-0.1.4.dev10/PKG-INFO +125 -0
  2. plain_parser-0.1.4.dev10/README.md +102 -0
  3. plain_parser-0.1.4.dev10/src/plain_parser/cli.py +233 -0
  4. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/loaders.py +26 -5
  5. plain_parser-0.1.4.dev10/tests/data/cli/parent_link/a/spec/top.plain +7 -0
  6. plain_parser-0.1.4.dev10/tests/data/cli/parent_link/b/shared/notes.md +1 -0
  7. plain_parser-0.1.4.dev10/tests/data/cli/parent_link/b/tpl/.gitkeep +0 -0
  8. plain_parser-0.1.4.dev10/tests/data/cli/resource_before_frid_walk/base.plain +8 -0
  9. plain_parser-0.1.4.dev10/tests/data/cli/resource_before_frid_walk/snippet.plain +1 -0
  10. plain_parser-0.1.4.dev10/tests/data/cli/resource_before_frid_walk/top.plain +14 -0
  11. plain_parser-0.1.4.dev10/tests/data/cli/shared_link_helper.plain +7 -0
  12. plain_parser-0.1.4.dev10/tests/data/cli/shared_link_top.plain +12 -0
  13. plain_parser-0.1.4.dev10/tests/data/cli/template_link/spec/top.plain +7 -0
  14. plain_parser-0.1.4.dev10/tests/data/cli/template_link/tpl/notes.md +1 -0
  15. plain_parser-0.1.4.dev10/tests/test_cli.py +695 -0
  16. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_loaders.py +32 -1
  17. plain_parser-0.1.4.dev9/PKG-INFO +0 -76
  18. plain_parser-0.1.4.dev9/README.md +0 -53
  19. plain_parser-0.1.4.dev9/src/plain_parser/cli.py +0 -117
  20. plain_parser-0.1.4.dev9/tests/test_cli.py +0 -329
  21. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/.flake8 +0 -0
  22. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/.github/dependabot.yml +0 -0
  23. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/.github/workflows/lint-and-test.yml +0 -0
  24. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/.github/workflows/publish-dev-to-pypi.yml +0 -0
  25. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/.github/workflows/publish-to-pypi.yml +0 -0
  26. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/.gitignore +0 -0
  27. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/LICENSE +0 -0
  28. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/pyproject.toml +0 -0
  29. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/__init__.py +0 -0
  30. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/concept_utils.py +0 -0
  31. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/exceptions.py +0 -0
  32. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/graph.py +0 -0
  33. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/liquid_nodes.py +0 -0
  34. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/plain_file.py +0 -0
  35. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/plain_spec.py +0 -0
  36. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/__init__.py +0 -0
  37. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/conftest.py +0 -0
  38. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/acceptance_test_binary_resource.plain +0 -0
  39. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/acceptance_test_resource.plain +0 -0
  40. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/base64_resource.plain +0 -0
  41. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/binary.bin +0 -0
  42. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/binary_resource.plain +0 -0
  43. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/code_variable_conflict.plain +0 -0
  44. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/code_variable_template.plain +0 -0
  45. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/missing_resource.plain +0 -0
  46. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/not_plain.md +0 -0
  47. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/notes.md +0 -0
  48. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/requires_binary_resource_base.plain +0 -0
  49. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/requires_binary_resource_top.plain +0 -0
  50. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/subdir/nested.plain +0 -0
  51. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/undefined_concept.plain +0 -0
  52. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/valid.plain +0 -0
  53. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/circular_imports_1.plain +0 -0
  54. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/circular_imports_2.plain +0 -0
  55. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/circular_imports_main.plain +0 -0
  56. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/diamond_import_1.plain +0 -0
  57. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/diamond_import_2.plain +0 -0
  58. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/diamond_import_common.plain +0 -0
  59. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/diamond_imports_main.plain +0 -0
  60. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/import_with_functionalities.plain +0 -0
  61. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/imports_module_with_functionalities.plain +0 -0
  62. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/non_existent_import.plain +0 -0
  63. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/add_new_task_modal_specification.yaml +0 -0
  64. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/duplicate_specification_heading.plain +0 -0
  65. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/empty_functional_specs_section.plain +0 -0
  66. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/invalid_specification_order.plain +0 -0
  67. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/missing_non_functional_requirements.plain +0 -0
  68. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/no_functional_specs_section.plain +0 -0
  69. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/plain_source_with_absolute_link.plain +0 -0
  70. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/plain_source_with_missing_link.plain +0 -0
  71. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/plain_source_with_url_link.plain +0 -0
  72. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/task_list_ui_specification.yaml +0 -0
  73. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/task_manager_with_reference_links.plain +0 -0
  74. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/without_non_functional_requirement.plain +0 -0
  75. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_acceptance_tests.plain +0 -0
  76. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_acceptance_tests_nondefined.plain +0 -0
  77. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_defined_nondefined.plain +0 -0
  78. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_defined_nondefined_2.plain +0 -0
  79. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_definition.plain +0 -0
  80. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_noconcepts.plain +0 -0
  81. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_nonconcept.plain +0 -0
  82. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_nondefined.plain +0 -0
  83. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_redefinition.plain +0 -0
  84. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_several_concepts.plain +0 -0
  85. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_valid.plain +0 -0
  86. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/cyclic_definitions.plain +0 -0
  87. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/cyclic_definitions_across_import.plain +0 -0
  88. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/cyclic_definitions_long.plain +0 -0
  89. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/cyclic_definitions_sub_bullet.plain +0 -0
  90. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts.plain +0 -0
  91. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_base.plain +0 -0
  92. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_defaults_exported.plain +0 -0
  93. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_example.plain +0 -0
  94. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_imported_exported.plain +0 -0
  95. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_inherited_reexported.plain +0 -0
  96. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_malformed.plain +0 -0
  97. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_missing_definition.plain +0 -0
  98. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_missing_definition_example.plain +0 -0
  99. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet.plain +0 -0
  100. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet_undefined.plain +0 -0
  101. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_nested.plain +0 -0
  102. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_nested_example.plain +0 -0
  103. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_no_definitions.plain +0 -0
  104. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_not_declared.plain +0 -0
  105. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_not_declared_example.plain +0 -0
  106. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_transitive_example.plain +0 -0
  107. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_transitive_l1.plain +0 -0
  108. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_transitive_l2.plain +0 -0
  109. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/plain_file_parser_with_comments.plain +0 -0
  110. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/plain_file_with_comments_indented.plain +0 -0
  111. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/regular_plain_source.plain +0 -0
  112. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts.plain +0 -0
  113. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_defs.plain +0 -0
  114. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_example.plain +0 -0
  115. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_l1.plain +0 -0
  116. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_l2.plain +0 -0
  117. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_missing.plain +0 -0
  118. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_module.plain +0 -0
  119. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_partial.plain +0 -0
  120. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_partial_duplicate.plain +0 -0
  121. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/requires_without_definitions_example.plain +0 -0
  122. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/topological_sort.plain +0 -0
  123. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/topological_sort_not_referenced.plain +0 -0
  124. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/chain_base.plain +0 -0
  125. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/chain_fork_top.plain +0 -0
  126. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/chain_middle.plain +0 -0
  127. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/chain_top.plain +0 -0
  128. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/circular_requires_main.plain +0 -0
  129. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/circular_requires_sub.plain +0 -0
  130. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/diamond_requires_1.plain +0 -0
  131. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/diamond_requires_2.plain +0 -0
  132. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/diamond_requires_common.plain +0 -0
  133. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/diamond_requires_main.plain +0 -0
  134. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/independent_requires_1.plain +0 -0
  135. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/independent_requires_2.plain +0 -0
  136. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/independent_requires_main.plain +0 -0
  137. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/invalid_ancestor_base.plain +0 -0
  138. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/invalid_ancestor_top.plain +0 -0
  139. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/non_existent_require.plain +0 -0
  140. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/normal_requires_1.plain +0 -0
  141. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/normal_requires_2.plain +0 -0
  142. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/normal_requires_common.plain +0 -0
  143. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/normal_requires_main.plain +0 -0
  144. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/shared_export_above.plain +0 -0
  145. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/shared_export_base.plain +0 -0
  146. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/shared_export_middle.plain +0 -0
  147. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/shared_export_top.plain +0 -0
  148. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/sample_base64_image.txt +0 -0
  149. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/simple.plain +0 -0
  150. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/block_level_include.plain +0 -0
  151. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/code_variables.plain +0 -0
  152. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/header.plain +0 -0
  153. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/implement.plain +0 -0
  154. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/implement_2.plain +0 -0
  155. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/malformed_tag.plain +0 -0
  156. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/template_include.plain +0 -0
  157. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/test_hardest_problem.plain +0 -0
  158. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/unterminated_output.plain +0 -0
  159. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_concept_utils.py +0 -0
  160. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_exceptions.py +0 -0
  161. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_graph.py +0 -0
  162. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_imports.py +0 -0
  163. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_plainfile.py +0 -0
  164. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_plainfileparser.py +0 -0
  165. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_plainspec.py +0 -0
  166. {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_requires.py +0 -0
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.5
2
+ Name: plain-parser
3
+ Version: 0.1.4.dev10
4
+ Summary: Parser for ***plain specification files
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Software Development :: Compilers
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: mistletoe<2,>=1.6.0
13
+ Requires-Dist: python-frontmatter<2,>=1.3.0
14
+ Requires-Dist: python-liquid2<0.5,>=0.4.0
15
+ Requires-Dist: pyyaml<7,>=6
16
+ Provides-Extra: dev
17
+ Requires-Dist: black==26.5.1; extra == 'dev'
18
+ Requires-Dist: flake8==7.3.0; extra == 'dev'
19
+ Requires-Dist: isort==8.0.1; extra == 'dev'
20
+ Requires-Dist: mypy==2.3.0; extra == 'dev'
21
+ Requires-Dist: pytest==9.1.1; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # plain-parser
25
+
26
+ Python parser for [`***plain`](https://www.plainlang.org/docs/) specification files.
27
+
28
+ Reads a `.plain` module and its `import` / `requires` chain, resolves Liquid templates,
29
+ validates concepts and linked resources, and returns the marshalled specification tree
30
+ plus the ordered list of functionalities (FRIDs).
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install plain-parser
36
+ ```
37
+
38
+ ## Command line
39
+
40
+ ```
41
+ plain-parser check <file.plain> [--template-dir DIR] [--config-name NAME]
42
+ plain-parser parse <file.plain> [--template-dir DIR] [--config-name NAME]
43
+ ```
44
+
45
+ Both commands parse the module and its `requires` chain, check that every linked resource is an existing
46
+ file (not a directory), and walk the module's functionalities. They stop at the first error.
47
+
48
+ ### `check`
49
+
50
+ Validates the module. In addition to the steps above it reads every linked file and rejects binary files and
51
+ files holding a large base64 blob.
52
+
53
+ ```
54
+ $ plain-parser check my_spec.plain
55
+ my_spec.plain: OK
56
+ ```
57
+
58
+ Exit `0` when the module is valid. On an error, `Error: …` is written to stderr and the exit code is `1`.
59
+
60
+ ### `parse`
61
+
62
+ Prints the parse result as one UTF-8 JSON document on stdout. Linked files are located but not read.
63
+
64
+ ```
65
+ $ plain-parser parse my_spec.plain
66
+ {
67
+ "format_version": 1,
68
+ "modules": [{"module": "...", "tree": {...}}, ...],
69
+ "resources": {...}
70
+ }
71
+ ```
72
+
73
+ - `format_version`: `1`.
74
+ - `modules`: every module in the `requires` chain, in build order: deepest ancestor first, the module itself last.
75
+ Each entry has the module name and its specification tree, in the shape described under *Python* below.
76
+ - `resources`: every linked-resource target in the chain, as written in the spec, mapped to the path it resolves
77
+ to, relative to the entry module's directory, with `/` as separator: `notes.md` when found next to the module,
78
+ `../tpl/notes.md` when found in a template directory. The directory part is lexical (symlinks not resolved) and
79
+ the target is appended as written (`..` kept), so joining the value onto the entry module's directory names the
80
+ file the parser matched. When no relative path exists (another drive on Windows) the value is the absolute path,
81
+ still with `/`. Two spellings of one file (`notes.md`, `./notes.md`) are two keys.
82
+
83
+ Exit `0` on success. On a parser error the JSON is `{"format_version": 1, "type": "<error class>",
84
+ "message": "…"}` and the exit code is `1`.
85
+
86
+ ### Options and exit codes
87
+
88
+ Modules and `{% include %}` templates are looked up in the `.plain` file's directory, then in `--template-dir`.
89
+ When `--template-dir` is not given, it is read from the `template-dir` key of `config.yaml` (or the file named by
90
+ `--config-name`), found next to the `.plain` file or in the working directory. Finding it in both is a usage
91
+ error. The config file is read even when `--template-dir` is given, so a malformed one fails the run either way.
92
+ Other config keys are ignored.
93
+
94
+ | Exit code | Meaning |
95
+ |---|---|
96
+ | `0` | Success |
97
+ | `1` | The module, its chain, or a linked resource is invalid |
98
+ | `2` | Usage error, including an ambiguous or malformed config file |
99
+ | `130` | Interrupted |
100
+
101
+ ## Python
102
+
103
+ ```python
104
+ from plain_parser import plain_file_parser
105
+
106
+ module_name, plain_source_tree, required_modules = plain_file_parser("my_spec.plain", ["."])
107
+ ```
108
+
109
+ `plain_source_tree` is a dict keyed by section (`definitions`, `implementation reqs`,
110
+ `test reqs`, `functional specs`), each a list of `{"markdown": ..., "linked_resources": [...]}`
111
+ entries. `plain_parser.plain_spec` has helpers for walking functionalities
112
+ (`get_frids`, `get_specifications_for_frid`, `collect_linked_resources`).
113
+
114
+ ## Development
115
+
116
+ ```bash
117
+ uv venv --python 3.11 && source .venv/bin/activate
118
+ uv pip install -e ".[dev]"
119
+ pytest
120
+ black . --check && isort . --check-only && flake8 . && mypy src
121
+ ```
122
+
123
+ ## License
124
+
125
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,102 @@
1
+ # plain-parser
2
+
3
+ Python parser for [`***plain`](https://www.plainlang.org/docs/) specification files.
4
+
5
+ Reads a `.plain` module and its `import` / `requires` chain, resolves Liquid templates,
6
+ validates concepts and linked resources, and returns the marshalled specification tree
7
+ plus the ordered list of functionalities (FRIDs).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install plain-parser
13
+ ```
14
+
15
+ ## Command line
16
+
17
+ ```
18
+ plain-parser check <file.plain> [--template-dir DIR] [--config-name NAME]
19
+ plain-parser parse <file.plain> [--template-dir DIR] [--config-name NAME]
20
+ ```
21
+
22
+ Both commands parse the module and its `requires` chain, check that every linked resource is an existing
23
+ file (not a directory), and walk the module's functionalities. They stop at the first error.
24
+
25
+ ### `check`
26
+
27
+ Validates the module. In addition to the steps above it reads every linked file and rejects binary files and
28
+ files holding a large base64 blob.
29
+
30
+ ```
31
+ $ plain-parser check my_spec.plain
32
+ my_spec.plain: OK
33
+ ```
34
+
35
+ Exit `0` when the module is valid. On an error, `Error: …` is written to stderr and the exit code is `1`.
36
+
37
+ ### `parse`
38
+
39
+ Prints the parse result as one UTF-8 JSON document on stdout. Linked files are located but not read.
40
+
41
+ ```
42
+ $ plain-parser parse my_spec.plain
43
+ {
44
+ "format_version": 1,
45
+ "modules": [{"module": "...", "tree": {...}}, ...],
46
+ "resources": {...}
47
+ }
48
+ ```
49
+
50
+ - `format_version`: `1`.
51
+ - `modules`: every module in the `requires` chain, in build order: deepest ancestor first, the module itself last.
52
+ Each entry has the module name and its specification tree, in the shape described under *Python* below.
53
+ - `resources`: every linked-resource target in the chain, as written in the spec, mapped to the path it resolves
54
+ to, relative to the entry module's directory, with `/` as separator: `notes.md` when found next to the module,
55
+ `../tpl/notes.md` when found in a template directory. The directory part is lexical (symlinks not resolved) and
56
+ the target is appended as written (`..` kept), so joining the value onto the entry module's directory names the
57
+ file the parser matched. When no relative path exists (another drive on Windows) the value is the absolute path,
58
+ still with `/`. Two spellings of one file (`notes.md`, `./notes.md`) are two keys.
59
+
60
+ Exit `0` on success. On a parser error the JSON is `{"format_version": 1, "type": "<error class>",
61
+ "message": "…"}` and the exit code is `1`.
62
+
63
+ ### Options and exit codes
64
+
65
+ Modules and `{% include %}` templates are looked up in the `.plain` file's directory, then in `--template-dir`.
66
+ When `--template-dir` is not given, it is read from the `template-dir` key of `config.yaml` (or the file named by
67
+ `--config-name`), found next to the `.plain` file or in the working directory. Finding it in both is a usage
68
+ error. The config file is read even when `--template-dir` is given, so a malformed one fails the run either way.
69
+ Other config keys are ignored.
70
+
71
+ | Exit code | Meaning |
72
+ |---|---|
73
+ | `0` | Success |
74
+ | `1` | The module, its chain, or a linked resource is invalid |
75
+ | `2` | Usage error, including an ambiguous or malformed config file |
76
+ | `130` | Interrupted |
77
+
78
+ ## Python
79
+
80
+ ```python
81
+ from plain_parser import plain_file_parser
82
+
83
+ module_name, plain_source_tree, required_modules = plain_file_parser("my_spec.plain", ["."])
84
+ ```
85
+
86
+ `plain_source_tree` is a dict keyed by section (`definitions`, `implementation reqs`,
87
+ `test reqs`, `functional specs`), each a list of `{"markdown": ..., "linked_resources": [...]}`
88
+ entries. `plain_parser.plain_spec` has helpers for walking functionalities
89
+ (`get_frids`, `get_specifications_for_frid`, `collect_linked_resources`).
90
+
91
+ ## Development
92
+
93
+ ```bash
94
+ uv venv --python 3.11 && source .venv/bin/activate
95
+ uv pip install -e ".[dev]"
96
+ pytest
97
+ black . --check && isort . --check-only && flake8 . && mypy src
98
+ ```
99
+
100
+ ## License
101
+
102
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,233 @@
1
+ """Command-line entry point: ``plain-parser {check,parse} <file.plain> [--template-dir DIR] [--config-name NAME]``."""
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ from dataclasses import dataclass
8
+
9
+ import yaml
10
+
11
+ from plain_parser import loaders, plain_file, plain_spec
12
+
13
+ DEFAULT_CONFIG_NAME = "config.yaml"
14
+ TEMPLATE_DIR_CONFIG_KEYS = ("template_dir", "template-dir")
15
+ PARSE_FORMAT_VERSION = 1
16
+
17
+
18
+ class AmbiguousConfigFileError(Exception):
19
+ pass
20
+
21
+
22
+ def build_parser() -> argparse.ArgumentParser:
23
+ parser = argparse.ArgumentParser(prog="plain-parser", description="Tools for ***plain specification files.")
24
+ subparsers = parser.add_subparsers(dest="command", required=True)
25
+
26
+ check_parser = subparsers.add_parser(
27
+ "check",
28
+ help="Validate a .plain module and its requires chain, and read every linked file: "
29
+ "binary files and large base64 blobs are rejected.",
30
+ )
31
+ parse_parser = subparsers.add_parser(
32
+ "parse",
33
+ help="Print the parse result of a .plain module and its requires chain as JSON. Linked files are "
34
+ "located but never read.",
35
+ )
36
+ for subparser in (check_parser, parse_parser):
37
+ subparser.add_argument("plain_file", help="Path to the .plain module.")
38
+ subparser.add_argument(
39
+ "--template-dir",
40
+ help="Directory searched for modules and templates after the .plain file's own directory. "
41
+ "Overrides the template-dir value of the config file.",
42
+ )
43
+ subparser.add_argument(
44
+ "--config-name",
45
+ default=DEFAULT_CONFIG_NAME,
46
+ help="Name of the config file to read template-dir from. Looked up in the .plain file's directory "
47
+ "and the current working directory. Defaults to %(default)s.",
48
+ )
49
+ return parser
50
+
51
+
52
+ def resolve_config_file(config_name: str, plain_file_path: str) -> str | None:
53
+ """The config file next to the .plain file, else the one in the working directory, else None."""
54
+ plain_file_dir = os.path.dirname(os.path.abspath(plain_file_path))
55
+ plain_dir_config = os.path.normpath(os.path.join(plain_file_dir, config_name))
56
+ cwd_config = os.path.normpath(os.path.join(os.getcwd(), config_name))
57
+
58
+ in_plain_dir = os.path.exists(plain_dir_config)
59
+ in_cwd = os.path.exists(cwd_config)
60
+ if in_plain_dir and in_cwd and plain_dir_config != cwd_config:
61
+ raise AmbiguousConfigFileError(
62
+ f"Config file '{config_name}' was found in two locations:\n"
63
+ f" - Plain file directory: {plain_file_dir}\n"
64
+ f" - Current working directory: {os.getcwd()}\n"
65
+ f"Remove the config file from one of these locations to resolve the ambiguity."
66
+ )
67
+ if in_plain_dir:
68
+ return plain_dir_config
69
+ if in_cwd:
70
+ return cwd_config
71
+ return None
72
+
73
+
74
+ def template_dir_from_config(config_file: str) -> str | None:
75
+ """The template-dir value of the config file, resolved against the file's directory. Other keys are ignored."""
76
+ with open(config_file) as f:
77
+ config = yaml.safe_load(f) or {}
78
+
79
+ for key in TEMPLATE_DIR_CONFIG_KEYS:
80
+ value = config.get(key)
81
+ if value:
82
+ value = os.path.expanduser(str(value))
83
+ return value if os.path.isabs(value) else os.path.join(os.path.dirname(config_file), value)
84
+ return None
85
+
86
+
87
+ @dataclass
88
+ class ParseResult:
89
+ modules: list[
90
+ tuple[str, dict]
91
+ ] # (module name, plain source tree) for the chain, deepest ancestor first, entry last.
92
+ # Each link target in the chain, as written, to the path it resolves to, relative to the entry module's dir
93
+ # with `/` as separator (loaders.relative_resource_path).
94
+ resources: dict[str, str]
95
+
96
+
97
+ def parse_spec(plain_file_path: str, template_dir: str | None, load_resources: bool) -> ParseResult:
98
+ """Parse the module and its requires chain, resolve its links, and walk the entry's FRIDs; raise on the first error.
99
+
100
+ With ``load_resources``, each module's linked files are also read and checked (text only, no large base64 blob) right
101
+ after that module's links are collected, so a resource error is reported before a FRID-walk error.
102
+ """
103
+ # Absolute, so the chain parse and the resource paths below search the same directories.
104
+ template_dirs = [os.path.dirname(os.path.abspath(plain_file_path))]
105
+ if template_dir:
106
+ template_dirs.append(os.path.abspath(template_dir))
107
+
108
+ chain = plain_file.parse_module_chain(os.path.basename(plain_file_path), template_dirs)
109
+
110
+ # The target joined as written onto its template dir (`..` kept, symlinks not resolved), made relative to the
111
+ # entry module's dir so the result does not depend on where the project sits on the machine.
112
+ resources: dict[str, str] = {}
113
+ for module_name, plain_source_tree in chain:
114
+ resources_list: list[dict] = []
115
+ plain_spec.collect_linked_resources(plain_source_tree, resources_list, None, True)
116
+ if load_resources:
117
+ loaders.load_linked_resources(template_dirs, resources_list, module_name)
118
+ for resource in resources_list:
119
+ target = resource["target"]
120
+ resource_dir = loaders.find_resource_dir(template_dirs, target)
121
+ resources[target] = loaders.relative_resource_path(template_dirs[0], resource_dir, target)
122
+
123
+ _, top_plain_source_tree = chain[-1]
124
+ for frid in plain_spec.get_frids(top_plain_source_tree):
125
+ plain_spec.get_specifications_for_frid(top_plain_source_tree, frid)
126
+
127
+ return ParseResult(chain, resources)
128
+
129
+
130
+ def check(plain_file_path: str, template_dir: str | None) -> None:
131
+ """Raise if the module, its requires chain, or its linked resources are invalid."""
132
+ parse_spec(plain_file_path, template_dir, load_resources=True)
133
+
134
+
135
+ def _error_output(type_name: str, message: str) -> dict:
136
+ return {"format_version": PARSE_FORMAT_VERSION, "type": type_name, "message": message}
137
+
138
+
139
+ def _escape(text: str) -> str:
140
+ """The text with every non-ASCII character as a backslash escape, so it always encodes."""
141
+ return text.encode("ascii", "backslashreplace").decode("ascii")
142
+
143
+
144
+ def _encode_output(output: dict) -> bytes:
145
+ """The output as UTF-8 JSON; raise if it is not JSON-serialisable or holds text that is not valid UTF-8."""
146
+ return (json.dumps(output, indent=2, ensure_ascii=False) + "\n").encode("utf-8")
147
+
148
+
149
+ def parse(plain_file_path: str, template_dir: str | None) -> int:
150
+ """Print the parse result, or the parser error, as one UTF-8 JSON document on stdout; return the exit code."""
151
+ output: dict
152
+ try:
153
+ result = parse_spec(plain_file_path, template_dir, load_resources=False)
154
+ output = {
155
+ "format_version": PARSE_FORMAT_VERSION,
156
+ "modules": [{"module": module_name, "tree": tree} for module_name, tree in result.modules],
157
+ "resources": result.resources,
158
+ }
159
+ exit_code = 0
160
+ except Exception as e:
161
+ output = _error_output(type(e).__name__, str(e))
162
+ exit_code = 1
163
+
164
+ try:
165
+ data = _encode_output(output)
166
+ except UnicodeEncodeError as e:
167
+ # A file or directory name that is not valid UTF-8, which is allowed on Linux.
168
+ if exit_code == 1:
169
+ # A parser error whose message names such a file. Keep its type and escape the name.
170
+ output = _error_output(output["type"], _escape(output["message"]))
171
+ else:
172
+ # A valid result that contains such a name. Report the output line that contains it.
173
+ line_start = e.object.rfind("\n", 0, e.start) + 1
174
+ line_end = e.object.find("\n", e.end)
175
+ line = e.object[line_start : line_end if line_end != -1 else None].strip().rstrip(",")
176
+ output = _error_output(
177
+ type(e).__name__, f"The parse result holds text that is not valid UTF-8: {_escape(line)}"
178
+ )
179
+ data = _encode_output(output)
180
+ exit_code = 1
181
+ except Exception as e:
182
+ # A value in the result that JSON cannot represent. Report it as a JSON error.
183
+ data = _encode_output(_error_output(type(e).__name__, _escape(str(e))))
184
+ exit_code = 1
185
+
186
+ # Write the UTF-8 bytes to stdout's byte stream, because sys.stdout encodes text with the system encoding, which
187
+ # may not be UTF-8.
188
+ buffer = getattr(sys.stdout, "buffer", None)
189
+ if buffer is None:
190
+ # A Python caller replaced stdout with a stream that takes only text.
191
+ sys.stdout.write(data.decode("utf-8"))
192
+ return exit_code
193
+
194
+ try:
195
+ sys.stdout.flush() # Send out anything already written to sys.stdout first.
196
+ buffer.write(data)
197
+ buffer.flush()
198
+ except BrokenPipeError:
199
+ # The reader closed the pipe early. Send stdout to /dev/null, so Python's flush at exit does not fail again,
200
+ # and keep the exit code.
201
+ devnull = os.open(os.devnull, os.O_WRONLY)
202
+ os.dup2(devnull, sys.stdout.fileno())
203
+ return exit_code
204
+
205
+
206
+ def main(argv: list[str] | None = None) -> int:
207
+ parser = build_parser()
208
+ args = parser.parse_args(argv)
209
+
210
+ try:
211
+ # The config file is resolved even when --template-dir is given, so an ambiguous config is always an error.
212
+ config_template_dir = _template_dir_from_config(parser, args)
213
+ if args.command == "parse":
214
+ return parse(args.plain_file, args.template_dir or config_template_dir)
215
+ check(args.plain_file, args.template_dir or config_template_dir)
216
+ except KeyboardInterrupt:
217
+ return 130
218
+ except Exception as e:
219
+ print(f"Error: {e}", file=sys.stderr)
220
+ return 1
221
+
222
+ print(f"{args.plain_file}: OK")
223
+ return 0
224
+
225
+
226
+ def _template_dir_from_config(parser: argparse.ArgumentParser, args: argparse.Namespace) -> str | None:
227
+ try:
228
+ config_file = resolve_config_file(args.config_name, args.plain_file)
229
+ return template_dir_from_config(config_file) if config_file is not None else None
230
+ except AmbiguousConfigFileError as e:
231
+ parser.error(str(e))
232
+ except Exception as e:
233
+ parser.error(f"Error reading config file: {e}")
@@ -36,20 +36,41 @@ 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.
39
+ def find_resource_dir(dirs, file_name):
40
+ """Return the first dir in which file_name exists, or None if not found.
41
41
 
42
42
  Mirrors the search order used by open_from so that validating a linked resource and
43
43
  loading it can never disagree.
44
44
  """
45
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
46
+ if os.path.exists(os.path.join(dir, file_name)):
47
+ return dir
49
48
 
50
49
  return None
51
50
 
52
51
 
52
+ def resolve_linked_resource(dirs, file_name):
53
+ """Return the first existing path for file_name across dirs, or None if not found."""
54
+ resource_dir = find_resource_dir(dirs, file_name)
55
+ return None if resource_dir is None else os.path.join(resource_dir, file_name)
56
+
57
+
58
+ def relative_resource_path(base_dir: str, resource_dir: str, target: str) -> str:
59
+ """The path of target in resource_dir, relative to base_dir, with `/` as separator.
60
+
61
+ The dir part is computed lexically (symlinks not resolved) and the target is appended as written (`..` kept),
62
+ so joining the result onto base_dir names the same file as os.path.join(resource_dir, target). When no relative
63
+ path exists (another drive on Windows), the absolute path is returned instead, still with `/`.
64
+ """
65
+ try:
66
+ rel_dir = os.path.relpath(resource_dir, base_dir)
67
+ except ValueError:
68
+ return os.path.join(resource_dir, target).replace(os.sep, "/")
69
+ if rel_dir == os.curdir:
70
+ return target
71
+ return rel_dir.replace(os.sep, "/") + "/" + target
72
+
73
+
53
74
  def load_linked_resources(template_dirs: list[str], resources_list, module_name: str):
54
75
  linked_resources = {}
55
76
 
@@ -0,0 +1,7 @@
1
+ ***implementation reqs***
2
+
3
+ - A req.
4
+
5
+ ***functional specs***
6
+
7
+ - A functionality. See [the notes](../shared/notes.md).
@@ -0,0 +1,8 @@
1
+ ***implementation reqs***
2
+
3
+ - :Implementation: is written in Python.
4
+ - :Implementation: follows [the data](../binary.bin).
5
+
6
+ ***functional specs***
7
+
8
+ - :Implementation: has an entry point.
@@ -0,0 +1 @@
1
+ the variable {{ name | code_variable }}
@@ -0,0 +1,14 @@
1
+ ---
2
+ requires:
3
+ - base
4
+ ---
5
+
6
+ ***implementation reqs***
7
+
8
+ - :Implementation: is written in Python.
9
+
10
+ ***functional specs***
11
+
12
+ - :Implementation: names {% include "snippet.plain", name: "first" %}.
13
+
14
+ - :Implementation: names {% include "snippet.plain", name: "second" %}.
@@ -0,0 +1,7 @@
1
+ ***implementation reqs***
2
+
3
+ - A req.
4
+
5
+ ***functional specs***
6
+
7
+ - A functionality. See [the notes](notes.md).
@@ -0,0 +1,12 @@
1
+ ---
2
+ requires:
3
+ - shared_link_helper
4
+ ---
5
+
6
+ ***implementation reqs***
7
+
8
+ - A req.
9
+
10
+ ***functional specs***
11
+
12
+ - A functionality. Also [the notes](notes.md).
@@ -0,0 +1,7 @@
1
+ ***implementation reqs***
2
+
3
+ - A req.
4
+
5
+ ***functional specs***
6
+
7
+ - A functionality. See [the notes](notes.md).