codeplain 0.1.0__py3-none-any.whl
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.
- codeplain-0.1.0.dist-info/METADATA +142 -0
- codeplain-0.1.0.dist-info/RECORD +51 -0
- codeplain-0.1.0.dist-info/WHEEL +5 -0
- codeplain-0.1.0.dist-info/entry_points.txt +2 -0
- codeplain-0.1.0.dist-info/licenses/LICENSE +21 -0
- codeplain-0.1.0.dist-info/top_level.txt +36 -0
- codeplain_REST_api.py +370 -0
- config/__init__.py +2 -0
- config/system_config.yaml +27 -0
- file_utils.py +316 -0
- git_utils.py +304 -0
- hash_key.py +29 -0
- plain2code.py +218 -0
- plain2code_arguments.py +286 -0
- plain2code_console.py +107 -0
- plain2code_exceptions.py +45 -0
- plain2code_nodes.py +108 -0
- plain2code_read_config.py +74 -0
- plain2code_state.py +75 -0
- plain2code_utils.py +56 -0
- plain_spec.py +360 -0
- render_machine/actions/analyze_specification_ambiguity.py +50 -0
- render_machine/actions/base_action.py +19 -0
- render_machine/actions/commit_conformance_tests_changes.py +46 -0
- render_machine/actions/commit_implementation_code_changes.py +22 -0
- render_machine/actions/create_dist.py +26 -0
- render_machine/actions/exit_with_error.py +22 -0
- render_machine/actions/fix_conformance_test.py +121 -0
- render_machine/actions/fix_unit_tests.py +57 -0
- render_machine/actions/prepare_repositories.py +50 -0
- render_machine/actions/prepare_testing_environment.py +30 -0
- render_machine/actions/refactor_code.py +48 -0
- render_machine/actions/render_conformance_tests.py +169 -0
- render_machine/actions/render_functional_requirement.py +69 -0
- render_machine/actions/run_conformance_tests.py +44 -0
- render_machine/actions/run_unit_tests.py +38 -0
- render_machine/actions/summarize_conformance_tests.py +34 -0
- render_machine/code_renderer.py +50 -0
- render_machine/conformance_test_helpers.py +68 -0
- render_machine/implementation_code_helpers.py +20 -0
- render_machine/render_context.py +280 -0
- render_machine/render_types.py +36 -0
- render_machine/render_utils.py +92 -0
- render_machine/state_machine_config.py +408 -0
- render_machine/states.py +52 -0
- render_machine/triggers.py +27 -0
- standard_template_library/__init__.py +1 -0
- standard_template_library/golang-console-app-template.plain +36 -0
- standard_template_library/python-console-app-template.plain +32 -0
- standard_template_library/typescript-react-app-template.plain +22 -0
- system_config.py +49 -0
plain2code_nodes.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Mapping, Sequence, TextIO
|
|
3
|
+
|
|
4
|
+
from liquid2 import Environment, RenderContext, Template, TemplateNotFoundError
|
|
5
|
+
from liquid2.builtin import IncludeTag
|
|
6
|
+
from liquid2.builtin.tags.include_tag import IncludeNode
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Plain2CodeIncludeNode(IncludeNode):
|
|
10
|
+
def render_to_output(self, context: RenderContext, buffer: TextIO) -> int:
|
|
11
|
+
"""Render the node to the output buffer."""
|
|
12
|
+
name = self.name.evaluate(context)
|
|
13
|
+
whitespaces = 0
|
|
14
|
+
is_comment = False
|
|
15
|
+
i = self.token.start
|
|
16
|
+
while self.token.source[i] != "\n" and i >= 0:
|
|
17
|
+
if self.token.source[i] == " ":
|
|
18
|
+
whitespaces += 1
|
|
19
|
+
elif self.token.source[i] == ">":
|
|
20
|
+
is_comment = True
|
|
21
|
+
break
|
|
22
|
+
else:
|
|
23
|
+
whitespaces = 0
|
|
24
|
+
i -= 1
|
|
25
|
+
|
|
26
|
+
if is_comment:
|
|
27
|
+
return buffer.write(str(self))
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
template = context.env.get_template(str(name), context=context, tag=self.tag, whitespaces=whitespaces)
|
|
31
|
+
except TemplateNotFoundError as err:
|
|
32
|
+
err.token = self.name.token
|
|
33
|
+
err.template_name = context.template.full_name()
|
|
34
|
+
raise
|
|
35
|
+
|
|
36
|
+
namespace: dict[str, object] = dict(arg.evaluate(context) for arg in self.args)
|
|
37
|
+
|
|
38
|
+
character_count = 0
|
|
39
|
+
|
|
40
|
+
with context.extend(namespace, template=template):
|
|
41
|
+
if self.var:
|
|
42
|
+
val = self.var.evaluate(context)
|
|
43
|
+
key = self.alias or template.name.split(".")[0]
|
|
44
|
+
|
|
45
|
+
if isinstance(val, Sequence) and not isinstance(val, str):
|
|
46
|
+
context.raise_for_loop_limit(len(val))
|
|
47
|
+
for itm in val:
|
|
48
|
+
namespace[key] = itm
|
|
49
|
+
character_count += template.render_with_context(context, buffer, partial=True)
|
|
50
|
+
else:
|
|
51
|
+
namespace[key] = val
|
|
52
|
+
character_count = template.render_with_context(context, buffer, partial=True)
|
|
53
|
+
else:
|
|
54
|
+
|
|
55
|
+
character_count = template.render_with_context(context, buffer, partial=True)
|
|
56
|
+
|
|
57
|
+
return character_count
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Plain2CodeIncludeTag(IncludeTag):
|
|
61
|
+
node_class = Plain2CodeIncludeNode
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Plain2CodeLoaderMixin:
|
|
65
|
+
def __init__(self, *args, **kwargs):
|
|
66
|
+
if not hasattr(self, "get_source"):
|
|
67
|
+
raise NotImplementedError("Class must implement get_source")
|
|
68
|
+
super().__init__(*args, **kwargs)
|
|
69
|
+
|
|
70
|
+
def load(
|
|
71
|
+
self,
|
|
72
|
+
env: Environment,
|
|
73
|
+
name: str,
|
|
74
|
+
*,
|
|
75
|
+
globals: Mapping[str, object] | None = None,
|
|
76
|
+
context: RenderContext | None = None,
|
|
77
|
+
**kwargs: object,
|
|
78
|
+
) -> Template:
|
|
79
|
+
"""
|
|
80
|
+
Find and parse template source code.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
env: The `Environment` attempting to load the template source text.
|
|
84
|
+
name: A name or identifier for a template's source text.
|
|
85
|
+
globals: A mapping of render context variables attached to the
|
|
86
|
+
resulting template.
|
|
87
|
+
context: An optional render context that can be used to narrow the template
|
|
88
|
+
source search space.
|
|
89
|
+
kwargs: Arbitrary arguments that can be used to narrow the template source
|
|
90
|
+
search space.
|
|
91
|
+
"""
|
|
92
|
+
source, full_name, uptodate, matter = self.get_source(env, name, context=context, **kwargs)
|
|
93
|
+
whitespaces = kwargs.get("whitespaces", 0)
|
|
94
|
+
assert isinstance(whitespaces, int)
|
|
95
|
+
source = source.rstrip().replace("\n", "\n" + " " * whitespaces)
|
|
96
|
+
|
|
97
|
+
path = Path(full_name)
|
|
98
|
+
|
|
99
|
+
template = env.from_string(
|
|
100
|
+
source,
|
|
101
|
+
name=path.name,
|
|
102
|
+
path=path,
|
|
103
|
+
globals=globals,
|
|
104
|
+
overlay_data=matter,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
template.uptodate = uptodate
|
|
108
|
+
return template
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from argparse import ArgumentParser, Namespace
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from plain2code_console import console
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_config(config_file: str) -> Dict[str, Any]:
|
|
11
|
+
"""Load configuration from YAML file."""
|
|
12
|
+
try:
|
|
13
|
+
with open(config_file, "r") as f:
|
|
14
|
+
return yaml.safe_load(f)
|
|
15
|
+
except Exception as e:
|
|
16
|
+
console.error(f"Error loading config file: {e}. Please check the config file path and the config file content.")
|
|
17
|
+
raise e
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_config(config: Dict[str, Any], parser: ArgumentParser) -> None:
|
|
21
|
+
"""Validate the configuration against the parser."""
|
|
22
|
+
actions = [action.dest for action in parser._actions]
|
|
23
|
+
for action in parser._actions:
|
|
24
|
+
if hasattr(action, "option_strings"):
|
|
25
|
+
actions.extend(opt.lstrip("-") for opt in action.option_strings)
|
|
26
|
+
|
|
27
|
+
for key in config.keys():
|
|
28
|
+
if key not in actions:
|
|
29
|
+
raise KeyError(f"Invalid configuration key: {key}")
|
|
30
|
+
return config
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_args_from_config(config_file: str, parser: ArgumentParser) -> Namespace:
|
|
34
|
+
"""
|
|
35
|
+
Read configuration from YAML file and return args compatible with plain2code_arguments.py.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config_file: Path to the YAML config file
|
|
39
|
+
Returns:
|
|
40
|
+
Namespace object with arguments as defined in plain2code_arguments.py
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
FileNotFoundError: If config file doesn't exist
|
|
44
|
+
KeyError: If argument not found in parser arguments
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
args = Namespace()
|
|
48
|
+
|
|
49
|
+
if config_file == "config.yaml":
|
|
50
|
+
if not os.path.exists(config_file):
|
|
51
|
+
console.info(f"Default config file {config_file} not found. No config file is read.")
|
|
52
|
+
return args
|
|
53
|
+
|
|
54
|
+
# Load config
|
|
55
|
+
config = load_config(config_file)
|
|
56
|
+
config = validate_config(config, parser)
|
|
57
|
+
|
|
58
|
+
for action in parser._actions:
|
|
59
|
+
# Create a list of possible config keys for this argument
|
|
60
|
+
possible_keys = [action.dest]
|
|
61
|
+
if hasattr(action, "option_strings"):
|
|
62
|
+
# Add all option strings without leading dashes
|
|
63
|
+
possible_keys.extend(opt.lstrip("-") for opt in action.option_strings)
|
|
64
|
+
|
|
65
|
+
# Handling multi-named arguments like --verbose and -v
|
|
66
|
+
config_value = None
|
|
67
|
+
for key in possible_keys:
|
|
68
|
+
if key in config:
|
|
69
|
+
config_value = config[key]
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
if config_value is not None:
|
|
73
|
+
setattr(args, action.dest, config_value)
|
|
74
|
+
return args
|
plain2code_state.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Contains all state and context information we need for the rendering process."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from plain2code_console import console
|
|
9
|
+
|
|
10
|
+
CONFORMANCE_TESTS_DEFINITION_FILE_NAME = "conformance_tests.json"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConformanceTestsUtils:
|
|
14
|
+
"""Manages the state of conformance tests."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
conformance_tests_folder: str,
|
|
19
|
+
conformance_tests_definition_file_name: str,
|
|
20
|
+
verbose: bool,
|
|
21
|
+
):
|
|
22
|
+
self.conformance_tests_folder = conformance_tests_folder
|
|
23
|
+
self.full_conformance_tests_definition_file_name = os.path.join(
|
|
24
|
+
self.conformance_tests_folder, conformance_tests_definition_file_name
|
|
25
|
+
)
|
|
26
|
+
self.verbose = verbose
|
|
27
|
+
|
|
28
|
+
def get_conformance_tests_json(self):
|
|
29
|
+
try:
|
|
30
|
+
with open(self.full_conformance_tests_definition_file_name, "r") as f:
|
|
31
|
+
return json.load(f)
|
|
32
|
+
except FileNotFoundError:
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
def dump_conformance_tests_json(self, conformance_tests_json: dict):
|
|
36
|
+
"""Dump the conformance tests definition to the file."""
|
|
37
|
+
if os.path.exists(self.conformance_tests_folder):
|
|
38
|
+
if self.verbose:
|
|
39
|
+
console.info(
|
|
40
|
+
f"Storing conformance tests definition to {self.full_conformance_tests_definition_file_name}"
|
|
41
|
+
)
|
|
42
|
+
with open(self.full_conformance_tests_definition_file_name, "w") as f:
|
|
43
|
+
json.dump(conformance_tests_json, f, indent=4)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RunState:
|
|
47
|
+
"""Contains information about the identifiable state of the rendering process."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, spec_filename: str, replay_with: Optional[str] = None):
|
|
50
|
+
self.replay: bool = replay_with is not None
|
|
51
|
+
if replay_with:
|
|
52
|
+
self.render_id: str = replay_with
|
|
53
|
+
else:
|
|
54
|
+
self.render_id: str = str(uuid.uuid4())
|
|
55
|
+
self.spec_filename: str = spec_filename
|
|
56
|
+
self.call_count: int = 0
|
|
57
|
+
self.unittest_batch_id: int = 0
|
|
58
|
+
self.frid_render_anaysis: dict[str, str] = {}
|
|
59
|
+
|
|
60
|
+
def increment_call_count(self):
|
|
61
|
+
self.call_count += 1
|
|
62
|
+
|
|
63
|
+
def increment_unittest_batch_id(self):
|
|
64
|
+
self.unittest_batch_id += 1
|
|
65
|
+
|
|
66
|
+
def add_rendering_analysis_for_frid(self, frid, rendering_analysis) -> None:
|
|
67
|
+
self.frid_render_anaysis[frid] = rendering_analysis
|
|
68
|
+
|
|
69
|
+
def to_dict(self):
|
|
70
|
+
return {
|
|
71
|
+
"render_id": self.render_id,
|
|
72
|
+
"call_count": self.call_count,
|
|
73
|
+
"replay": self.replay,
|
|
74
|
+
"spec_filename": self.spec_filename,
|
|
75
|
+
}
|
plain2code_utils.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import plain_spec
|
|
5
|
+
from plain2code_console import console
|
|
6
|
+
|
|
7
|
+
AMBIGUITY_CAUSES = {
|
|
8
|
+
"reference_resource_ambiguity": "Ambiguity is in the reference resources",
|
|
9
|
+
"definition_ambiguity": "Ambiguity is in the definitions",
|
|
10
|
+
"non_functional_requirement_ambiguity": "Ambiguity is in the non-functional requirements",
|
|
11
|
+
"functional_requirement_ambiguity": "Ambiguity is in the functional requirements",
|
|
12
|
+
"other": "Ambiguity in the other parts of the specification",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RetryOnlyFilter(logging.Filter):
|
|
17
|
+
def filter(self, record):
|
|
18
|
+
# Allow all logs with level > DEBUG (i.e., INFO and above)
|
|
19
|
+
if record.levelno > logging.DEBUG:
|
|
20
|
+
return True
|
|
21
|
+
# For DEBUG logs, only allow if message matches retry-related patterns
|
|
22
|
+
msg = record.getMessage().lower()
|
|
23
|
+
return (
|
|
24
|
+
"retrying due to" in msg
|
|
25
|
+
or "raising timeout error" in msg
|
|
26
|
+
or "raising connection error" in msg
|
|
27
|
+
or "encountered exception" in msg
|
|
28
|
+
or "retrying request" in msg
|
|
29
|
+
or "retry left" in msg
|
|
30
|
+
or "1 retry left" in msg
|
|
31
|
+
or "retries left" in msg
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def print_dry_run_output(plain_source_tree: dict, render_range: Optional[list[str]]):
|
|
36
|
+
frid = plain_spec.get_first_frid(plain_source_tree)
|
|
37
|
+
|
|
38
|
+
while frid is not None:
|
|
39
|
+
is_inside_range = render_range is None or frid in render_range
|
|
40
|
+
|
|
41
|
+
if is_inside_range:
|
|
42
|
+
specifications, _ = plain_spec.get_specifications_for_frid(plain_source_tree, frid)
|
|
43
|
+
functional_requirement_text = specifications[plain_spec.FUNCTIONAL_REQUIREMENTS][-1]
|
|
44
|
+
console.info("\n-------------------------------------")
|
|
45
|
+
console.info(f"Rendering functional requirement {frid}")
|
|
46
|
+
console.info(f"[b]{functional_requirement_text}[/b]")
|
|
47
|
+
console.info("-------------------------------------\n")
|
|
48
|
+
if plain_spec.ACCEPTANCE_TESTS in specifications:
|
|
49
|
+
for i, acceptance_test in enumerate(specifications[plain_spec.ACCEPTANCE_TESTS], 1):
|
|
50
|
+
console.info(f"\nGenerating acceptance test #{i}:\n\n{acceptance_test}")
|
|
51
|
+
else:
|
|
52
|
+
console.info("\n-------------------------------------")
|
|
53
|
+
console.info(f"Skipping rendering iteration: {frid}")
|
|
54
|
+
console.info("-------------------------------------\n")
|
|
55
|
+
|
|
56
|
+
frid = plain_spec.get_next_frid(plain_source_tree, frid)
|
plain_spec.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from liquid2.filter import with_context
|
|
7
|
+
|
|
8
|
+
DEFINITIONS = "Definitions:"
|
|
9
|
+
NON_FUNCTIONAL_REQUIREMENTS = "Non-Functional Requirements:"
|
|
10
|
+
TEST_REQUIREMENTS = "Test Requirements:"
|
|
11
|
+
FUNCTIONAL_REQUIREMENTS = "Functional Requirements:"
|
|
12
|
+
ACCEPTANCE_TESTS = "acceptance_tests"
|
|
13
|
+
ACCEPTANCE_TEST_HEADING = "Acceptance Tests:"
|
|
14
|
+
|
|
15
|
+
ALLOWED_SPECIFICATION_HEADINGS = [
|
|
16
|
+
DEFINITIONS,
|
|
17
|
+
NON_FUNCTIONAL_REQUIREMENTS,
|
|
18
|
+
TEST_REQUIREMENTS,
|
|
19
|
+
FUNCTIONAL_REQUIREMENTS,
|
|
20
|
+
ACCEPTANCE_TEST_HEADING,
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InvalidLiquidVariableName(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def collect_specification_linked_resources(specification, specification_heading, linked_resources_list):
|
|
29
|
+
linked_resources = []
|
|
30
|
+
if "linked_resources" in specification:
|
|
31
|
+
linked_resources.extend(specification["linked_resources"])
|
|
32
|
+
|
|
33
|
+
for resource in linked_resources:
|
|
34
|
+
resource_found = False
|
|
35
|
+
for resource_map in linked_resources_list:
|
|
36
|
+
if resource["text"] == resource_map["text"]:
|
|
37
|
+
if resource["target"] != resource_map["target"]:
|
|
38
|
+
raise Exception(
|
|
39
|
+
f"The file {resource['target']} is linked to multiple linked resources with the same text: {resource['text']}"
|
|
40
|
+
)
|
|
41
|
+
elif resource["target"] == resource_map["target"]:
|
|
42
|
+
if resource["text"] != resource_map["text"]:
|
|
43
|
+
raise Exception(
|
|
44
|
+
f"The linked resource {resource['text']} is linked to multiple files: {resource_map['target']}"
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
if resource_found:
|
|
50
|
+
raise Exception(
|
|
51
|
+
"Duplicate linked resource found: " + resource["text"] + " (" + resource["target"] + ")"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
resource_found = True
|
|
55
|
+
resource_map["sections"].append(specification_heading)
|
|
56
|
+
|
|
57
|
+
if not resource_found:
|
|
58
|
+
linked_resources_list.append(
|
|
59
|
+
{"text": resource["text"], "target": resource["target"], "sections": [specification_heading]}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def collect_linked_resources_in_section(
|
|
64
|
+
section, linked_resources_list, specifications_list, include_acceptance_tests, frid=Optional[str]
|
|
65
|
+
):
|
|
66
|
+
# When should we collect specification headings in the current section. Either when one of the following holds true:
|
|
67
|
+
# - frid wasn't specified
|
|
68
|
+
# - section has no ID (and that's exactly when it's the root section)
|
|
69
|
+
# - section has ID, frid was specified and the specified frid inside the current section tree
|
|
70
|
+
should_collect_specification_headings_in_current_section = (
|
|
71
|
+
frid is None or "ID" not in section or frid.startswith(section["ID"])
|
|
72
|
+
)
|
|
73
|
+
if should_collect_specification_headings_in_current_section:
|
|
74
|
+
specifications_to_collect = list(set([DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS]))
|
|
75
|
+
if specifications_list:
|
|
76
|
+
specifications_to_collect = list(set(specifications_to_collect) & set(specifications_list))
|
|
77
|
+
for specification_heading in specifications_to_collect:
|
|
78
|
+
if specification_heading in section:
|
|
79
|
+
for requirement in section[specification_heading]:
|
|
80
|
+
collect_specification_linked_resources(requirement, specification_heading, linked_resources_list)
|
|
81
|
+
|
|
82
|
+
if FUNCTIONAL_REQUIREMENTS in section and (
|
|
83
|
+
not specifications_list or FUNCTIONAL_REQUIREMENTS in specifications_list
|
|
84
|
+
):
|
|
85
|
+
functional_requirement_count = 0
|
|
86
|
+
for requirement in section[FUNCTIONAL_REQUIREMENTS]:
|
|
87
|
+
collect_specification_linked_resources(requirement, FUNCTIONAL_REQUIREMENTS, linked_resources_list)
|
|
88
|
+
|
|
89
|
+
functional_requirement_count += 1
|
|
90
|
+
section_id = section.get("ID", None)
|
|
91
|
+
current_frid = get_current_frid(section_id, functional_requirement_count)
|
|
92
|
+
|
|
93
|
+
if ACCEPTANCE_TESTS in requirement and include_acceptance_tests and (frid is None or frid == current_frid):
|
|
94
|
+
for acceptance_test in requirement[ACCEPTANCE_TESTS]:
|
|
95
|
+
collect_specification_linked_resources(
|
|
96
|
+
acceptance_test, FUNCTIONAL_REQUIREMENTS, linked_resources_list
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if frid is not None and current_frid == frid:
|
|
100
|
+
# here, we rely on the fact that FRIDs are incrementing. And if we have reached the current FRID, we should
|
|
101
|
+
# not collect any FRIDs after the current one.
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
if "sections" in section:
|
|
105
|
+
for subsection in section["sections"]:
|
|
106
|
+
if collect_linked_resources_in_section(
|
|
107
|
+
subsection, linked_resources_list, specifications_list, include_acceptance_tests, frid
|
|
108
|
+
):
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# TODO: check if this function can be refactored to return the list of linked resources instead of modifying it in place
|
|
115
|
+
def collect_linked_resources(
|
|
116
|
+
plain_source_tree, linked_resources_list, specifications_list, include_acceptance_tests, frid=None
|
|
117
|
+
):
|
|
118
|
+
|
|
119
|
+
if not isinstance(plain_source_tree, dict):
|
|
120
|
+
raise ValueError("[plain_source_tree must be a dictionary.")
|
|
121
|
+
|
|
122
|
+
if frid is not None:
|
|
123
|
+
functional_requirements = get_frids(plain_source_tree)
|
|
124
|
+
if frid not in functional_requirements:
|
|
125
|
+
raise ValueError(f"frid {frid} does not exist.")
|
|
126
|
+
|
|
127
|
+
result = collect_linked_resources_in_section(
|
|
128
|
+
plain_source_tree, linked_resources_list, specifications_list, include_acceptance_tests, frid
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Sort linked_resources_list by the "text" field
|
|
132
|
+
linked_resources_list.sort(key=lambda x: x["text"])
|
|
133
|
+
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_frids(plain_source_tree):
|
|
138
|
+
if FUNCTIONAL_REQUIREMENTS in plain_source_tree:
|
|
139
|
+
for functional_requirement_id in range(1, len(plain_source_tree[FUNCTIONAL_REQUIREMENTS]) + 1):
|
|
140
|
+
if "ID" in plain_source_tree:
|
|
141
|
+
yield plain_source_tree["ID"] + "." + str(functional_requirement_id)
|
|
142
|
+
else:
|
|
143
|
+
yield str(functional_requirement_id)
|
|
144
|
+
|
|
145
|
+
if "sections" in plain_source_tree:
|
|
146
|
+
for section in plain_source_tree["sections"]:
|
|
147
|
+
yield from get_frids(section)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_first_frid(plain_source_tree):
|
|
151
|
+
return next(get_frids(plain_source_tree), None)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_current_frid(section_id: Optional[str], functional_requirement_count: int) -> str:
|
|
155
|
+
if section_id is None:
|
|
156
|
+
return str(functional_requirement_count)
|
|
157
|
+
else:
|
|
158
|
+
return section_id + "." + str(functional_requirement_count)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def get_next_frid(plain_source_tree, frid):
|
|
162
|
+
functional_requirements = get_frids(plain_source_tree)
|
|
163
|
+
for temp_frid in functional_requirements:
|
|
164
|
+
if temp_frid == frid:
|
|
165
|
+
return next(functional_requirements, None)
|
|
166
|
+
|
|
167
|
+
raise Exception(f"Functional requirement {frid} does not exist.")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_previous_frid(plain_source_tree, frid):
|
|
171
|
+
previous_frid = None
|
|
172
|
+
for temp_frid in get_frids(plain_source_tree):
|
|
173
|
+
if temp_frid == frid:
|
|
174
|
+
return previous_frid
|
|
175
|
+
|
|
176
|
+
previous_frid = temp_frid
|
|
177
|
+
|
|
178
|
+
raise Exception(f"Functional requirement {frid} does not exist.")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def get_specification_item_markdown(specification_item, code_variables, replace_code_variables):
|
|
182
|
+
markdown = specification_item["markdown"]
|
|
183
|
+
if "code_variables" in specification_item:
|
|
184
|
+
for code_variable in specification_item["code_variables"]:
|
|
185
|
+
if code_variable["name"] in code_variables:
|
|
186
|
+
if code_variables[code_variable["name"]] != code_variable["value"]:
|
|
187
|
+
raise Exception(
|
|
188
|
+
f"Code variable {code_variable['name']} has multiple values: {code_variables[code_variable['name']]} and {code_variable['value']}"
|
|
189
|
+
)
|
|
190
|
+
else:
|
|
191
|
+
code_variables[code_variable["name"]] = code_variable["value"]
|
|
192
|
+
|
|
193
|
+
if replace_code_variables:
|
|
194
|
+
markdown = markdown.replace(f"{{{{ {code_variable['name']} }}}}", code_variable["value"])
|
|
195
|
+
|
|
196
|
+
return markdown
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_specifications_from_plain_source_tree(
|
|
200
|
+
frid,
|
|
201
|
+
plain_source_tree,
|
|
202
|
+
definitions,
|
|
203
|
+
non_functional_requirements,
|
|
204
|
+
test_requirements,
|
|
205
|
+
functional_requirements,
|
|
206
|
+
acceptance_tests,
|
|
207
|
+
code_variables,
|
|
208
|
+
replace_code_variables,
|
|
209
|
+
section_id=None,
|
|
210
|
+
):
|
|
211
|
+
return_frid = None
|
|
212
|
+
if FUNCTIONAL_REQUIREMENTS in plain_source_tree and len(plain_source_tree[FUNCTIONAL_REQUIREMENTS]) > 0:
|
|
213
|
+
functional_requirement_count = 0
|
|
214
|
+
for functional_requirement in plain_source_tree[FUNCTIONAL_REQUIREMENTS]:
|
|
215
|
+
functional_requirement_count += 1
|
|
216
|
+
if section_id is None:
|
|
217
|
+
current_frid = str(functional_requirement_count)
|
|
218
|
+
else:
|
|
219
|
+
current_frid = section_id + "." + str(functional_requirement_count)
|
|
220
|
+
|
|
221
|
+
functional_requirements.append(
|
|
222
|
+
get_specification_item_markdown(functional_requirement, code_variables, replace_code_variables)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if current_frid == frid:
|
|
226
|
+
if ACCEPTANCE_TESTS in functional_requirement:
|
|
227
|
+
for acceptance_test in functional_requirement[ACCEPTANCE_TESTS]:
|
|
228
|
+
acceptance_tests.append(
|
|
229
|
+
get_specification_item_markdown(acceptance_test, code_variables, replace_code_variables)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return_frid = current_frid
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
if "sections" in plain_source_tree:
|
|
236
|
+
for section in plain_source_tree["sections"]:
|
|
237
|
+
sub_frid = get_specifications_from_plain_source_tree(
|
|
238
|
+
frid,
|
|
239
|
+
section,
|
|
240
|
+
definitions,
|
|
241
|
+
non_functional_requirements,
|
|
242
|
+
test_requirements,
|
|
243
|
+
functional_requirements,
|
|
244
|
+
acceptance_tests,
|
|
245
|
+
code_variables,
|
|
246
|
+
replace_code_variables,
|
|
247
|
+
section["ID"],
|
|
248
|
+
)
|
|
249
|
+
if sub_frid is not None:
|
|
250
|
+
return_frid = sub_frid
|
|
251
|
+
break
|
|
252
|
+
|
|
253
|
+
if return_frid is not None:
|
|
254
|
+
if DEFINITIONS in plain_source_tree and plain_source_tree[DEFINITIONS] is not None:
|
|
255
|
+
definitions[0:0] = [
|
|
256
|
+
get_specification_item_markdown(specification, code_variables, replace_code_variables)
|
|
257
|
+
for specification in plain_source_tree[DEFINITIONS]
|
|
258
|
+
]
|
|
259
|
+
if (
|
|
260
|
+
NON_FUNCTIONAL_REQUIREMENTS in plain_source_tree
|
|
261
|
+
and plain_source_tree[NON_FUNCTIONAL_REQUIREMENTS] is not None
|
|
262
|
+
):
|
|
263
|
+
non_functional_requirements[0:0] = [
|
|
264
|
+
get_specification_item_markdown(specification, code_variables, replace_code_variables)
|
|
265
|
+
for specification in plain_source_tree[NON_FUNCTIONAL_REQUIREMENTS]
|
|
266
|
+
]
|
|
267
|
+
if TEST_REQUIREMENTS in plain_source_tree and plain_source_tree[TEST_REQUIREMENTS] is not None:
|
|
268
|
+
test_requirements[0:0] = [
|
|
269
|
+
get_specification_item_markdown(specification, code_variables, replace_code_variables)
|
|
270
|
+
for specification in plain_source_tree[TEST_REQUIREMENTS]
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
return return_frid
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def get_specifications_for_frid(plain_source_tree, frid, replace_code_variables=True):
|
|
277
|
+
definitions = []
|
|
278
|
+
non_functional_requirements = []
|
|
279
|
+
test_requirements = []
|
|
280
|
+
functional_requirements = []
|
|
281
|
+
acceptance_tests = []
|
|
282
|
+
|
|
283
|
+
code_variables = {}
|
|
284
|
+
|
|
285
|
+
result = get_specifications_from_plain_source_tree(
|
|
286
|
+
frid,
|
|
287
|
+
plain_source_tree,
|
|
288
|
+
definitions,
|
|
289
|
+
non_functional_requirements,
|
|
290
|
+
test_requirements,
|
|
291
|
+
functional_requirements,
|
|
292
|
+
acceptance_tests,
|
|
293
|
+
code_variables,
|
|
294
|
+
replace_code_variables,
|
|
295
|
+
)
|
|
296
|
+
if result is None:
|
|
297
|
+
raise Exception(f"Functional requirement {frid} does not exist.")
|
|
298
|
+
|
|
299
|
+
specifications = {
|
|
300
|
+
DEFINITIONS: definitions,
|
|
301
|
+
NON_FUNCTIONAL_REQUIREMENTS: non_functional_requirements,
|
|
302
|
+
TEST_REQUIREMENTS: test_requirements,
|
|
303
|
+
FUNCTIONAL_REQUIREMENTS: functional_requirements,
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if acceptance_tests:
|
|
307
|
+
specifications[ACCEPTANCE_TESTS] = acceptance_tests
|
|
308
|
+
|
|
309
|
+
if code_variables:
|
|
310
|
+
return specifications, code_variables
|
|
311
|
+
else:
|
|
312
|
+
return specifications, None
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@with_context
|
|
316
|
+
def code_variable_liquid_filter(value, *, context):
|
|
317
|
+
if len(context.scope) == 0:
|
|
318
|
+
raise Exception("Invalid use of code_variable filter!")
|
|
319
|
+
|
|
320
|
+
if "code_variables" in context.globals:
|
|
321
|
+
code_variables = context.globals["code_variables"]
|
|
322
|
+
|
|
323
|
+
variable = next(iter(context.scope.items()))
|
|
324
|
+
|
|
325
|
+
unique_str = uuid.uuid4().hex
|
|
326
|
+
|
|
327
|
+
code_variables[unique_str] = {variable[0]: value}
|
|
328
|
+
|
|
329
|
+
return unique_str
|
|
330
|
+
else:
|
|
331
|
+
|
|
332
|
+
return value
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@with_context
|
|
336
|
+
def prohibited_chars_liquid_filter(value, prohibited_chars, *, context):
|
|
337
|
+
if not isinstance(value, str):
|
|
338
|
+
value = str(value)
|
|
339
|
+
|
|
340
|
+
if len(context.scope) == 0:
|
|
341
|
+
raise Exception("Invalid use of prohibited_chars filter!")
|
|
342
|
+
|
|
343
|
+
variable = next(iter(context.scope.items()))
|
|
344
|
+
variable_name = variable[0]
|
|
345
|
+
|
|
346
|
+
for char in prohibited_chars:
|
|
347
|
+
if char in value:
|
|
348
|
+
raise InvalidLiquidVariableName(
|
|
349
|
+
f"'{char}' is not a valid character for variable '{variable_name}' (value: '{value}')."
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
return value
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def hash_text(text):
|
|
356
|
+
return hashlib.sha256(text.encode()).hexdigest()
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def get_hash_value(specifications):
|
|
360
|
+
return hash_text(json.dumps(specifications, indent=4))
|