plain-parser 0.1.0__tar.gz → 0.1.1.dev2__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 (139) hide show
  1. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/PKG-INFO +16 -1
  2. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/README.md +14 -0
  3. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/pyproject.toml +4 -0
  4. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/src/plain_parser/__init__.py +2 -0
  5. plain_parser-0.1.1.dev2/src/plain_parser/cli.py +117 -0
  6. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/src/plain_parser/plain_file.py +84 -41
  7. plain_parser-0.1.1.dev2/tests/data/cli/acceptance_test_binary_resource.plain +10 -0
  8. plain_parser-0.1.1.dev2/tests/data/cli/acceptance_test_resource.plain +10 -0
  9. plain_parser-0.1.1.dev2/tests/data/cli/base64_resource.plain +7 -0
  10. plain_parser-0.1.1.dev2/tests/data/cli/binary.bin +0 -0
  11. plain_parser-0.1.1.dev2/tests/data/cli/binary_resource.plain +7 -0
  12. plain_parser-0.1.1.dev2/tests/data/cli/code_variable_conflict.plain +9 -0
  13. plain_parser-0.1.1.dev2/tests/data/cli/code_variable_template.plain +1 -0
  14. plain_parser-0.1.1.dev2/tests/data/cli/missing_resource.plain +7 -0
  15. plain_parser-0.1.1.dev2/tests/data/cli/not_plain.md +1 -0
  16. plain_parser-0.1.1.dev2/tests/data/cli/notes.md +3 -0
  17. plain_parser-0.1.1.dev2/tests/data/cli/requires_binary_resource_base.plain +7 -0
  18. plain_parser-0.1.1.dev2/tests/data/cli/requires_binary_resource_top.plain +12 -0
  19. plain_parser-0.1.1.dev2/tests/data/cli/subdir/nested.plain +7 -0
  20. plain_parser-0.1.1.dev2/tests/data/cli/undefined_concept.plain +13 -0
  21. plain_parser-0.1.1.dev2/tests/data/cli/valid.plain +11 -0
  22. plain_parser-0.1.1.dev2/tests/data/requires/chain_base.plain +7 -0
  23. plain_parser-0.1.1.dev2/tests/data/requires/chain_fork_top.plain +13 -0
  24. plain_parser-0.1.1.dev2/tests/data/requires/chain_middle.plain +12 -0
  25. plain_parser-0.1.1.dev2/tests/data/requires/chain_top.plain +12 -0
  26. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/requires/diamond_requires_1.plain +4 -0
  27. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/requires/diamond_requires_2.plain +4 -0
  28. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/requires/independent_requires_1.plain +4 -0
  29. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/requires/independent_requires_2.plain +4 -0
  30. plain_parser-0.1.1.dev2/tests/data/requires/invalid_ancestor_base.plain +7 -0
  31. plain_parser-0.1.1.dev2/tests/data/requires/invalid_ancestor_top.plain +12 -0
  32. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/requires/normal_requires_1.plain +4 -0
  33. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/requires/normal_requires_2.plain +4 -0
  34. plain_parser-0.1.1.dev2/tests/test_cli.py +325 -0
  35. plain_parser-0.1.1.dev2/tests/test_requires.py +60 -0
  36. plain_parser-0.1.0/tests/test_requires.py +0 -28
  37. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/.flake8 +0 -0
  38. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/.github/workflows/lint-and-test.yml +0 -0
  39. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/.github/workflows/publish-dev-to-pypi.yml +0 -0
  40. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/.github/workflows/publish-to-pypi.yml +0 -0
  41. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/.gitignore +0 -0
  42. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/LICENSE +0 -0
  43. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/src/plain_parser/concept_utils.py +0 -0
  44. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/src/plain_parser/exceptions.py +0 -0
  45. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/src/plain_parser/liquid_nodes.py +0 -0
  46. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/src/plain_parser/loaders.py +0 -0
  47. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/src/plain_parser/plain_spec.py +0 -0
  48. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/__init__.py +0 -0
  49. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/conftest.py +0 -0
  50. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/imports/circular_imports_1.plain +0 -0
  51. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/imports/circular_imports_2.plain +0 -0
  52. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/imports/circular_imports_main.plain +0 -0
  53. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/imports/diamond_import_1.plain +0 -0
  54. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/imports/diamond_import_2.plain +0 -0
  55. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/imports/diamond_import_common.plain +0 -0
  56. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/imports/diamond_imports_main.plain +0 -0
  57. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/imports/import_with_functionalities.plain +0 -0
  58. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/imports/imports_module_with_functionalities.plain +0 -0
  59. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/imports/non_existent_import.plain +0 -0
  60. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfile/add_new_task_modal_specification.yaml +0 -0
  61. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfile/duplicate_specification_heading.plain +0 -0
  62. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfile/empty_functional_specs_section.plain +0 -0
  63. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfile/invalid_specification_order.plain +0 -0
  64. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfile/missing_non_functional_requirements.plain +0 -0
  65. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfile/no_functional_specs_section.plain +0 -0
  66. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfile/plain_source_with_absolute_link.plain +0 -0
  67. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfile/plain_source_with_url_link.plain +0 -0
  68. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfile/task_list_ui_specification.yaml +0 -0
  69. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfile/task_manager_with_reference_links.plain +0 -0
  70. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfile/without_non_functional_requirement.plain +0 -0
  71. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/concept_validation_acceptance_tests.plain +0 -0
  72. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/concept_validation_acceptance_tests_nondefined.plain +0 -0
  73. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/concept_validation_defined_nondefined.plain +0 -0
  74. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/concept_validation_defined_nondefined_2.plain +0 -0
  75. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/concept_validation_definition.plain +0 -0
  76. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/concept_validation_noconcepts.plain +0 -0
  77. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/concept_validation_nonconcept.plain +0 -0
  78. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/concept_validation_nondefined.plain +0 -0
  79. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/concept_validation_redefinition.plain +0 -0
  80. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/concept_validation_several_concepts.plain +0 -0
  81. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/concept_validation_valid.plain +0 -0
  82. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/cyclic_definitions.plain +0 -0
  83. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts.plain +0 -0
  84. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_base.plain +0 -0
  85. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_defaults_exported.plain +0 -0
  86. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_example.plain +0 -0
  87. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_imported_exported.plain +0 -0
  88. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_inherited_reexported.plain +0 -0
  89. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_malformed.plain +0 -0
  90. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_missing_definition.plain +0 -0
  91. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_missing_definition_example.plain +0 -0
  92. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet.plain +0 -0
  93. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet_undefined.plain +0 -0
  94. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_nested.plain +0 -0
  95. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_nested_example.plain +0 -0
  96. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_no_definitions.plain +0 -0
  97. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_not_declared.plain +0 -0
  98. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_not_declared_example.plain +0 -0
  99. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_transitive_example.plain +0 -0
  100. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_transitive_l1.plain +0 -0
  101. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/exported_concepts_transitive_l2.plain +0 -0
  102. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/plain_file_parser_with_comments.plain +0 -0
  103. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/plain_file_with_comments_indented.plain +0 -0
  104. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/regular_plain_source.plain +0 -0
  105. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/required_concepts.plain +0 -0
  106. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/required_concepts_defs.plain +0 -0
  107. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/required_concepts_example.plain +0 -0
  108. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/required_concepts_l1.plain +0 -0
  109. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/required_concepts_l2.plain +0 -0
  110. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/required_concepts_missing.plain +0 -0
  111. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/required_concepts_module.plain +0 -0
  112. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/required_concepts_partial.plain +0 -0
  113. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/required_concepts_partial_duplicate.plain +0 -0
  114. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/requires_without_definitions_example.plain +0 -0
  115. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/topological_sort.plain +0 -0
  116. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/plainfileparser/topological_sort_not_referenced.plain +0 -0
  117. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/requires/circular_requires_main.plain +0 -0
  118. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/requires/circular_requires_sub.plain +0 -0
  119. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/requires/diamond_requires_common.plain +0 -0
  120. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/requires/diamond_requires_main.plain +0 -0
  121. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/requires/independent_requires_main.plain +0 -0
  122. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/requires/non_existent_require.plain +0 -0
  123. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/requires/normal_requires_common.plain +0 -0
  124. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/requires/normal_requires_main.plain +0 -0
  125. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/sample_base64_image.txt +0 -0
  126. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/simple.plain +0 -0
  127. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/templates/block_level_include.plain +0 -0
  128. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/templates/code_variables.plain +0 -0
  129. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/templates/header.plain +0 -0
  130. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/templates/implement.plain +0 -0
  131. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/templates/implement_2.plain +0 -0
  132. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/templates/template_include.plain +0 -0
  133. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/data/templates/test_hardest_problem.plain +0 -0
  134. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/test_exceptions.py +0 -0
  135. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/test_imports.py +0 -0
  136. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/test_loaders.py +0 -0
  137. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/test_plainfile.py +0 -0
  138. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/tests/test_plainfileparser.py +0 -0
  139. {plain_parser-0.1.0 → plain_parser-0.1.1.dev2}/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.0
3
+ Version: 0.1.1.dev2
4
4
  Summary: Parser for ***plain specification files
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -13,6 +13,7 @@ Requires-Dist: mistletoe<2,>=1.6.0
13
13
  Requires-Dist: networkx<4,>=3.6.1
14
14
  Requires-Dist: python-frontmatter<2,>=1.3.0
15
15
  Requires-Dist: python-liquid2<0.5,>=0.4.0
16
+ Requires-Dist: pyyaml<7,>=6
16
17
  Provides-Extra: dev
17
18
  Requires-Dist: black==26.5.1; extra == 'dev'
18
19
  Requires-Dist: flake8==7.3.0; extra == 'dev'
@@ -37,6 +38,20 @@ pip install plain-parser
37
38
 
38
39
  ## Usage
39
40
 
41
+ Validate a module and its `requires` chain from the command line (`<file>: OK` and exit `0` when valid; `Error: …` on stderr and exit `1` otherwise):
42
+
43
+ ```bash
44
+ plain-parser check my_spec.plain [--template-dir DIR] [--config-name NAME]
45
+ ```
46
+
47
+ Modules and `{% include %}` templates are looked up in the `.plain` file's directory, then in `--template-dir`.
48
+ When `--template-dir` is not given, it is read from the `template-dir` key of `config.yaml` (or the file named by
49
+ `--config-name`), found next to the `.plain` file or in the working directory. Finding it in both is a usage error
50
+ (exit `2`). The config file is read even when `--template-dir` is given, so a malformed one fails the run either way.
51
+ Other config keys are ignored.
52
+
53
+ Or from Python:
54
+
40
55
  ```python
41
56
  from plain_parser import plain_file_parser
42
57
 
@@ -14,6 +14,20 @@ pip install plain-parser
14
14
 
15
15
  ## Usage
16
16
 
17
+ Validate a module and its `requires` chain from the command line (`<file>: OK` and exit `0` when valid; `Error: …` on stderr and exit `1` otherwise):
18
+
19
+ ```bash
20
+ plain-parser check my_spec.plain [--template-dir DIR] [--config-name NAME]
21
+ ```
22
+
23
+ Modules and `{% include %}` templates are looked up in the `.plain` file's directory, then in `--template-dir`.
24
+ When `--template-dir` is not given, it is read from the `template-dir` key of `config.yaml` (or the file named by
25
+ `--config-name`), found next to the `.plain` file or in the working directory. Finding it in both is a usage error
26
+ (exit `2`). The config file is read even when `--template-dir` is given, so a malformed one fails the run either way.
27
+ Other config keys are ignored.
28
+
29
+ Or from Python:
30
+
17
31
  ```python
18
32
  from plain_parser import plain_file_parser
19
33
 
@@ -21,6 +21,7 @@ dependencies = [
21
21
  "mistletoe>=1.6.0,<2",
22
22
  "python-frontmatter>=1.3.0,<2",
23
23
  "networkx>=3.6.1,<4",
24
+ "pyyaml>=6,<7",
24
25
  ]
25
26
 
26
27
  [project.optional-dependencies]
@@ -32,6 +33,9 @@ dev = [
32
33
  "mypy==2.3.0",
33
34
  ]
34
35
 
36
+ [project.scripts]
37
+ plain-parser = "plain_parser.cli:main"
38
+
35
39
  [tool.hatch.version]
36
40
  source = "vcs"
37
41
 
@@ -6,6 +6,7 @@ from plain_parser.plain_file import (
6
6
  get_filename_from_module_name,
7
7
  get_module_name_from_filename,
8
8
  marshall_plain_source,
9
+ parse_module_chain,
9
10
  parse_plain_file,
10
11
  parse_plain_source,
11
12
  plain_file_parser,
@@ -19,6 +20,7 @@ __all__ = [
19
20
  "get_module_name_from_filename",
20
21
  "loaders",
21
22
  "marshall_plain_source",
23
+ "parse_module_chain",
22
24
  "parse_plain_file",
23
25
  "parse_plain_source",
24
26
  "plain_file",
@@ -0,0 +1,117 @@
1
+ """Command-line entry point: ``plain-parser check <file.plain> [--template-dir DIR] [--config-name NAME]``."""
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+
7
+ import yaml
8
+
9
+ from plain_parser import loaders, plain_file, plain_spec
10
+
11
+ DEFAULT_CONFIG_NAME = "config.yaml"
12
+ TEMPLATE_DIR_CONFIG_KEYS = ("template_dir", "template-dir")
13
+
14
+
15
+ class AmbiguousConfigFileError(Exception):
16
+ pass
17
+
18
+
19
+ def build_parser() -> argparse.ArgumentParser:
20
+ parser = argparse.ArgumentParser(prog="plain-parser", description="Tools for ***plain specification files.")
21
+ subparsers = parser.add_subparsers(dest="command", required=True)
22
+
23
+ check_parser = subparsers.add_parser("check", help="Validate a .plain module and its requires chain.")
24
+ check_parser.add_argument("plain_file", help="Path to the .plain module.")
25
+ check_parser.add_argument(
26
+ "--template-dir",
27
+ help="Directory searched for modules and templates after the .plain file's own directory. "
28
+ "Overrides the template-dir value of the config file.",
29
+ )
30
+ check_parser.add_argument(
31
+ "--config-name",
32
+ default=DEFAULT_CONFIG_NAME,
33
+ help="Name of the config file to read template-dir from. Looked up in the .plain file's directory "
34
+ "and the current working directory. Defaults to %(default)s.",
35
+ )
36
+ return parser
37
+
38
+
39
+ def resolve_config_file(config_name: str, plain_file_path: str) -> str | None:
40
+ """The config file next to the .plain file, else the one in the working directory, else None."""
41
+ plain_file_dir = os.path.dirname(os.path.abspath(plain_file_path))
42
+ plain_dir_config = os.path.normpath(os.path.join(plain_file_dir, config_name))
43
+ cwd_config = os.path.normpath(os.path.join(os.getcwd(), config_name))
44
+
45
+ in_plain_dir = os.path.exists(plain_dir_config)
46
+ in_cwd = os.path.exists(cwd_config)
47
+ if in_plain_dir and in_cwd and plain_dir_config != cwd_config:
48
+ raise AmbiguousConfigFileError(
49
+ f"Config file '{config_name}' was found in two locations:\n"
50
+ f" - Plain file directory: {plain_file_dir}\n"
51
+ f" - Current working directory: {os.getcwd()}\n"
52
+ f"Remove the config file from one of these locations to resolve the ambiguity."
53
+ )
54
+ if in_plain_dir:
55
+ return plain_dir_config
56
+ if in_cwd:
57
+ return cwd_config
58
+ return None
59
+
60
+
61
+ def template_dir_from_config(config_file: str) -> str | None:
62
+ """The template-dir value of the config file, resolved against the file's directory. Other keys are ignored."""
63
+ with open(config_file) as f:
64
+ config = yaml.safe_load(f) or {}
65
+
66
+ for key in TEMPLATE_DIR_CONFIG_KEYS:
67
+ value = config.get(key)
68
+ if value:
69
+ value = os.path.expanduser(str(value))
70
+ return value if os.path.isabs(value) else os.path.join(os.path.dirname(config_file), value)
71
+ return None
72
+
73
+
74
+ def check(plain_file_path: str, template_dir: str | None) -> None:
75
+ """Raise if the module, its requires chain, or its linked resources are invalid."""
76
+ template_dirs = [os.path.dirname(os.path.abspath(plain_file_path))]
77
+ if template_dir:
78
+ template_dirs.append(template_dir)
79
+
80
+ chain = plain_file.parse_module_chain(os.path.basename(plain_file_path), template_dirs)
81
+
82
+ for module_name, plain_source_tree in chain:
83
+ resources_list: list[dict] = []
84
+ plain_spec.collect_linked_resources(plain_source_tree, resources_list, None, True)
85
+ loaders.load_linked_resources(template_dirs, resources_list, module_name)
86
+
87
+ _, top_plain_source_tree = chain[-1]
88
+ for frid in plain_spec.get_frids(top_plain_source_tree):
89
+ plain_spec.get_specifications_for_frid(top_plain_source_tree, frid)
90
+
91
+
92
+ def main(argv: list[str] | None = None) -> int:
93
+ parser = build_parser()
94
+ args = parser.parse_args(argv)
95
+
96
+ try:
97
+ # The config file is resolved even when --template-dir is given, so an ambiguous config is always an error.
98
+ config_template_dir = _template_dir_from_config(parser, args)
99
+ check(args.plain_file, args.template_dir or config_template_dir)
100
+ except KeyboardInterrupt:
101
+ return 130
102
+ except Exception as e:
103
+ print(f"Error: {e}", file=sys.stderr)
104
+ return 1
105
+
106
+ print(f"{args.plain_file}: OK")
107
+ return 0
108
+
109
+
110
+ def _template_dir_from_config(parser: argparse.ArgumentParser, args: argparse.Namespace) -> str | None:
111
+ try:
112
+ config_file = resolve_config_file(args.config_name, args.plain_file)
113
+ return template_dir_from_config(config_file) if config_file is not None else None
114
+ except AmbiguousConfigFileError as e:
115
+ parser.error(str(e))
116
+ except Exception as e:
117
+ parser.error(f"Error reading config file: {e}")
@@ -701,11 +701,16 @@ def parse_plain_file(
701
701
 
702
702
  def process_required_modules(
703
703
  required_modules: list[str],
704
- code_variables: dict,
705
704
  template_dirs: list[str],
706
705
  all_required_modules: list[str],
707
706
  modules_trace: list[str],
707
+ chain: list[tuple[str, dict]],
708
708
  ) -> list[mistletoe.block_token.token]:
709
+ """Parse and fully validate every required module, deepest first.
710
+
711
+ Appends ``(module name, marshalled plain source tree)`` to ``chain`` for each module not already in it
712
+ and returns the exported definitions of the directly required modules.
713
+ """
709
714
  exported_definitions = list[mistletoe.block_token.token]()
710
715
  for module_name in required_modules:
711
716
  if module_name in modules_trace:
@@ -714,10 +719,12 @@ def process_required_modules(
714
719
  if len(all_required_modules) > 0 and module_name == all_required_modules[-1]:
715
720
  continue
716
721
 
722
+ code_variables: dict = {}
717
723
  plain_file_parse_result = parse_plain_file(
718
724
  module_name, code_variables, template_dirs, imported_modules=[], modules_trace=[]
719
725
  )
720
726
 
727
+ ancestor_exported_definitions: list[mistletoe.block_token.token] = []
721
728
  if len(plain_file_parse_result.required_modules) == 0:
722
729
  if len(all_required_modules) > 0:
723
730
  # For now we require that there is fixed order how required modules are dependent.
@@ -727,12 +734,12 @@ def process_required_modules(
727
734
  f"Plain syntax error: There must be a fixed order how required modules are dependent ({module_name})."
728
735
  )
729
736
  else:
730
- process_required_modules(
737
+ ancestor_exported_definitions = process_required_modules(
731
738
  plain_file_parse_result.required_modules,
732
- code_variables,
733
739
  template_dirs,
734
740
  all_required_modules,
735
741
  modules_trace + [module_name],
742
+ chain,
736
743
  )
737
744
 
738
745
  if EXPORTED_CONCEPTS_DIRECTIVE in plain_file_parse_result.plain_source_obj.metadata:
@@ -748,6 +755,12 @@ def process_required_modules(
748
755
  )
749
756
  )
750
757
 
758
+ marshalled_plain_source = validate_and_marshall_module(
759
+ plain_file_parse_result, module_name, ancestor_exported_definitions, code_variables
760
+ )
761
+ if module_name not in (chain_module_name for chain_module_name, _ in chain):
762
+ chain.append((module_name, marshalled_plain_source))
763
+
751
764
  all_required_modules.append(module_name)
752
765
 
753
766
  return exported_definitions
@@ -775,35 +788,13 @@ def process_exported_definitions(plain_source: dict, exported_definitions: list[
775
788
  plain_source[plain_spec.DEFINITIONS].children.append(exported_definition)
776
789
 
777
790
 
778
- def plain_file_parser( # noqa: C901
779
- plain_source_file_name: str,
780
- template_dirs: list[str],
781
- ) -> tuple[str, dict, list[str]]:
782
- # code_variables are used to pass code variables to the plain source
783
- # they need to be passed as an argument to the function because they populated when liquid templating is applied
784
- # and we need to pass them to the marshalled_plain_source_tree after it's rendered
785
- plain_source_file_path = Path(plain_source_file_name)
786
- if plain_source_file_path.suffix != PLAIN_SOURCE_FILE_EXTENSION:
787
- raise PlainSyntaxError(
788
- f"Plain syntax error: Invalid plain file extension: {plain_source_file_path.suffix}. Expected: {PLAIN_SOURCE_FILE_EXTENSION}."
789
- )
790
-
791
- module_name = (
792
- plain_source_file_path.stem
793
- if plain_source_file_path.is_absolute()
794
- else plain_source_file_path.with_suffix("").as_posix()
795
- )
796
-
797
- code_variables = {}
798
-
799
- plain_file_parse_result = parse_plain_file(
800
- module_name,
801
- code_variables,
802
- template_dirs,
803
- imported_modules=[],
804
- modules_trace=[],
805
- )
806
-
791
+ def validate_and_marshall_module(
792
+ plain_file_parse_result: PlainFileParseResult,
793
+ module_name: str,
794
+ exported_definitions: list[mistletoe.block_token.token],
795
+ code_variables: dict,
796
+ ) -> dict:
797
+ """Every check a module must pass after parsing, and its marshalled plain source tree."""
807
798
  if len(plain_file_parse_result.required_concepts) > 0:
808
799
  missing_required_concepts_msg = "Missing required concepts: "
809
800
  missing_required_concepts_msg += ", ".join(plain_file_parse_result.required_concepts)
@@ -831,14 +822,6 @@ def plain_file_parser( # noqa: C901
831
822
  has_requires=bool(plain_file_parse_result.required_modules),
832
823
  )
833
824
 
834
- exported_definitions = process_required_modules(
835
- plain_file_parse_result.required_modules,
836
- code_variables={},
837
- template_dirs=template_dirs,
838
- all_required_modules=[],
839
- modules_trace=[],
840
- )
841
-
842
825
  process_exported_definitions(plain_file_parse_result.plain_source, exported_definitions)
843
826
 
844
827
  process_acceptance_tests(plain_file_parse_result.plain_source)
@@ -858,4 +841,64 @@ def plain_file_parser( # noqa: C901
858
841
  if plain_spec.DEFINITIONS in marshalled_plain_source:
859
842
  concept_utils.sort_definitions(marshalled_plain_source[plain_spec.DEFINITIONS])
860
843
 
861
- return module_name, marshalled_plain_source, plain_file_parse_result.required_modules
844
+ return marshalled_plain_source
845
+
846
+
847
+ def _parse_module(
848
+ plain_source_file_name: str, template_dirs: list[str]
849
+ ) -> tuple[str, dict, list[str], list[tuple[str, dict]]]:
850
+ plain_source_file_path = Path(plain_source_file_name)
851
+ if plain_source_file_path.suffix != PLAIN_SOURCE_FILE_EXTENSION:
852
+ raise PlainSyntaxError(
853
+ f"Plain syntax error: Invalid plain file extension: {plain_source_file_path.suffix}. Expected: {PLAIN_SOURCE_FILE_EXTENSION}."
854
+ )
855
+
856
+ module_name = (
857
+ plain_source_file_path.stem
858
+ if plain_source_file_path.is_absolute()
859
+ else plain_source_file_path.with_suffix("").as_posix()
860
+ )
861
+
862
+ # code_variables are populated when liquid templating is applied and consumed by process_code_variables
863
+ # once the plain source is marshalled.
864
+ code_variables: dict = {}
865
+
866
+ plain_file_parse_result = parse_plain_file(
867
+ module_name,
868
+ code_variables,
869
+ template_dirs,
870
+ imported_modules=[],
871
+ modules_trace=[],
872
+ )
873
+
874
+ chain: list[tuple[str, dict]] = []
875
+ exported_definitions = process_required_modules(
876
+ plain_file_parse_result.required_modules,
877
+ template_dirs=template_dirs,
878
+ all_required_modules=[],
879
+ modules_trace=[],
880
+ chain=chain,
881
+ )
882
+
883
+ marshalled_plain_source = validate_and_marshall_module(
884
+ plain_file_parse_result, module_name, exported_definitions, code_variables
885
+ )
886
+
887
+ return module_name, marshalled_plain_source, plain_file_parse_result.required_modules, chain
888
+
889
+
890
+ def plain_file_parser(plain_source_file_name: str, template_dirs: list[str]) -> tuple[str, dict, list[str]]:
891
+ """Parse a module, validating it and every module in its ``requires`` chain."""
892
+ module_name, marshalled_plain_source, required_modules, _ = _parse_module(plain_source_file_name, template_dirs)
893
+ return module_name, marshalled_plain_source, required_modules
894
+
895
+
896
+ def parse_module_chain(plain_file_name: str, template_dirs: list[str]) -> list[tuple[str, dict]]:
897
+ """Parse a module and every module in its ``requires`` chain.
898
+
899
+ Returns ``(module name, marshalled plain source tree)`` pairs: every required module
900
+ first, deepest ancestors first, then the module itself. A module reached through more
901
+ than one ``requires`` path appears once, at its first (deepest) position.
902
+ """
903
+ module_name, marshalled_plain_source, _, chain = _parse_module(plain_file_name, template_dirs)
904
+ return chain + [(module_name, marshalled_plain_source)]
@@ -0,0 +1,10 @@
1
+ ***implementation reqs***
2
+
3
+ - An implementation requirement.
4
+
5
+ ***functional specs***
6
+
7
+ - A functionality.
8
+
9
+ ***acceptance tests***
10
+ - Behaves as described in [the binary](binary.bin).
@@ -0,0 +1,10 @@
1
+ ***implementation reqs***
2
+
3
+ - An implementation requirement.
4
+
5
+ ***functional specs***
6
+
7
+ - A functionality.
8
+
9
+ ***acceptance tests***
10
+ - Behaves as described in [the notes](notes.md).
@@ -0,0 +1,7 @@
1
+ ***implementation reqs***
2
+
3
+ - Follow [the sample](../sample_base64_image.txt).
4
+
5
+ ***functional specs***
6
+
7
+ - A functionality.
@@ -0,0 +1,7 @@
1
+ ***implementation reqs***
2
+
3
+ - Follow [the binary](binary.bin).
4
+
5
+ ***functional specs***
6
+
7
+ - A functionality.
@@ -0,0 +1,9 @@
1
+ ***implementation reqs***
2
+
3
+ - An implementation requirement.
4
+
5
+ ***functional specs***
6
+
7
+ - Implement {% include "code_variable_template.plain", variable_name: "first" %}
8
+
9
+ - Implement {% include "code_variable_template.plain", variable_name: "second" %}
@@ -0,0 +1 @@
1
+ something named {{ variable_name | code_variable }}
@@ -0,0 +1,7 @@
1
+ ***implementation reqs***
2
+
3
+ - Follow [the missing notes](does_not_exist.md).
4
+
5
+ ***functional specs***
6
+
7
+ - A functionality.
@@ -0,0 +1 @@
1
+ Not a plain file.
@@ -0,0 +1,3 @@
1
+ # Notes
2
+
3
+ Plain text notes.
@@ -0,0 +1,7 @@
1
+ ***implementation reqs***
2
+
3
+ - Follow [the binary](binary.bin).
4
+
5
+ ***functional specs***
6
+
7
+ - A base functionality.
@@ -0,0 +1,12 @@
1
+ ---
2
+ requires:
3
+ - requires_binary_resource_base
4
+ ---
5
+
6
+ ***implementation reqs***
7
+
8
+ - Top implementation requirement.
9
+
10
+ ***functional specs***
11
+
12
+ - A top functionality.
@@ -0,0 +1,7 @@
1
+ ***implementation reqs***
2
+
3
+ - Nested implementation requirement.
4
+
5
+ ***functional specs***
6
+
7
+ - A nested functionality.
@@ -0,0 +1,13 @@
1
+ ***definitions***
2
+
3
+ - :App: is a console application.
4
+
5
+ ***implementation reqs***
6
+
7
+ - :App: is written in Python.
8
+
9
+ ***functional specs***
10
+
11
+ - :App: greets the :User:.
12
+
13
+ - :App: says goodbye to the :Visitor:.
@@ -0,0 +1,11 @@
1
+ ***definitions***
2
+
3
+ - :App: is a console application.
4
+
5
+ ***implementation reqs***
6
+
7
+ - :App: is written in Python.
8
+
9
+ ***functional specs***
10
+
11
+ - :App: prints "hello, world".
@@ -0,0 +1,7 @@
1
+ ***implementation reqs***
2
+
3
+ - Base implementation requirement.
4
+
5
+ ***functional specs***
6
+
7
+ - A base functionality.
@@ -0,0 +1,13 @@
1
+ ---
2
+ requires:
3
+ - chain_base
4
+ - chain_middle
5
+ ---
6
+
7
+ ***implementation reqs***
8
+
9
+ - Fork implementation requirement.
10
+
11
+ ***functional specs***
12
+
13
+ - A fork functionality.
@@ -0,0 +1,12 @@
1
+ ---
2
+ requires:
3
+ - chain_base
4
+ ---
5
+
6
+ ***implementation reqs***
7
+
8
+ - Middle implementation requirement.
9
+
10
+ ***functional specs***
11
+
12
+ - A middle functionality.
@@ -0,0 +1,12 @@
1
+ ---
2
+ requires:
3
+ - chain_middle
4
+ ---
5
+
6
+ ***implementation reqs***
7
+
8
+ - Top implementation requirement.
9
+
10
+ ***functional specs***
11
+
12
+ - A top functionality.
@@ -7,6 +7,10 @@ requires:
7
7
 
8
8
  - :App: is a web application.
9
9
 
10
+ ***implementation reqs***
11
+
12
+ - An implementation requirement.
13
+
10
14
  ***functional specs***
11
15
 
12
16
  - Implement the entry point for :App:.
@@ -7,6 +7,10 @@ requires:
7
7
 
8
8
  - :User: is the end-user of the application.
9
9
 
10
+ ***implementation reqs***
11
+
12
+ - An implementation requirement.
13
+
10
14
  ***functional specs***
11
15
 
12
16
  - :User: should be able to log in using their email and password.
@@ -2,6 +2,10 @@
2
2
 
3
3
  - :App: is a web application.
4
4
 
5
+ ***implementation reqs***
6
+
7
+ - An implementation requirement.
8
+
5
9
  ***functional specs***
6
10
 
7
11
  - Implement the entry point for :App:.
@@ -2,6 +2,10 @@
2
2
 
3
3
  - :User: is the end-user of the application.
4
4
 
5
+ ***implementation reqs***
6
+
7
+ - An implementation requirement.
8
+
5
9
  ***functional specs***
6
10
 
7
11
  - :User: should be able to log in using their email and password.
@@ -0,0 +1,7 @@
1
+ ***implementation reqs***
2
+
3
+ - Base implementation requirement.
4
+
5
+ ***functional specs***
6
+
7
+ - A base functionality that uses :UndefinedThing:.
@@ -0,0 +1,12 @@
1
+ ---
2
+ requires:
3
+ - invalid_ancestor_base
4
+ ---
5
+
6
+ ***implementation reqs***
7
+
8
+ - Top implementation requirement.
9
+
10
+ ***functional specs***
11
+
12
+ - A top functionality.
@@ -7,6 +7,10 @@ requires:
7
7
 
8
8
  - :App: is a web application.
9
9
 
10
+ ***implementation reqs***
11
+
12
+ - An implementation requirement.
13
+
10
14
  ***functional specs***
11
15
 
12
16
  - Implement the entry point for :App:.
@@ -7,6 +7,10 @@ requires:
7
7
 
8
8
  - :User: is the end-user of the application.
9
9
 
10
+ ***implementation reqs***
11
+
12
+ - An implementation requirement.
13
+
10
14
  ***functional specs***
11
15
 
12
16
  - :User: should be able to log in using their email and password.