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.
- plain_parser-0.1.4.dev10/PKG-INFO +125 -0
- plain_parser-0.1.4.dev10/README.md +102 -0
- plain_parser-0.1.4.dev10/src/plain_parser/cli.py +233 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/loaders.py +26 -5
- plain_parser-0.1.4.dev10/tests/data/cli/parent_link/a/spec/top.plain +7 -0
- plain_parser-0.1.4.dev10/tests/data/cli/parent_link/b/shared/notes.md +1 -0
- plain_parser-0.1.4.dev10/tests/data/cli/parent_link/b/tpl/.gitkeep +0 -0
- plain_parser-0.1.4.dev10/tests/data/cli/resource_before_frid_walk/base.plain +8 -0
- plain_parser-0.1.4.dev10/tests/data/cli/resource_before_frid_walk/snippet.plain +1 -0
- plain_parser-0.1.4.dev10/tests/data/cli/resource_before_frid_walk/top.plain +14 -0
- plain_parser-0.1.4.dev10/tests/data/cli/shared_link_helper.plain +7 -0
- plain_parser-0.1.4.dev10/tests/data/cli/shared_link_top.plain +12 -0
- plain_parser-0.1.4.dev10/tests/data/cli/template_link/spec/top.plain +7 -0
- plain_parser-0.1.4.dev10/tests/data/cli/template_link/tpl/notes.md +1 -0
- plain_parser-0.1.4.dev10/tests/test_cli.py +695 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_loaders.py +32 -1
- plain_parser-0.1.4.dev9/PKG-INFO +0 -76
- plain_parser-0.1.4.dev9/README.md +0 -53
- plain_parser-0.1.4.dev9/src/plain_parser/cli.py +0 -117
- plain_parser-0.1.4.dev9/tests/test_cli.py +0 -329
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/.flake8 +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/.github/dependabot.yml +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/.github/workflows/lint-and-test.yml +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/.github/workflows/publish-dev-to-pypi.yml +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/.github/workflows/publish-to-pypi.yml +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/.gitignore +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/LICENSE +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/pyproject.toml +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/__init__.py +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/concept_utils.py +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/exceptions.py +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/graph.py +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/liquid_nodes.py +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/plain_file.py +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/src/plain_parser/plain_spec.py +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/__init__.py +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/conftest.py +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/acceptance_test_binary_resource.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/acceptance_test_resource.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/base64_resource.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/binary.bin +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/binary_resource.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/code_variable_conflict.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/code_variable_template.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/missing_resource.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/not_plain.md +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/notes.md +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/requires_binary_resource_base.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/requires_binary_resource_top.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/subdir/nested.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/undefined_concept.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/cli/valid.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/circular_imports_1.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/circular_imports_2.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/circular_imports_main.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/diamond_import_1.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/diamond_import_2.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/diamond_import_common.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/diamond_imports_main.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/import_with_functionalities.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/imports_module_with_functionalities.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/imports/non_existent_import.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/add_new_task_modal_specification.yaml +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/duplicate_specification_heading.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/empty_functional_specs_section.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/invalid_specification_order.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/missing_non_functional_requirements.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/no_functional_specs_section.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/plain_source_with_absolute_link.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/plain_source_with_missing_link.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/plain_source_with_url_link.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/task_list_ui_specification.yaml +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/task_manager_with_reference_links.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfile/without_non_functional_requirement.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_acceptance_tests.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_acceptance_tests_nondefined.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_defined_nondefined.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_defined_nondefined_2.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_definition.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_noconcepts.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_nonconcept.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_nondefined.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_redefinition.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_several_concepts.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/concept_validation_valid.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/cyclic_definitions.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/cyclic_definitions_across_import.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/cyclic_definitions_long.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/cyclic_definitions_sub_bullet.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_base.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_defaults_exported.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_example.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_imported_exported.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_inherited_reexported.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_malformed.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_missing_definition.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_missing_definition_example.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet_undefined.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_nested.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_nested_example.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_no_definitions.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_not_declared.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_not_declared_example.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_transitive_example.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_transitive_l1.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/exported_concepts_transitive_l2.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/plain_file_parser_with_comments.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/plain_file_with_comments_indented.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/regular_plain_source.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_defs.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_example.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_l1.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_l2.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_missing.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_module.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_partial.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/required_concepts_partial_duplicate.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/requires_without_definitions_example.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/topological_sort.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/plainfileparser/topological_sort_not_referenced.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/chain_base.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/chain_fork_top.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/chain_middle.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/chain_top.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/circular_requires_main.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/circular_requires_sub.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/diamond_requires_1.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/diamond_requires_2.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/diamond_requires_common.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/diamond_requires_main.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/independent_requires_1.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/independent_requires_2.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/independent_requires_main.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/invalid_ancestor_base.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/invalid_ancestor_top.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/non_existent_require.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/normal_requires_1.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/normal_requires_2.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/normal_requires_common.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/normal_requires_main.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/shared_export_above.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/shared_export_base.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/shared_export_middle.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/requires/shared_export_top.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/sample_base64_image.txt +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/simple.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/block_level_include.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/code_variables.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/header.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/implement.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/implement_2.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/malformed_tag.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/template_include.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/test_hardest_problem.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/data/templates/unterminated_output.plain +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_concept_utils.py +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_exceptions.py +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_graph.py +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_imports.py +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_plainfile.py +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_plainfileparser.py +0 -0
- {plain_parser-0.1.4.dev9 → plain_parser-0.1.4.dev10}/tests/test_plainspec.py +0 -0
- {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
|
|
40
|
-
"""Return the first
|
|
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
|
-
|
|
47
|
-
|
|
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 @@
|
|
|
1
|
+
notes
|
|
File without changes
|
|
@@ -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 @@
|
|
|
1
|
+
notes
|