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.
- syncspec-0.1.0/LICENSE +21 -0
- syncspec-0.1.0/PKG-INFO +41 -0
- syncspec-0.1.0/README.md +23 -0
- syncspec-0.1.0/pyproject.toml +32 -0
- syncspec-0.1.0/setup.cfg +4 -0
- syncspec-0.1.0/src/syncspec/__init__.py +6 -0
- syncspec-0.1.0/src/syncspec/block.py +11 -0
- syncspec-0.1.0/src/syncspec/combine_errors_context.py +6 -0
- syncspec-0.1.0/src/syncspec/combine_nodes.py +19 -0
- syncspec-0.1.0/src/syncspec/combine_nodes_context.py +7 -0
- syncspec-0.1.0/src/syncspec/combine_strings.py +9 -0
- syncspec-0.1.0/src/syncspec/combine_strings_context.py +6 -0
- syncspec-0.1.0/src/syncspec/create_blocks.py +81 -0
- syncspec-0.1.0/src/syncspec/create_blocks_context.py +14 -0
- syncspec-0.1.0/src/syncspec/edge.py +8 -0
- syncspec-0.1.0/src/syncspec/file.py +6 -0
- syncspec-0.1.0/src/syncspec/fragment.py +7 -0
- syncspec-0.1.0/src/syncspec/fragment_text.py +30 -0
- syncspec-0.1.0/src/syncspec/fragment_text_context.py +8 -0
- syncspec-0.1.0/src/syncspec/function.py +12 -0
- syncspec-0.1.0/src/syncspec/graph_edges.py +20 -0
- syncspec-0.1.0/src/syncspec/graph_edges_context.py +7 -0
- syncspec-0.1.0/src/syncspec/import_block.py +95 -0
- syncspec-0.1.0/src/syncspec/import_block_context.py +8 -0
- syncspec-0.1.0/src/syncspec/include_block.py +65 -0
- syncspec-0.1.0/src/syncspec/include_block_context.py +8 -0
- syncspec-0.1.0/src/syncspec/node.py +8 -0
- syncspec-0.1.0/src/syncspec/production.py +20 -0
- syncspec-0.1.0/src/syncspec/source_block.py +65 -0
- syncspec-0.1.0/src/syncspec/source_block_context.py +8 -0
- syncspec-0.1.0/src/syncspec/string.py +7 -0
- syncspec-0.1.0/src/syncspec/syncspec_list.py +29 -0
- syncspec-0.1.0/src/syncspec/syncspec_list_context.py +9 -0
- syncspec-0.1.0/src/syncspec/syncspec_text.py +96 -0
- syncspec-0.1.0/src/syncspec/syncspec_text_context.py +11 -0
- syncspec-0.1.0/src/syncspec/text.py +6 -0
- syncspec-0.1.0/src/syncspec/utilities.py +6 -0
- syncspec-0.1.0/src/syncspec/validate_text.py +91 -0
- syncspec-0.1.0/src/syncspec/validate_text_context.py +8 -0
- syncspec-0.1.0/src/syncspec/validated_text.py +6 -0
- syncspec-0.1.0/src/syncspec.egg-info/PKG-INFO +41 -0
- syncspec-0.1.0/src/syncspec.egg-info/SOURCES.txt +54 -0
- syncspec-0.1.0/src/syncspec.egg-info/dependency_links.txt +1 -0
- syncspec-0.1.0/src/syncspec.egg-info/requires.txt +6 -0
- syncspec-0.1.0/src/syncspec.egg-info/top_level.txt +1 -0
- syncspec-0.1.0/tests/test_combine_nodes.py +25 -0
- syncspec-0.1.0/tests/test_combine_strings.py +28 -0
- syncspec-0.1.0/tests/test_create_blocks.py +69 -0
- syncspec-0.1.0/tests/test_fragment_text.py +77 -0
- syncspec-0.1.0/tests/test_function.py +22 -0
- syncspec-0.1.0/tests/test_graph_edges.py +24 -0
- syncspec-0.1.0/tests/test_import_block.py +78 -0
- syncspec-0.1.0/tests/test_include_block.py +41 -0
- syncspec-0.1.0/tests/test_source_block.py +54 -0
- syncspec-0.1.0/tests/test_syncspec_text.py +41 -0
- 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.
|
syncspec-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
syncspec-0.1.0/README.md
ADDED
|
@@ -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*"]
|
syncspec-0.1.0/setup.cfg
ADDED
|
@@ -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,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,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,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,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,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,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,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
|