syncraft 0.2.5__py3-none-any.whl → 0.2.6__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.

Potentially problematic release.


This version of syncraft might be problematic. Click here for more details.

syncraft/utils.py ADDED
@@ -0,0 +1,214 @@
1
+ from __future__ import annotations
2
+ from typing import Tuple, Any, Set, Optional
3
+ from sqlglot.expressions import Expression
4
+ from syncraft.syntax import Syntax
5
+ from syncraft.algebra import Left, Right, Error, Either, Algebra
6
+ from syncraft.parser import ParserState, Token
7
+
8
+
9
+ def rich_error(err: Error)->None:
10
+ try:
11
+ from rich import print
12
+ from rich.table import Table as RichTable
13
+ lst = err.to_list()
14
+ leaf = lst[0]
15
+ tbl = RichTable(title="Parser Error", show_lines=True)
16
+ tbl.add_column("Leaf Parser Field", style="blue")
17
+ tbl.add_column("Leaf Parser Value", style="yellow")
18
+ flds: Set[str] = set(leaf.keys())
19
+ for fld in sorted(flds):
20
+ leaf_value = leaf.get(fld, "N/A")
21
+ tbl.add_row(f"{fld}", f"{leaf_value}")
22
+ print(tbl)
23
+ except ImportError:
24
+ print(err)
25
+
26
+
27
+ def rich_parser(p: Syntax)-> None:
28
+ try:
29
+ from rich import print
30
+ print("Parser Debug Information:")
31
+ print(repr(p))
32
+ except ImportError:
33
+ print(p)
34
+
35
+ def rich_debug(this: Algebra[Any, ParserState[Any]],
36
+ state: ParserState[Any],
37
+ result: Either[Any, Tuple[Any, ParserState[Any]]])-> None:
38
+ try:
39
+ from rich import print
40
+ from rich.table import Table as RichTable
41
+ def value_to_str(value: Any, prefix:str='') -> str:
42
+ if isinstance(value, (tuple, list)):
43
+ if len(value) == 0:
44
+ return prefix + str(value)
45
+ else:
46
+ return '\n'.join(value_to_str(item, prefix=prefix+' - ') for item in value)
47
+ else:
48
+ if isinstance(value, Expression):
49
+ return prefix + value.sql()
50
+ elif isinstance(value, Token):
51
+ return prefix + f"{value.token_type.name}({value.text})"
52
+ elif isinstance(value, Syntax):
53
+ return prefix + (value.meta.name or 'N/A')
54
+ else:
55
+ return prefix + str(value)
56
+
57
+ tbl = RichTable(title=f"Debug: {this.name}", show_lines=True)
58
+ tbl.add_column("Parser", style="blue")
59
+ tbl.add_column("Old State", style="cyan")
60
+ tbl.add_column("Result", style="magenta")
61
+ tbl.add_column("New State", style="green")
62
+ tbl.add_column("Consumed", style="green")
63
+ if isinstance(result, Left):
64
+ tbl.add_row(value_to_str(this), value_to_str(state), value_to_str(result.value), 'N/A', 'N/A')
65
+ else:
66
+ assert isinstance(result, Right), f"Expected result to be a Right value, got {type(result)}, {result}"
67
+ value, new_state = result.value
68
+ tbl.add_row(value_to_str(this),
69
+ value_to_str(state),
70
+ value_to_str(value),
71
+ value_to_str(new_state))
72
+
73
+ print(tbl)
74
+ except ImportError:
75
+ print(this)
76
+ print(state)
77
+ print(result)
78
+
79
+
80
+
81
+
82
+ def syntax2svg(syntax: Syntax[Any, Any]) -> Optional[str]:
83
+ try:
84
+ from railroad import Diagram, Terminal, Sequence, Choice, OneOrMore, Comment, Group, Optional as RROptional
85
+ def to_railroad(s: Syntax[Any, Any]):
86
+ meta = s.meta
87
+ if meta is None or meta.name is None:
88
+ return Terminal(str(s))
89
+ children = [to_railroad(p) for p in meta.parameter]
90
+ if meta.name in ('>>', '//', '+'):
91
+ assert len(children) == 2
92
+ return Sequence(children[0], children[1])
93
+ elif meta.name == '|':
94
+ assert len(children) == 2
95
+ return Choice(0, children[0], children[1])
96
+ elif meta.name in ('*',):
97
+ assert len(children) == 1
98
+ return OneOrMore(children[0])
99
+ elif meta.name in ('~',):
100
+ assert len(children) == 1
101
+ return RROptional(children[0])
102
+ elif meta.name.startswith('token'):
103
+ return Terminal(meta.name)
104
+ elif meta.name == "sep_by":
105
+ assert len(children) == 2
106
+ return Sequence(children[0], OneOrMore(Sequence(children[1], children[0])))
107
+ elif meta.name.startswith('to'):
108
+ assert len(children) == 1
109
+ return Sequence(Comment(meta.name), children[0])
110
+ elif meta.name.startswith('bind'):
111
+ assert len(children) == 1
112
+ return Sequence(Comment(meta.name), children[0])
113
+ elif meta.name.startswith('mark'):
114
+ assert len(children) == 1
115
+ return Sequence(Comment(meta.name), children[0])
116
+ elif meta.name.startswith('when'):
117
+ assert len(children) == 2
118
+ return Group(Choice(0, children[0], children[1]),
119
+ label="Conditional on env/config")
120
+ elif meta.name.startswith('success'):
121
+ return Terminal(meta.name)
122
+ elif meta.name.startswith('fail'):
123
+ return Terminal(meta.name)
124
+ elif meta.name.startswith('lazy'):
125
+ return Terminal(meta.name)
126
+ elif meta.name == 'anything':
127
+ return Terminal(meta.name)
128
+ elif meta.name == 'until':
129
+ return Terminal(meta.name)
130
+ else:
131
+ return Sequence(Terminal(meta.name), *(children if children else []))
132
+
133
+ diagram = Diagram(to_railroad(syntax))
134
+ return diagram.writeSvgString()
135
+ except ImportError:
136
+ return None
137
+
138
+ def ast2svg(ast: Any) -> Optional[str]:
139
+ """
140
+ Generate SVG visualization for a Syncraft AST node using graphviz.
141
+ Returns SVG string or None if graphviz is not available.
142
+ """
143
+ try:
144
+ import graphviz
145
+ except ImportError:
146
+ return None
147
+
148
+ def node_label(node):
149
+ from syncraft.ast import Nothing, Marked, Choice, Many, Then, Collect, Custom, Token
150
+ if isinstance(node, Nothing):
151
+ return "Nothing"
152
+ elif isinstance(node, Marked):
153
+ return f"Marked(name={node.name})"
154
+ elif isinstance(node, Choice):
155
+ return f"Choice(kind={getattr(node.kind, 'name', node.kind)})"
156
+ elif isinstance(node, Many):
157
+ return "Many"
158
+ elif isinstance(node, Then):
159
+ return f"Then(kind={node.kind.name})"
160
+ elif isinstance(node, Collect):
161
+ return f"Collect({getattr(node.collector, '__name__', str(node.collector))})"
162
+ elif isinstance(node, Custom):
163
+ return f"Custom(meta={node.meta})"
164
+ elif isinstance(node, Token):
165
+ return f"Token({node.token_type.name}: {node.text})"
166
+ elif hasattr(node, '__class__'):
167
+ return node.__class__.__name__
168
+ else:
169
+ return str(node)
170
+
171
+ def add_nodes_edges(dot, node, parent_id=None, node_id_gen=[0]):
172
+ from syncraft.ast import Nothing, Marked, Choice, Many, Then, Collect, Custom, Token
173
+ node_id = f"n{node_id_gen[0]}"
174
+ node_id_gen[0] += 1
175
+ label = node_label(node)
176
+ dot.node(node_id, label)
177
+ if parent_id is not None:
178
+ dot.edge(parent_id, node_id)
179
+
180
+ # Walk children according to AST type
181
+ if isinstance(node, Nothing):
182
+ return
183
+ elif isinstance(node, Marked):
184
+ add_nodes_edges(dot, node.value, node_id, node_id_gen)
185
+ elif isinstance(node, Choice):
186
+ if node.value is not None:
187
+ add_nodes_edges(dot, node.value, node_id, node_id_gen)
188
+ elif isinstance(node, Many):
189
+ for child in node.value:
190
+ add_nodes_edges(dot, child, node_id, node_id_gen)
191
+ elif isinstance(node, Then):
192
+ add_nodes_edges(dot, node.left, node_id, node_id_gen)
193
+ add_nodes_edges(dot, node.right, node_id, node_id_gen)
194
+ elif isinstance(node, Collect):
195
+ add_nodes_edges(dot, node.value, node_id, node_id_gen)
196
+ elif isinstance(node, Custom):
197
+ add_nodes_edges(dot, node.value, node_id, node_id_gen)
198
+ # Token is a leaf
199
+ # For other types, try to walk __dict__ if they are dataclasses
200
+ elif hasattr(node, '__dataclass_fields__'):
201
+ for f in node.__dataclass_fields__:
202
+ v = getattr(node, f)
203
+ if isinstance(v, (list, tuple)):
204
+ for item in v:
205
+ if hasattr(item, '__class__'):
206
+ add_nodes_edges(dot, item, node_id, node_id_gen)
207
+ elif hasattr(v, '__class__') and v is not node:
208
+ add_nodes_edges(dot, v, node_id, node_id_gen)
209
+
210
+ dot = graphviz.Digraph(format='svg')
211
+ add_nodes_edges(dot, ast)
212
+ return dot.pipe().decode('utf-8')
213
+
214
+
syncraft/walker.py ADDED
@@ -0,0 +1,147 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import (
4
+ Any, Tuple, Generator as PyGenerator, TypeVar, Generic, Optional, Callable, Hashable
5
+ )
6
+ from dataclasses import dataclass, replace, field
7
+ from syncraft.algebra import (
8
+ Algebra, Either, Right, Incomplete, Left, SyncraftError
9
+ )
10
+ from syncraft.ast import TokenSpec, ThenSpec, ManySpec, ChoiceSpec, LazySpec
11
+ from syncraft.parser import TokenType
12
+ from syncraft.constraint import Bindable, FrozenDict
13
+
14
+ import re
15
+ from syncraft.syntax import Syntax
16
+ from syncraft.cache import Cache
17
+
18
+
19
+ S = TypeVar('S', bound=Bindable)
20
+ A = TypeVar('A')
21
+ B = TypeVar('B')
22
+ SS = TypeVar('SS', bound=Hashable)
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class WalkerState(Bindable, Generic[SS]):
32
+ reducer: Optional[Callable[[Any, SS], SS]] = None
33
+ acc: Optional[SS] = None
34
+ visited: frozenset = field(default_factory=frozenset)
35
+
36
+
37
+ def reduce(self, value: Any) -> WalkerState[SS]:
38
+ if self.reducer:
39
+ new_acc = self.reducer(value, self.acc) if self.acc is not None else value
40
+ return replace(self, acc=new_acc)
41
+ else:
42
+ return replace(self, acc=value)
43
+
44
+ def visit(self, key: Hashable) -> WalkerState[SS]:
45
+ return replace(self, visited=self.visited | {key})
46
+
47
+
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class Walker(Algebra[SS, WalkerState[SS]]):
52
+ @classmethod
53
+ def state(cls, reducer: Callable[[Any, SS], SS], init: SS )->WalkerState[SS]: # type: ignore
54
+ assert callable(reducer), f"reducer must be a Reducer or None, got {type(reducer)}"
55
+ return WalkerState(reducer=reducer, acc=init)
56
+
57
+
58
+ @classmethod
59
+ def lazy(cls, thunk: Callable[[], Algebra[Any, WalkerState[SS]]], cache: Cache) -> Algebra[Any, WalkerState[SS]]:
60
+ def alazy_run(input: WalkerState[SS], use_cache:bool) -> PyGenerator[Incomplete[WalkerState[SS]], WalkerState[SS], Either[Any, Tuple[Any, WalkerState[SS]]]]:
61
+ result = yield from thunk().run(input, use_cache)
62
+ return result
63
+
64
+ def lazy_run(input: WalkerState[SS], use_cache:bool) -> PyGenerator[Incomplete[WalkerState[SS]], WalkerState[SS], Either[Any, Tuple[Any, WalkerState[SS]]]]:
65
+ print('thunk', thunk, input.visited)
66
+ if thunk in input.visited:
67
+ return Right((None, input))
68
+ else:
69
+ thunk_result = yield from thunk().run(input, use_cache)
70
+ match thunk_result:
71
+ case Right((value, from_thunk)):
72
+ data = LazySpec(value=value)
73
+ return Right((data, from_thunk.visit(thunk).reduce(data)))
74
+ raise SyncraftError("flat_map should always return a value or an error.", offending=thunk_result, expect=(Left, Right))
75
+ return cls(lazy_run, name=cls.__name__ + '.lazy', cache=cache)
76
+
77
+
78
+ def then_both(self, other: Algebra[Any, WalkerState[SS]]) -> Algebra[Any, WalkerState[SS]]:
79
+ def then_run(input: WalkerState[SS], use_cache:bool) -> PyGenerator[Incomplete[WalkerState[SS]], WalkerState[SS], Either[Any, Tuple[Any, WalkerState[SS]]]]:
80
+ self_result = yield from self.run(input, use_cache=use_cache)
81
+ match self_result:
82
+ case Right((value, from_left)):
83
+ other_result = yield from other.run(from_left, use_cache)
84
+ match other_result:
85
+ case Right((result, from_right)):
86
+ data = ThenSpec(left=value, right=result)
87
+ return Right((data, from_right.reduce(data)))
88
+ raise SyncraftError("flat_map should always return a value or an error.", offending=self_result, expect=(Left, Right))
89
+ return self.__class__(then_run, name=self.name, cache=self.cache | other.cache)
90
+
91
+ def then_left(self, other: Algebra[Any, WalkerState[SS]]) -> Algebra[Any, WalkerState[SS]]:
92
+ return self.then_both(other) # For simplicity, treat as both
93
+
94
+ def then_right(self, other: Algebra[Any, WalkerState[SS]]) -> Algebra[Any, WalkerState[SS]]:
95
+ return self.then_both(other)
96
+
97
+
98
+ def many(self, *, at_least: int, at_most: Optional[int]) -> Algebra[Any, WalkerState[SS]]:
99
+ if at_least <=0 or (at_most is not None and at_most < at_least):
100
+ raise SyncraftError(f"Invalid arguments for many: at_least={at_least}, at_most={at_most}", offending=(at_least, at_most), expect="at_least>0 and (at_most is None or at_most>=at_least)")
101
+ def many_run(input: WalkerState[SS], use_cache:bool) -> PyGenerator[Incomplete[WalkerState[SS]], WalkerState[SS], Either[Any, Tuple[Any, WalkerState[SS]]]]:
102
+ self_result = yield from self.run(input, use_cache)
103
+ match self_result:
104
+ case Right((value, from_self)):
105
+ data = ManySpec(value=value, at_least=at_least, at_most=at_most)
106
+ return Right((data, from_self.reduce(data)))
107
+ raise SyncraftError("many should always return a value or an error.", offending=self_result, expect=(Left, Right))
108
+ return self.__class__(many_run, name=f"many({self.name})", cache=self.cache)
109
+
110
+
111
+ def or_else(self, other: Algebra[Any, WalkerState[SS]]) -> Algebra[Any, WalkerState[SS]]:
112
+ def or_else_run(input: WalkerState[SS], use_cache:bool) -> PyGenerator[Incomplete[WalkerState[SS]], WalkerState[SS], Either[Any, Tuple[Any, WalkerState[SS]]]]:
113
+ self_result = yield from self.run(input, use_cache=use_cache)
114
+ match self_result:
115
+ case Right((value, from_left)):
116
+ other_result = yield from other.run(from_left, use_cache)
117
+ match other_result:
118
+ case Right((result, from_right)):
119
+ data = ChoiceSpec(left=value, right=result)
120
+ return Right((data, from_right.reduce(data)))
121
+ raise SyncraftError("", offending=self)
122
+ return self.__class__(or_else_run, name=f"or_else({self.name} | {other.name})", cache=self.cache | other.cache)
123
+
124
+ @classmethod
125
+ def token(cls,
126
+ *,
127
+ cache: Cache,
128
+ token_type: Optional[TokenType] = None,
129
+ text: Optional[str] = None,
130
+ case_sensitive: bool = False,
131
+ regex: Optional[re.Pattern[str]] = None
132
+ )-> Algebra[Any, WalkerState[SS]]:
133
+ def token_run(input: WalkerState[SS], use_cache:bool) -> PyGenerator[Incomplete[WalkerState[SS]], WalkerState[SS], Either[Any, Tuple[Any, WalkerState[SS]]]]:
134
+ yield from ()
135
+ data = TokenSpec(token_type=token_type, text=text, regex=regex, case_sensitive=case_sensitive)
136
+ return Right((data, input.reduce(data)))
137
+ return cls(token_run, name=cls.__name__ + f'.token({token_type or text or regex})', cache=cache)
138
+
139
+
140
+ def walk(syntax: Syntax[Any, Any], reducer: Callable[[Any, Any], SS], init: SS)-> Optional[SS]:
141
+ from syncraft.syntax import run
142
+ from rich import print
143
+ v, s = run(syntax=syntax, alg=Walker, use_cache=False, reducer=reducer, init=init)
144
+ if s is not None:
145
+ return s.acc
146
+ else:
147
+ return None
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: syncraft
3
+ Version: 0.2.6
4
+ Summary: Parser combinator library
5
+ Author-email: Michael Afmokt <michael@esacca.com>
6
+ License-Expression: MIT
7
+ Keywords: parser,combinator,sql,sqlite,generator,printer
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: rstr>=3.2.2
12
+ Requires-Dist: sqlglot>=27.7.0
13
+ Dynamic: license-file
14
+
15
+ # Syncraft
16
+
17
+ Syncraft is a parser/generator combinator library for Python. It helps you
18
+
19
+ - Build grammars
20
+ - Parse SQL statement to AST
21
+ - Search AST by grammar
22
+ - Convert AST to dataclass
23
+ - Check constraints over the AST/dataclass
24
+ - Change dataclass and convert back to AST
25
+
26
+
27
+ ## Installation
28
+
29
+ ### pip
30
+ ```bash
31
+ pip install syncraft
32
+ ```
33
+
34
+ ### uv
35
+ ```bash
36
+ uv add syncraft
37
+ ```
38
+
39
+ Python 3.10+ is required.
40
+
41
+ ### With pip
42
+ ```bash
43
+ pip install syncraft[dev]
44
+ ```
45
+
46
+ ### With uv
47
+ ```bash
48
+ uv sync --group dev
49
+ ```
50
+
51
+ TODO
52
+ - [ ] debug constraint.datalog
53
+ - [ ] debug sqlite3
54
+ - [ ] collect terminal from syntax and build PLY lexer
55
+ - [ ] chunker
56
+ - [ ] unify in find
@@ -0,0 +1,20 @@
1
+ syncraft/__init__.py,sha256=g1Rd3DwPJ-Z7dL0PBNyNkcLBmpkp8bIBDWabkeBCD48,1329
2
+ syncraft/algebra.py,sha256=W6n4YH75VUU960DTW1ZGljmuhvOtLOzkL6_w9DG6fWI,20706
3
+ syncraft/ast.py,sha256=kMpZtox3S41t6srptJzd8G0p7Imu-FX2reC6Zdk47PY,20779
4
+ syncraft/cache.py,sha256=n7YpN6OmkDySVKRP-FWE8Ni6WqzMHz2b4TsSbBPRJtA,3655
5
+ syncraft/constraint.py,sha256=a6j_VafRor8W7FBXh20DoVnIUO9nfn7eIValigCf9lU,16318
6
+ syncraft/dev.py,sha256=v7jdb2aOVCGbio-Jw14tRhO09FkhWc0vrDdIkIKPu2Y,186
7
+ syncraft/finder.py,sha256=Mv9BYrsDjjq62Z4NO3gElZAbkEgSBINhSbqFbKEQW4Q,4347
8
+ syncraft/generator.py,sha256=ybliYMDCC7z_Q3ODUYvYvwaKaMl7lULtJkmtX_c6lWw,21209
9
+ syncraft/lexer.py,sha256=npPHAEEbBRQYcFHPMHtXmiVLbCZAnjpTtj3cnVui4W0,3720
10
+ syncraft/parser.py,sha256=jt34gkHl4OJuImedR4abPGPw0VZtSOPzuTQRsvx8G_k,10094
11
+ syncraft/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ syncraft/sqlite3.py,sha256=Pq09IHZOwuWg5W82l9D1flzd36QV0TOHQpTJ5U02V8g,34701
13
+ syncraft/syntax.py,sha256=O4HE4IDZ6fclP7kRy5L6rIuIdp_LmnMsr0LRuAIIS8s,25033
14
+ syncraft/utils.py,sha256=2V446Il2q4mR7bwwGX5_qruJPU0WXU2PniXEjl6EOPE,8746
15
+ syncraft/walker.py,sha256=EoTSCCiaIPpBpsd6EcbFCw2_0jlDyNkemAeDoqA4Mus,7118
16
+ syncraft-0.2.6.dist-info/licenses/LICENSE,sha256=wHSV424U5csa3339dy1AZbsz2xsd0hrkMx2QK48CcUk,1062
17
+ syncraft-0.2.6.dist-info/METADATA,sha256=tFUXcscYjk8tgtahL7BPkTstoFN7o3rJgi2QL9z3y-s,1023
18
+ syncraft-0.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ syncraft-0.2.6.dist-info/top_level.txt,sha256=Kq3t8ESXB2xW1Xt3uPmkENFc-c4f2pamNmaURBk7zc8,9
20
+ syncraft-0.2.6.dist-info/RECORD,,
syncraft/diagnostic.py DELETED
@@ -1,70 +0,0 @@
1
- from __future__ import annotations
2
- from rich import print
3
- from rich.table import Table as RichTable
4
- from typing import Tuple, Any, Set
5
- from syncraft.syntax import Syntax
6
- from syncraft.algebra import Left, Right, Error, Either, Algebra
7
-
8
- from syncraft.parser import ParserState, Token
9
- from sqlglot.expressions import Expression
10
-
11
-
12
- def rich_error(err: Error)->None:
13
- lst = err.to_list()
14
- root, leaf = lst[0], lst[-1]
15
- tbl = RichTable(title="Parser Error", show_lines=True)
16
- tbl.add_column("Root Parser Field", style="blue")
17
- tbl.add_column("Root Parser Value", style="green")
18
- tbl.add_column("...")
19
- tbl.add_column("Leaf Parser Field", style="blue")
20
- tbl.add_column("Leaf Parser Value", style="yellow")
21
- flds: Set[str] = set(root.keys()) | set(leaf.keys())
22
- for fld in sorted(flds):
23
- root_value = root.get(fld, "N/A")
24
- leaf_value = leaf.get(fld, "N/A")
25
- tbl.add_row(f"{fld}", f"{root_value}", "...", f"{fld}", f"{leaf_value}")
26
- print(tbl)
27
-
28
-
29
- def rich_parser(p: Syntax)-> None:
30
- print("Parser Debug Information:")
31
- print(p.meta.to_string(lambda _ : True) or repr(p))
32
-
33
- def rich_debug(this: Algebra[Any, ParserState[Any]],
34
- state: ParserState[Any],
35
- result: Either[Any, Tuple[Any, ParserState[Any]]])-> None:
36
- def value_to_str(value: Any, prefix:str='') -> str:
37
- if isinstance(value, (tuple, list)):
38
- if len(value) == 0:
39
- return prefix + str(value)
40
- else:
41
- return '\n'.join(value_to_str(item, prefix=prefix+' - ') for item in value)
42
- else:
43
- if isinstance(value, Expression):
44
- return prefix + value.sql()
45
- elif isinstance(value, Token):
46
- return prefix + f"{value.token_type.name}({value.text})"
47
- elif isinstance(value, Syntax):
48
- return prefix + (value.meta.to_string(lambda _ : True) or 'N/A')
49
- else:
50
- return prefix + str(value)
51
-
52
- tbl = RichTable(title=f"Debug: {this.name}", show_lines=True)
53
- tbl.add_column("Parser", style="blue")
54
- tbl.add_column("Old State", style="cyan")
55
- tbl.add_column("Result", style="magenta")
56
- tbl.add_column("New State", style="green")
57
- tbl.add_column("Consumed", style="green")
58
- if isinstance(result, Left):
59
- tbl.add_row(value_to_str(this), value_to_str(state), value_to_str(result.value), 'N/A', 'N/A')
60
- else:
61
- assert isinstance(result, Right), f"Expected result to be a Right value, got {type(result)}, {result}"
62
- value, new_state = result.value
63
- tbl.add_row(value_to_str(this),
64
- value_to_str(state),
65
- value_to_str(value),
66
- value_to_str(new_state),
67
- value_to_str(state.delta(new_state)))
68
-
69
- print(tbl)
70
-
@@ -1,113 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: syncraft
3
- Version: 0.2.5
4
- Summary: Parser combinator library
5
- Author-email: Michael Afmokt <michael@esacca.com>
6
- License-Expression: MIT
7
- Keywords: parser,combinator,sql,sqlite,generator,printer
8
- Requires-Python: >=3.10
9
- Description-Content-Type: text/markdown
10
- License-File: LICENSE
11
- Requires-Dist: rich>=14.1.0
12
- Requires-Dist: rstr>=3.2.2
13
- Requires-Dist: sqlglot>=27.7.0
14
- Dynamic: license-file
15
-
16
- # Syncraft
17
-
18
- Syncraft is a parser/generator combinator library for Python. It helps you
19
-
20
- - Build grammars
21
- - Parse SQL statement to AST
22
- - Search AST by grammar
23
- - Convert AST to dataclass
24
- - Check constraints over the AST/dataclass
25
- - Change dataclass and convert back to AST
26
-
27
-
28
- ## Installation
29
-
30
- ### pip
31
- ```bash
32
- pip install syncraft
33
- ```
34
-
35
- ### uv
36
- ```bash
37
- uv add syncraft
38
- ```
39
-
40
- Python 3.10+ is required.
41
-
42
- ## Quickstart
43
-
44
- !. Define grammar
45
-
46
- ```python
47
- from dataclasses import dataclass
48
- from syncraft import literal, parse, generate
49
-
50
- A = literal("a")
51
- B = literal("b")
52
- syntax = A + B # sequence
53
-
54
- ast, _ = parse(syntax, "a b", dialect="sqlite")
55
- gen, _ = generate(syntax, ast)
56
- assert ast == gen
57
- ```
58
-
59
- Collect parsed pieces into dataclasses using marks and `.to()`:
60
-
61
- ```python
62
- from dataclasses import dataclass
63
- from syncraft import literal
64
-
65
- @dataclass
66
- class Pair:
67
- first: any
68
- second: any
69
-
70
- A = literal("a").mark("first")
71
- B = literal("b").mark("second")
72
- syntax = (A + B).to(Pair)
73
-
74
- ast, _ = parse(syntax, "a b", dialect="sqlite")
75
- value, invert = ast.bimap()
76
- # value is Pair(first=VAR(a), second=VAR(b))
77
- round_tripped, _ = generate(syntax, invert(value))
78
- assert round_tripped == ast
79
- ```
80
-
81
- Use the built‑in SQLite grammar snippets to parse statements:
82
-
83
- ```python
84
- from syncraft import parse
85
- from syncraft.sqlite3 import select_stmt
86
-
87
- ast, _ = parse(select_stmt, "select a from t where a > 1", dialect="sqlite")
88
- ```
89
-
90
- ## Core ideas
91
-
92
- - Syntax describes structure and transforms values; Algebra executes it.
93
- - AST types: Then, Choice, Many, Marked, Collect, Nothing, Token.
94
- - Operators: `+` (both), `>>` (keep right), `//` (keep left), `|` (choice), `~` (optional), `many()`, `sep_by()`, `between()`.
95
- - Error model supports backtracking and commit (`cut()`).
96
-
97
- ## Documentation
98
-
99
- - Tutorials and API reference are built with MkDocs. Local preview:
100
- 1) install dev deps (see `pyproject.toml` dev group)
101
- 2) activate your venv and run `mkdocs serve`
102
-
103
- - Version injection: pages can use `{{ version }}`. It is provided by mkdocs-macros via `docs/main.py`, which resolves the version in this order:
104
- - `[project].version` from `pyproject.toml`
105
- - installed package metadata (`importlib.metadata.version('syncraft')`)
106
- - fallback `"0.0.0"`
107
-
108
- The macros plugin is configured in `mkdocs.yml` with `module_name: docs/main`.
109
-
110
- ## Contributing / Roadmap
111
-
112
- - Improve performance and add benchmarks
113
- - Expand tutorials and SQLite coverage examples
@@ -1,16 +0,0 @@
1
- syncraft/__init__.py,sha256=8LIO0m0XEAsgKd--G8u6uT-lljLDtKMeIequNRNv2dc,894
2
- syncraft/algebra.py,sha256=zm6HzaE2XeyhWInoKDSJrVtnkjJV6BsD0zPBRz7iDAI,23333
3
- syncraft/ast.py,sha256=F6aRdxZ6IBNvnk1xe_ugEBvL-BXNXdkN4s1YDIuNw3Y,19134
4
- syncraft/constraint.py,sha256=qq-DkCH-8NgGqSYrNo4achq5t4gEEycelFTJWg4I_ek,15439
5
- syncraft/diagnostic.py,sha256=cgwcQnCcgrCRX3h-oGTDb5rcJAtitPV3LfH9eLvO93E,2837
6
- syncraft/finder.py,sha256=FCOHGdJf2NHG_letl8-IArfBywEolmtZcTJQPOj1iYw,4147
7
- syncraft/generator.py,sha256=bmXcnExG-M44ChxdS9K_6H0MdCa9gD7erJ8aAu-LpXM,20444
8
- syncraft/parser.py,sha256=9z9Iq9_Ih3mlXkuz85gsND4xshZARgAqo5-R_DpgA9I,17338
9
- syncraft/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- syncraft/sqlite3.py,sha256=Pq09IHZOwuWg5W82l9D1flzd36QV0TOHQpTJ5U02V8g,34701
11
- syncraft/syntax.py,sha256=nSWE1SHvVDYON0UQM8kgp14kx54hbKBYHV0xB6fgwkk,22366
12
- syncraft-0.2.5.dist-info/licenses/LICENSE,sha256=wHSV424U5csa3339dy1AZbsz2xsd0hrkMx2QK48CcUk,1062
13
- syncraft-0.2.5.dist-info/METADATA,sha256=X4WtYQxjCWYSf3JF7UI2zyERU-NJQObI-gwTcUxzHB4,2821
14
- syncraft-0.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
- syncraft-0.2.5.dist-info/top_level.txt,sha256=Kq3t8ESXB2xW1Xt3uPmkENFc-c4f2pamNmaURBk7zc8,9
16
- syncraft-0.2.5.dist-info/RECORD,,