syncspec 0.1.0__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 (56) hide show
  1. syncspec-0.1.0/LICENSE +21 -0
  2. syncspec-0.1.0/PKG-INFO +41 -0
  3. syncspec-0.1.0/README.md +23 -0
  4. syncspec-0.1.0/pyproject.toml +32 -0
  5. syncspec-0.1.0/setup.cfg +4 -0
  6. syncspec-0.1.0/src/syncspec/__init__.py +6 -0
  7. syncspec-0.1.0/src/syncspec/block.py +11 -0
  8. syncspec-0.1.0/src/syncspec/combine_errors_context.py +6 -0
  9. syncspec-0.1.0/src/syncspec/combine_nodes.py +19 -0
  10. syncspec-0.1.0/src/syncspec/combine_nodes_context.py +7 -0
  11. syncspec-0.1.0/src/syncspec/combine_strings.py +9 -0
  12. syncspec-0.1.0/src/syncspec/combine_strings_context.py +6 -0
  13. syncspec-0.1.0/src/syncspec/create_blocks.py +81 -0
  14. syncspec-0.1.0/src/syncspec/create_blocks_context.py +14 -0
  15. syncspec-0.1.0/src/syncspec/edge.py +8 -0
  16. syncspec-0.1.0/src/syncspec/file.py +6 -0
  17. syncspec-0.1.0/src/syncspec/fragment.py +7 -0
  18. syncspec-0.1.0/src/syncspec/fragment_text.py +30 -0
  19. syncspec-0.1.0/src/syncspec/fragment_text_context.py +8 -0
  20. syncspec-0.1.0/src/syncspec/function.py +12 -0
  21. syncspec-0.1.0/src/syncspec/graph_edges.py +20 -0
  22. syncspec-0.1.0/src/syncspec/graph_edges_context.py +7 -0
  23. syncspec-0.1.0/src/syncspec/import_block.py +95 -0
  24. syncspec-0.1.0/src/syncspec/import_block_context.py +8 -0
  25. syncspec-0.1.0/src/syncspec/include_block.py +65 -0
  26. syncspec-0.1.0/src/syncspec/include_block_context.py +8 -0
  27. syncspec-0.1.0/src/syncspec/node.py +8 -0
  28. syncspec-0.1.0/src/syncspec/production.py +20 -0
  29. syncspec-0.1.0/src/syncspec/source_block.py +65 -0
  30. syncspec-0.1.0/src/syncspec/source_block_context.py +8 -0
  31. syncspec-0.1.0/src/syncspec/string.py +7 -0
  32. syncspec-0.1.0/src/syncspec/syncspec_list.py +29 -0
  33. syncspec-0.1.0/src/syncspec/syncspec_list_context.py +9 -0
  34. syncspec-0.1.0/src/syncspec/syncspec_text.py +96 -0
  35. syncspec-0.1.0/src/syncspec/syncspec_text_context.py +11 -0
  36. syncspec-0.1.0/src/syncspec/text.py +6 -0
  37. syncspec-0.1.0/src/syncspec/utilities.py +6 -0
  38. syncspec-0.1.0/src/syncspec/validate_text.py +91 -0
  39. syncspec-0.1.0/src/syncspec/validate_text_context.py +8 -0
  40. syncspec-0.1.0/src/syncspec/validated_text.py +6 -0
  41. syncspec-0.1.0/src/syncspec.egg-info/PKG-INFO +41 -0
  42. syncspec-0.1.0/src/syncspec.egg-info/SOURCES.txt +54 -0
  43. syncspec-0.1.0/src/syncspec.egg-info/dependency_links.txt +1 -0
  44. syncspec-0.1.0/src/syncspec.egg-info/requires.txt +6 -0
  45. syncspec-0.1.0/src/syncspec.egg-info/top_level.txt +1 -0
  46. syncspec-0.1.0/tests/test_combine_nodes.py +25 -0
  47. syncspec-0.1.0/tests/test_combine_strings.py +28 -0
  48. syncspec-0.1.0/tests/test_create_blocks.py +69 -0
  49. syncspec-0.1.0/tests/test_fragment_text.py +77 -0
  50. syncspec-0.1.0/tests/test_function.py +22 -0
  51. syncspec-0.1.0/tests/test_graph_edges.py +24 -0
  52. syncspec-0.1.0/tests/test_import_block.py +78 -0
  53. syncspec-0.1.0/tests/test_include_block.py +41 -0
  54. syncspec-0.1.0/tests/test_source_block.py +54 -0
  55. syncspec-0.1.0/tests/test_syncspec_text.py +41 -0
  56. syncspec-0.1.0/tests/test_validate_text.py +83 -0
syncspec-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kim Jarvis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.4
2
+ Name: syncspec
3
+ Version: 0.1.0
4
+ Summary: Transclusion for Spec Driven Development
5
+ Author-email: Kim Jarvis <kim.jarvis@tpfsystems.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kimjarvis/syncspec
8
+ Project-URL: Issues, https://github.com/kimjarvis/syncspec/issues
9
+ Requires-Python: >=3.6
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: jinja2>=3.0.0
13
+ Requires-Dist: networkx>=2.5
14
+ Requires-Dist: pydot>=1.4.2
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=6.0.0; extra == "dev"
17
+ Dynamic: license-file
18
+
19
+ # syncspec
20
+
21
+ Transclusion for Spec Driven Development
22
+
23
+ # Install
24
+
25
+ ```bash
26
+ pip install syncspec
27
+ ```
28
+
29
+ # Command line usage
30
+
31
+ ```bash
32
+ syncspec -i specs/
33
+ ```
34
+
35
+ # Python interface
36
+
37
+ ```python
38
+ from syncspec import syncspec
39
+
40
+ syncspec("specs/")
41
+ ```
@@ -0,0 +1,23 @@
1
+ # syncspec
2
+
3
+ Transclusion for Spec Driven Development
4
+
5
+ # Install
6
+
7
+ ```bash
8
+ pip install syncspec
9
+ ```
10
+
11
+ # Command line usage
12
+
13
+ ```bash
14
+ syncspec -i specs/
15
+ ```
16
+
17
+ # Python interface
18
+
19
+ ```python
20
+ from syncspec import syncspec
21
+
22
+ syncspec("specs/")
23
+ ```
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "syncspec"
7
+ version = "0.1.0"
8
+ description = "Transclusion for Spec Driven Development"
9
+ readme = "README.md"
10
+ authors = [
11
+ {name = "Kim Jarvis", email = "kim.jarvis@tpfsystems.com"}
12
+ ]
13
+ license = {text = "MIT"}
14
+ requires-python = ">=3.6"
15
+ dependencies = [
16
+ "jinja2>=3.0.0", # Compatible with Python 3.6+
17
+ "networkx>=2.5", # networkx 3.x requires Python >=3.8, so use older version
18
+ "pydot>=1.4.2", # Compatible with Python 3.6+
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ dev = [
23
+ "pytest>=6.0.0", # pytest 9.x requires Python >=3.8
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/kimjarvis/syncspec"
28
+ Issues = "https://github.com/kimjarvis/syncspec/issues"
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+ include = ["syncspec*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """SyncSpec - Your package description"""
2
+
3
+ from .function import syncspec
4
+
5
+ __all__ = ['syncspec']
6
+ __version__ = "0.1.0"
@@ -0,0 +1,11 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict, Any, Optional
3
+
4
+ @dataclass
5
+ class Block:
6
+ directive: Dict[str, Any]
7
+ prefix: str
8
+ suffix: str
9
+ text: str
10
+ line_number: int
11
+ name: str
@@ -0,0 +1,6 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Dict
3
+
4
+ @dataclass
5
+ class CombineErrorsContext:
6
+ text: str
@@ -0,0 +1,19 @@
1
+ from src.syncspec.node import Node
2
+ from src.syncspec.edge import Edge
3
+ from src.syncspec.combine_nodes_context import CombineNodesContext
4
+
5
+
6
+ def make_combine_nodes(context: CombineNodesContext):
7
+ def combine_nodes(node: Node) -> Edge:
8
+ dtype = node.directive_type
9
+ node_id = f"{dtype}_{node.name}_{node.line_number}"
10
+ label = f"{dtype}\n{node.name}\n:{node.line_number}"
11
+ colors = {"source": "lightblue", "export": "red", "include": "lightgreen", "import": "yellow"}
12
+
13
+ context.G.add_node(
14
+ node_id, key=node.key, directive_type=dtype, label=label,
15
+ shape="rectangle", style="filled", fillcolor=colors.get(dtype, "white")
16
+ )
17
+ return Edge(dtype, node.key, node.line_number, node.name)
18
+
19
+ return combine_nodes
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Dict
3
+ import networkx as nx
4
+
5
+ @dataclass
6
+ class CombineNodesContext:
7
+ G: nx.DiGraph
@@ -0,0 +1,9 @@
1
+ from src.syncspec.string import String
2
+ from src.syncspec.combine_strings_context import CombineStringsContext
3
+
4
+
5
+ def make_combine_strings(context: CombineStringsContext):
6
+ def combine_strings(string: String) -> None:
7
+ context.text += string.text
8
+
9
+ return combine_strings
@@ -0,0 +1,6 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Dict
3
+
4
+ @dataclass
5
+ class CombineStringsContext:
6
+ text: str
@@ -0,0 +1,81 @@
1
+ import logging
2
+ import json
3
+ from typing import Union, Dict, Any
4
+
5
+ from src.syncspec.utilities import format_error
6
+ from src.syncspec.fragment import Fragment
7
+ from src.syncspec.block import Block
8
+ from src.syncspec.string import String
9
+ from src.syncspec.create_blocks_context import CreateBlocksContext
10
+
11
+
12
+ def _parse_json_content(text: str) -> Dict[str, Any]:
13
+ try:
14
+ data = json.loads(text)
15
+ except json.JSONDecodeError:
16
+ try:
17
+ data = json.loads('{' + text + '}')
18
+ except json.JSONDecodeError:
19
+ raise ValueError("Invalid JSON structure")
20
+
21
+ if not isinstance(data, dict):
22
+ raise ValueError("JSON root must be an object")
23
+ if any(k is None for k in data.keys()):
24
+ raise ValueError("Dictionary contains None keys")
25
+ return data
26
+
27
+
28
+ def make_create_blocks(context: CreateBlocksContext):
29
+ def create_blocks(fragment: Fragment) -> Union[Block, String, None]:
30
+ state = context.index % 4
31
+ result: Union[Block, String, None] = None
32
+
33
+ if state == 0:
34
+ result = String(text=fragment.text, line_number=fragment.line_number, name=fragment.name)
35
+
36
+ elif state == 1:
37
+ context.prefix = fragment.text
38
+ context.prefix_line_number = fragment.line_number
39
+ # Spec requests context.prefix_name = fragment.name, but field missing in class.
40
+
41
+ try:
42
+ context.directive = _parse_json_content(fragment.text)
43
+ context.prefix_valid = True
44
+ result = None
45
+ except (json.JSONDecodeError, ValueError):
46
+ context.prefix_valid = False
47
+ logging.error(format_error("JSON parsing failed", fragment.name, fragment.line_number))
48
+ result = String(
49
+ text=context.open_delimiter + fragment.text + context.close_delimiter,
50
+ line_number=fragment.line_number,
51
+ name=fragment.name
52
+ )
53
+
54
+ elif state == 2:
55
+ if context.prefix_valid:
56
+ context.text = fragment.text
57
+ result = None
58
+ else:
59
+ result = String(text=fragment.text, line_number=fragment.line_number, name=fragment.name)
60
+
61
+ elif state == 3:
62
+ if context.prefix_valid:
63
+ result = Block(
64
+ directive=context.directive,
65
+ prefix=context.prefix,
66
+ suffix=fragment.text,
67
+ text=context.text,
68
+ line_number=context.prefix_line_number,
69
+ name=fragment.name
70
+ )
71
+ else:
72
+ result = String(
73
+ text=context.open_delimiter + fragment.text + context.close_delimiter,
74
+ line_number=fragment.line_number,
75
+ name=fragment.name
76
+ )
77
+
78
+ context.index += 1
79
+ return result
80
+
81
+ return create_blocks
@@ -0,0 +1,14 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Dict
3
+
4
+ @dataclass
5
+ class CreateBlocksContext:
6
+ index: int
7
+ prefix: str
8
+ prefix_line_number: int
9
+ prefix_valid: bool
10
+ directive: Dict[str, Any]
11
+ text: str
12
+ open_delimiter: str
13
+ close_delimiter: str
14
+
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass
4
+ class Edge:
5
+ directive_type: str
6
+ key: str
7
+ line_number: int
8
+ name: str
@@ -0,0 +1,6 @@
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass
4
+ class File:
5
+ text: str
6
+ name: str
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass
4
+ class Fragment:
5
+ text: str
6
+ line_number: int
7
+ name: str
@@ -0,0 +1,30 @@
1
+ import re
2
+ from typing import List
3
+ from src.syncspec.validated_text import ValidatedText
4
+ from src.syncspec.fragment import Fragment
5
+ from src.syncspec.fragment_text_context import FragmentTextContext
6
+
7
+
8
+ def make_fragment_text(context: FragmentTextContext):
9
+ def fragment_text(text: ValidatedText) -> List[Fragment]:
10
+ pattern = f"({re.escape(context.open_delimiter)}|{re.escape(context.close_delimiter)})"
11
+ parts = re.split(pattern, text.text)
12
+
13
+ fragments = []
14
+ current_line = context.line_number
15
+
16
+ for part in parts:
17
+ if part in (context.open_delimiter, context.close_delimiter):
18
+ current_line += part.count('\n')
19
+ else:
20
+ fragments.append(Fragment(
21
+ text=part,
22
+ name=text.name,
23
+ line_number=current_line
24
+ ))
25
+ current_line += part.count('\n')
26
+
27
+ context.line_number = current_line
28
+ return fragments
29
+
30
+ return fragment_text
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Dict
3
+
4
+ @dataclass
5
+ class FragmentTextContext:
6
+ open_delimiter: str
7
+ close_delimiter: str
8
+ line_number: int
@@ -0,0 +1,12 @@
1
+ """Core functionality of syncspec"""
2
+
3
+
4
+ def syncspec(*args, **kwargs):
5
+ """
6
+ Main syncspec function.
7
+
8
+ Replace this docstring with your actual function documentation.
9
+ """
10
+ # Your function implementation here
11
+ result = "Hello from syncspec!"
12
+ return result
@@ -0,0 +1,20 @@
1
+ from typing import Callable
2
+ from src.syncspec.edge import Edge
3
+ from src.syncspec.graph_edges_context import GraphEdgesContext
4
+
5
+
6
+ def make_graph_edges(context: GraphEdgesContext) -> Callable[[Edge], None]:
7
+ def graph_edges(edge: Edge) -> None:
8
+ target_id = f"{edge.directive_type}_{edge.name}_{edge.line_number}"
9
+ source_map = {"include": "source", "import": "export"}
10
+ source_type = source_map.get(edge.directive_type)
11
+
12
+ if not source_type:
13
+ return
14
+
15
+ nodes = list(context.G.nodes(data=True))
16
+ for node_id, attrs in nodes:
17
+ if attrs.get("directive_type") == source_type and attrs.get("key") == edge.key:
18
+ context.G.add_edge(node_id, target_id)
19
+
20
+ return graph_edges
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Dict
3
+ import networkx as nx
4
+
5
+ @dataclass
6
+ class GraphEdgesContext:
7
+ G: nx.DiGraph
@@ -0,0 +1,95 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Union, Tuple, Dict, Any
4
+
5
+ from src.syncspec.node import Node
6
+ from src.syncspec.utilities import format_error
7
+ from src.syncspec.string import String
8
+ from src.syncspec.block import Block
9
+ from src.syncspec.import_block_context import ImportBlockContext
10
+
11
+
12
+ def make_import_block(context: ImportBlockContext):
13
+ def import_block(block: Block) -> Union[Tuple[String, Node, Node], Block, String]:
14
+ if "import" not in block.directive:
15
+ return block
16
+
17
+ import_path = block.directive["import"]
18
+ log_err = lambda msg: _log_and_return_error(msg, block, context)
19
+
20
+ # Path Validation
21
+ try:
22
+ root = Path(context.import_path).resolve()
23
+ target = (root / import_path).resolve()
24
+
25
+ if not str(target).startswith(str(root) + '/') and str(target) != str(root):
26
+ return log_err("Path traversal detected")
27
+ if not target.exists():
28
+ return log_err("File does not exist")
29
+ if not target.is_file():
30
+ return log_err("Not a file")
31
+
32
+ # Read and Decode
33
+ try:
34
+ v = target.read_text(encoding='utf-8')
35
+ except UnicodeDecodeError:
36
+ return log_err("File is not valid UTF-8 text")
37
+ except PermissionError:
38
+ return log_err("File is not readable")
39
+ except Exception as e:
40
+ return log_err(f"Validation failed: {str(e)}")
41
+
42
+ # Head/Tail Validation
43
+ head = block.directive.get("head", 0)
44
+ tail = block.directive.get("tail", 0)
45
+
46
+ if not isinstance(head, int) or isinstance(head, bool) or head < 0:
47
+ return log_err("Invalid head value")
48
+ if not isinstance(tail, int) or isinstance(tail, bool) or tail < 0:
49
+ return log_err("Invalid tail value")
50
+
51
+ u_lines = block.text.splitlines(keepends=True)
52
+ total_lines = len(u_lines)
53
+
54
+ if head + tail > total_lines:
55
+ return log_err("Head and tail overlap")
56
+
57
+ top = "".join(u_lines[:head])
58
+ bottom = "".join(u_lines[-tail:] if tail > 0 else [])
59
+
60
+ # Construct Result String
61
+ s_text = (
62
+ context.open_delimiter +
63
+ block.prefix +
64
+ context.close_delimiter +
65
+ top +
66
+ v +
67
+ "\n" +
68
+ bottom +
69
+ context.open_delimiter +
70
+ block.suffix +
71
+ context.close_delimiter
72
+ )
73
+ res_string = String(text=s_text, line_number=block.line_number, name=block.name)
74
+
75
+ # Construct Nodes
76
+ n_export = Node(directive_type="export", key=import_path, line_number=0, name=import_path)
77
+ n_import = Node(directive_type="import", key=import_path, line_number=block.line_number, name=block.name)
78
+
79
+ return (res_string, n_export, n_import)
80
+
81
+ return import_block
82
+
83
+
84
+ def _log_and_return_error(message: str, block: Block, context: ImportBlockContext) -> String:
85
+ logging.error(format_error(message, block.name, block.line_number))
86
+ text = (
87
+ context.open_delimiter +
88
+ block.prefix +
89
+ context.close_delimiter +
90
+ block.text +
91
+ context.open_delimiter +
92
+ block.suffix +
93
+ context.close_delimiter
94
+ )
95
+ return String(text=text, line_number=block.line_number, name=block.name)
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import ClassVar, Dict, Any
3
+
4
+ @dataclass
5
+ class ImportBlockContext:
6
+ import_path: str
7
+ open_delimiter: str
8
+ close_delimiter: str
@@ -0,0 +1,65 @@
1
+ import logging
2
+ from typing import Union, Tuple
3
+
4
+ from src.syncspec.node import Node
5
+ from src.syncspec.utilities import format_error
6
+ from src.syncspec.string import String
7
+ from src.syncspec.block import Block
8
+ from src.syncspec.include_block_context import IncludeBlockContext
9
+
10
+
11
+ def make_include_block(context: IncludeBlockContext):
12
+ def include_block(block: Block) -> Union[Tuple[String, Node], Block, String]:
13
+ def return_error(msg: str) -> String:
14
+ logging.error(format_error(msg, block.name, block.line_number))
15
+ return String(
16
+ text=context.open_delimiter + block.prefix + context.close_delimiter +
17
+ block.text +
18
+ context.open_delimiter + block.suffix + context.close_delimiter,
19
+ line_number=block.line_number,
20
+ name=block.name
21
+ )
22
+
23
+ if "include" not in block.directive:
24
+ return block
25
+
26
+ key = block.directive["include"]
27
+ if not isinstance(key, str):
28
+ return return_error("'include' directive must be a string")
29
+
30
+ if key not in context.state:
31
+ return return_error(f"include key '{key}' not found in context")
32
+
33
+ v = context.state[key]
34
+ if not isinstance(v, str):
35
+ return return_error(f"value for key '{key}' must be a string")
36
+
37
+ head = block.directive.get("head", 0)
38
+ tail = block.directive.get("tail", 0)
39
+
40
+ if isinstance(head, bool) or not isinstance(head, int) or head < 0:
41
+ return return_error("'head' must be a non-negative integer")
42
+ if isinstance(tail, bool) or not isinstance(tail, int) or tail < 0:
43
+ return return_error("'tail' must be a non-negative integer")
44
+
45
+ lines = block.text.splitlines(keepends=True)
46
+ total_lines = len(lines)
47
+
48
+ if head + tail > total_lines:
49
+ return return_error(f"head ({head}) + tail ({tail}) exceeds total lines ({total_lines})")
50
+
51
+ top = "".join(lines[:head])
52
+ bottom = "".join(lines[-tail:]) if tail > 0 else ""
53
+
54
+ s_text = (
55
+ context.open_delimiter + block.prefix + context.close_delimiter +
56
+ top + v + bottom +
57
+ context.open_delimiter + block.suffix + context.close_delimiter
58
+ )
59
+
60
+ s_obj = String(text=s_text, line_number=block.line_number, name=block.name)
61
+ n_obj = Node(directive_type="include", key=key, line_number=block.line_number, name=block.name)
62
+
63
+ return s_obj, n_obj
64
+
65
+ return include_block
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import ClassVar, Dict, Any
3
+
4
+ @dataclass
5
+ class IncludeBlockContext:
6
+ state: Dict[str, Any]
7
+ open_delimiter: str
8
+ close_delimiter: str
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass
4
+ class Node:
5
+ directive_type: str
6
+ key: str
7
+ line_number: int
8
+ name: str
@@ -0,0 +1,20 @@
1
+ import inspect
2
+ from typing import get_type_hints
3
+
4
+ def build_rules(rule_functions):
5
+ return [
6
+ (get_type_hints(fn).get(next(iter(inspect.signature(fn).parameters)), object), fn)
7
+ for fn in rule_functions
8
+ ]
9
+
10
+ def production(facts, rules):
11
+ for rule_type, fn in rules:
12
+ new_facts = []
13
+ for fact in facts:
14
+ if isinstance(fact, rule_type):
15
+ res = fn(fact)
16
+ new_facts.extend(res) if isinstance(res, (list, tuple)) else new_facts.append(res)
17
+ else:
18
+ new_facts.append(fact)
19
+ facts = new_facts
20
+ return facts