yuho 5.0.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.
- yuho/__init__.py +16 -0
- yuho/ast/__init__.py +196 -0
- yuho/ast/builder.py +926 -0
- yuho/ast/constant_folder.py +280 -0
- yuho/ast/dead_code.py +199 -0
- yuho/ast/exhaustiveness.py +503 -0
- yuho/ast/nodes.py +907 -0
- yuho/ast/overlap.py +291 -0
- yuho/ast/reachability.py +293 -0
- yuho/ast/scope_analysis.py +490 -0
- yuho/ast/transformer.py +490 -0
- yuho/ast/type_check.py +471 -0
- yuho/ast/type_inference.py +425 -0
- yuho/ast/visitor.py +239 -0
- yuho/cli/__init__.py +14 -0
- yuho/cli/commands/__init__.py +1 -0
- yuho/cli/commands/api.py +431 -0
- yuho/cli/commands/ast_viz.py +334 -0
- yuho/cli/commands/check.py +218 -0
- yuho/cli/commands/config.py +311 -0
- yuho/cli/commands/contribute.py +122 -0
- yuho/cli/commands/diff.py +487 -0
- yuho/cli/commands/explain.py +240 -0
- yuho/cli/commands/fmt.py +253 -0
- yuho/cli/commands/generate.py +316 -0
- yuho/cli/commands/graph.py +410 -0
- yuho/cli/commands/init.py +120 -0
- yuho/cli/commands/library.py +656 -0
- yuho/cli/commands/lint.py +503 -0
- yuho/cli/commands/lsp.py +36 -0
- yuho/cli/commands/preview.py +377 -0
- yuho/cli/commands/repl.py +444 -0
- yuho/cli/commands/serve.py +44 -0
- yuho/cli/commands/test.py +528 -0
- yuho/cli/commands/transpile.py +121 -0
- yuho/cli/commands/wizard.py +370 -0
- yuho/cli/completions.py +182 -0
- yuho/cli/error_formatter.py +193 -0
- yuho/cli/main.py +1064 -0
- yuho/config/__init__.py +46 -0
- yuho/config/loader.py +235 -0
- yuho/config/mask.py +194 -0
- yuho/config/schema.py +147 -0
- yuho/library/__init__.py +84 -0
- yuho/library/index.py +328 -0
- yuho/library/install.py +699 -0
- yuho/library/lockfile.py +330 -0
- yuho/library/package.py +421 -0
- yuho/library/resolver.py +791 -0
- yuho/library/signature.py +335 -0
- yuho/llm/__init__.py +45 -0
- yuho/llm/config.py +75 -0
- yuho/llm/factory.py +123 -0
- yuho/llm/prompts.py +146 -0
- yuho/llm/providers.py +383 -0
- yuho/llm/utils.py +470 -0
- yuho/lsp/__init__.py +14 -0
- yuho/lsp/code_action_handler.py +518 -0
- yuho/lsp/completion_handler.py +85 -0
- yuho/lsp/diagnostics.py +100 -0
- yuho/lsp/hover_handler.py +130 -0
- yuho/lsp/server.py +1425 -0
- yuho/mcp/__init__.py +10 -0
- yuho/mcp/server.py +1452 -0
- yuho/parser/__init__.py +8 -0
- yuho/parser/source_location.py +108 -0
- yuho/parser/wrapper.py +311 -0
- yuho/testing/__init__.py +48 -0
- yuho/testing/coverage.py +274 -0
- yuho/testing/fixtures.py +263 -0
- yuho/transpile/__init__.py +52 -0
- yuho/transpile/alloy_transpiler.py +546 -0
- yuho/transpile/base.py +100 -0
- yuho/transpile/blocks_transpiler.py +338 -0
- yuho/transpile/english_transpiler.py +470 -0
- yuho/transpile/graphql_transpiler.py +404 -0
- yuho/transpile/json_transpiler.py +217 -0
- yuho/transpile/jsonld_transpiler.py +250 -0
- yuho/transpile/latex_preamble.py +161 -0
- yuho/transpile/latex_transpiler.py +406 -0
- yuho/transpile/latex_utils.py +206 -0
- yuho/transpile/mermaid_transpiler.py +357 -0
- yuho/transpile/registry.py +275 -0
- yuho/verify/__init__.py +43 -0
- yuho/verify/alloy.py +352 -0
- yuho/verify/combined.py +218 -0
- yuho/verify/z3_solver.py +1155 -0
- yuho-5.0.0.dist-info/METADATA +186 -0
- yuho-5.0.0.dist-info/RECORD +91 -0
- yuho-5.0.0.dist-info/WHEEL +4 -0
- yuho-5.0.0.dist-info/entry_points.txt +2 -0
yuho/parser/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Yuho parser module - tree-sitter based parsing for .yh files
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from yuho.parser.source_location import SourceLocation
|
|
6
|
+
from yuho.parser.wrapper import Parser, get_parser, clear_parser_cache
|
|
7
|
+
|
|
8
|
+
__all__ = ["Parser", "SourceLocation", "get_parser", "clear_parser_cache"]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Source location tracking for AST nodes and error reporting.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class SourceLocation:
|
|
11
|
+
"""
|
|
12
|
+
Represents a span of source code with file, line, and column information.
|
|
13
|
+
|
|
14
|
+
All line and column numbers are 1-indexed for user-facing display.
|
|
15
|
+
Internally, tree-sitter uses 0-indexed positions which are converted
|
|
16
|
+
in the Parser wrapper.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
file: Path to the source file (or "<string>" for inline parsing)
|
|
20
|
+
line: Starting line number (1-indexed)
|
|
21
|
+
col: Starting column number (1-indexed)
|
|
22
|
+
end_line: Ending line number (1-indexed)
|
|
23
|
+
end_col: Ending column number (1-indexed, exclusive)
|
|
24
|
+
offset: Byte offset from start of file
|
|
25
|
+
end_offset: Byte offset of end position
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
file: str
|
|
29
|
+
line: int
|
|
30
|
+
col: int
|
|
31
|
+
end_line: int
|
|
32
|
+
end_col: int
|
|
33
|
+
offset: Optional[int] = None
|
|
34
|
+
end_offset: Optional[int] = None
|
|
35
|
+
|
|
36
|
+
def __str__(self) -> str:
|
|
37
|
+
"""Format as 'file:line:col' for error messages."""
|
|
38
|
+
if self.line == self.end_line:
|
|
39
|
+
return f"{self.file}:{self.line}:{self.col}"
|
|
40
|
+
return f"{self.file}:{self.line}:{self.col}-{self.end_line}:{self.end_col}"
|
|
41
|
+
|
|
42
|
+
def __repr__(self) -> str:
|
|
43
|
+
return (
|
|
44
|
+
f"SourceLocation(file={self.file!r}, "
|
|
45
|
+
f"line={self.line}, col={self.col}, "
|
|
46
|
+
f"end_line={self.end_line}, end_col={self.end_col})"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_tree_sitter_node(cls, node, file: str = "<string>") -> "SourceLocation":
|
|
51
|
+
"""
|
|
52
|
+
Create a SourceLocation from a tree-sitter node.
|
|
53
|
+
|
|
54
|
+
Tree-sitter uses 0-indexed rows and columns, so we add 1 for
|
|
55
|
+
user-facing display.
|
|
56
|
+
"""
|
|
57
|
+
start_point = node.start_point
|
|
58
|
+
end_point = node.end_point
|
|
59
|
+
|
|
60
|
+
return cls(
|
|
61
|
+
file=file,
|
|
62
|
+
line=start_point[0] + 1,
|
|
63
|
+
col=start_point[1] + 1,
|
|
64
|
+
end_line=end_point[0] + 1,
|
|
65
|
+
end_col=end_point[1] + 1,
|
|
66
|
+
offset=node.start_byte,
|
|
67
|
+
end_offset=node.end_byte,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def unknown(cls, file: str = "<unknown>") -> "SourceLocation":
|
|
72
|
+
"""Create a placeholder location for generated or unknown nodes."""
|
|
73
|
+
return cls(file=file, line=0, col=0, end_line=0, end_col=0)
|
|
74
|
+
|
|
75
|
+
def contains(self, other: "SourceLocation") -> bool:
|
|
76
|
+
"""Check if this location fully contains another location."""
|
|
77
|
+
if self.file != other.file:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
if self.offset is not None and other.offset is not None:
|
|
81
|
+
return (
|
|
82
|
+
self.offset <= other.offset
|
|
83
|
+
and self.end_offset >= other.end_offset
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Fallback to line/col comparison
|
|
87
|
+
start_before = (self.line < other.line) or (
|
|
88
|
+
self.line == other.line and self.col <= other.col
|
|
89
|
+
)
|
|
90
|
+
end_after = (self.end_line > other.end_line) or (
|
|
91
|
+
self.end_line == other.end_line and self.end_col >= other.end_col
|
|
92
|
+
)
|
|
93
|
+
return start_before and end_after
|
|
94
|
+
|
|
95
|
+
def merge(self, other: "SourceLocation") -> "SourceLocation":
|
|
96
|
+
"""Create a new location spanning from this location to another."""
|
|
97
|
+
if self.file != other.file:
|
|
98
|
+
raise ValueError(f"Cannot merge locations from different files: {self.file} and {other.file}")
|
|
99
|
+
|
|
100
|
+
return SourceLocation(
|
|
101
|
+
file=self.file,
|
|
102
|
+
line=min(self.line, other.line),
|
|
103
|
+
col=self.col if self.line <= other.line else other.col,
|
|
104
|
+
end_line=max(self.end_line, other.end_line),
|
|
105
|
+
end_col=self.end_col if self.end_line >= other.end_line else other.end_col,
|
|
106
|
+
offset=min(self.offset, other.offset) if self.offset and other.offset else None,
|
|
107
|
+
end_offset=max(self.end_offset, other.end_offset) if self.end_offset and other.end_offset else None,
|
|
108
|
+
)
|
yuho/parser/wrapper.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parser wrapper class for tree-sitter based Yuho parsing.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, List, Iterator
|
|
8
|
+
import os
|
|
9
|
+
import threading
|
|
10
|
+
|
|
11
|
+
from yuho.parser.source_location import SourceLocation
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Module-level parser cache for performance
|
|
15
|
+
_parser_cache: Optional["Parser"] = None
|
|
16
|
+
_parser_lock = threading.Lock()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ParseError:
|
|
21
|
+
"""
|
|
22
|
+
Represents a syntax error encountered during parsing.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
message: Human-readable error description
|
|
26
|
+
location: Source location where the error occurred
|
|
27
|
+
node_type: The tree-sitter node type that has the error (if available)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
message: str
|
|
31
|
+
location: SourceLocation
|
|
32
|
+
node_type: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
def __str__(self) -> str:
|
|
35
|
+
return f"{self.location}: {self.message}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ParseResult:
|
|
40
|
+
"""
|
|
41
|
+
Result of parsing a Yuho source file.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
tree: The tree-sitter Tree object (or None if parsing failed completely)
|
|
45
|
+
errors: List of syntax errors found during parsing
|
|
46
|
+
source: The original source string
|
|
47
|
+
file: The file path (or "<string>" for inline parsing)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
tree: object # tree_sitter.Tree
|
|
51
|
+
errors: List[ParseError]
|
|
52
|
+
source: str
|
|
53
|
+
file: str
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_valid(self) -> bool:
|
|
57
|
+
"""Return True if the source parsed without errors."""
|
|
58
|
+
return len(self.errors) == 0
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def root_node(self):
|
|
62
|
+
"""Return the root node of the parse tree."""
|
|
63
|
+
return self.tree.root_node if self.tree else None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Parser:
|
|
67
|
+
"""
|
|
68
|
+
Parser for Yuho source files using tree-sitter.
|
|
69
|
+
|
|
70
|
+
This class wraps the tree-sitter parser and provides a convenient
|
|
71
|
+
interface for parsing Yuho source code and extracting syntax errors.
|
|
72
|
+
|
|
73
|
+
Usage:
|
|
74
|
+
parser = Parser()
|
|
75
|
+
result = parser.parse('struct Foo { int x, }')
|
|
76
|
+
if result.is_valid:
|
|
77
|
+
# Process the AST
|
|
78
|
+
root = result.root_node
|
|
79
|
+
else:
|
|
80
|
+
for error in result.errors:
|
|
81
|
+
print(error)
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self):
|
|
85
|
+
"""Initialize the parser with the Yuho language grammar."""
|
|
86
|
+
self._parser = None
|
|
87
|
+
self._language = None
|
|
88
|
+
self._initialized = False
|
|
89
|
+
|
|
90
|
+
def _ensure_initialized(self):
|
|
91
|
+
"""Lazily initialize the tree-sitter parser."""
|
|
92
|
+
if self._initialized:
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
from tree_sitter import Parser as TSParser
|
|
97
|
+
from tree_sitter_yuho import language
|
|
98
|
+
|
|
99
|
+
self._parser = TSParser()
|
|
100
|
+
self._language = language()
|
|
101
|
+
self._parser.set_language(self._language)
|
|
102
|
+
self._initialized = True
|
|
103
|
+
except ImportError as e:
|
|
104
|
+
raise ImportError(
|
|
105
|
+
"tree-sitter and tree-sitter-yuho must be installed. "
|
|
106
|
+
"Run: pip install tree-sitter tree-sitter-yuho"
|
|
107
|
+
) from e
|
|
108
|
+
|
|
109
|
+
def parse(self, source: str, file: str = "<string>") -> ParseResult:
|
|
110
|
+
"""
|
|
111
|
+
Parse a Yuho source string.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
source: The Yuho source code to parse
|
|
115
|
+
file: Optional file path for error messages
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
ParseResult containing the tree and any errors
|
|
119
|
+
"""
|
|
120
|
+
self._ensure_initialized()
|
|
121
|
+
|
|
122
|
+
# Encode source as bytes for tree-sitter
|
|
123
|
+
source_bytes = source.encode("utf-8")
|
|
124
|
+
tree = self._parser.parse(source_bytes)
|
|
125
|
+
|
|
126
|
+
# Collect errors by walking the tree
|
|
127
|
+
errors = list(self._collect_errors(tree.root_node, source, file))
|
|
128
|
+
|
|
129
|
+
return ParseResult(
|
|
130
|
+
tree=tree,
|
|
131
|
+
errors=errors,
|
|
132
|
+
source=source,
|
|
133
|
+
file=file,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def parse_file(self, path: str | Path) -> ParseResult:
|
|
137
|
+
"""
|
|
138
|
+
Parse a Yuho source file.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
path: Path to the .yh file
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
ParseResult containing the tree and any errors
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
FileNotFoundError: If the file does not exist
|
|
148
|
+
UnicodeDecodeError: If the file is not valid UTF-8
|
|
149
|
+
"""
|
|
150
|
+
path = Path(path)
|
|
151
|
+
|
|
152
|
+
if not path.exists():
|
|
153
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
154
|
+
|
|
155
|
+
source = path.read_text(encoding="utf-8")
|
|
156
|
+
return self.parse(source, file=str(path))
|
|
157
|
+
|
|
158
|
+
def _collect_errors(
|
|
159
|
+
self, node, source: str, file: str
|
|
160
|
+
) -> Iterator[ParseError]:
|
|
161
|
+
"""
|
|
162
|
+
Walk the parse tree and collect error nodes.
|
|
163
|
+
|
|
164
|
+
Tree-sitter marks nodes with errors using:
|
|
165
|
+
- is_error: True for ERROR nodes
|
|
166
|
+
- is_missing: True for MISSING nodes (expected but not found)
|
|
167
|
+
- has_error: True if this node or any descendant has errors
|
|
168
|
+
"""
|
|
169
|
+
if node.is_error:
|
|
170
|
+
# This is an ERROR node - unexpected token(s)
|
|
171
|
+
location = SourceLocation.from_tree_sitter_node(node, file)
|
|
172
|
+
error_text = source[node.start_byte : node.end_byte]
|
|
173
|
+
|
|
174
|
+
if len(error_text) > 50:
|
|
175
|
+
error_text = error_text[:47] + "..."
|
|
176
|
+
|
|
177
|
+
yield ParseError(
|
|
178
|
+
message=f"Unexpected syntax: {error_text!r}",
|
|
179
|
+
location=location,
|
|
180
|
+
node_type="ERROR",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
elif node.is_missing:
|
|
184
|
+
# This is a MISSING node - expected syntax not found
|
|
185
|
+
location = SourceLocation.from_tree_sitter_node(node, file)
|
|
186
|
+
|
|
187
|
+
# Generate helpful message based on what's missing
|
|
188
|
+
missing_type = node.type
|
|
189
|
+
message = self._missing_node_message(missing_type)
|
|
190
|
+
|
|
191
|
+
yield ParseError(
|
|
192
|
+
message=message,
|
|
193
|
+
location=location,
|
|
194
|
+
node_type=f"MISSING:{missing_type}",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
elif node.has_error:
|
|
198
|
+
# Recurse into children to find the actual error nodes
|
|
199
|
+
for child in node.children:
|
|
200
|
+
yield from self._collect_errors(child, source, file)
|
|
201
|
+
|
|
202
|
+
def _missing_node_message(self, node_type: str) -> str:
|
|
203
|
+
"""Generate a helpful error message for missing nodes."""
|
|
204
|
+
messages = {
|
|
205
|
+
";": "Missing semicolon",
|
|
206
|
+
",": "Missing comma",
|
|
207
|
+
"{": "Missing opening brace '{'",
|
|
208
|
+
"}": "Missing closing brace '}'",
|
|
209
|
+
"(": "Missing opening parenthesis '('",
|
|
210
|
+
")": "Missing closing parenthesis ')'",
|
|
211
|
+
"[": "Missing opening bracket '['",
|
|
212
|
+
"]": "Missing closing bracket ']'",
|
|
213
|
+
":=": "Missing assignment operator ':='",
|
|
214
|
+
":": "Missing colon ':'",
|
|
215
|
+
"identifier": "Expected identifier",
|
|
216
|
+
"string_literal": "Expected string literal",
|
|
217
|
+
"integer_literal": "Expected integer",
|
|
218
|
+
"_type": "Expected type annotation",
|
|
219
|
+
"_expression": "Expected expression",
|
|
220
|
+
}
|
|
221
|
+
return messages.get(node_type, f"Missing {node_type}")
|
|
222
|
+
|
|
223
|
+
def walk_tree(self, tree) -> Iterator:
|
|
224
|
+
"""
|
|
225
|
+
Generator that yields all nodes in the tree via depth-first traversal.
|
|
226
|
+
|
|
227
|
+
Usage:
|
|
228
|
+
for node in parser.walk_tree(result.tree):
|
|
229
|
+
print(node.type, node.text)
|
|
230
|
+
"""
|
|
231
|
+
cursor = tree.walk()
|
|
232
|
+
|
|
233
|
+
visited_children = False
|
|
234
|
+
while True:
|
|
235
|
+
if not visited_children:
|
|
236
|
+
yield cursor.node
|
|
237
|
+
if cursor.goto_first_child():
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
if cursor.goto_next_sibling():
|
|
241
|
+
visited_children = False
|
|
242
|
+
elif cursor.goto_parent():
|
|
243
|
+
visited_children = True
|
|
244
|
+
else:
|
|
245
|
+
break
|
|
246
|
+
|
|
247
|
+
def query(self, tree, query_string: str) -> List:
|
|
248
|
+
"""
|
|
249
|
+
Run a tree-sitter query against the parse tree.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
tree: The parsed tree (from ParseResult.tree)
|
|
253
|
+
query_string: A tree-sitter query in S-expression format
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
List of (node, capture_name) tuples
|
|
257
|
+
|
|
258
|
+
Usage:
|
|
259
|
+
captures = parser.query(result.tree, '(struct_definition name: (identifier) @name)')
|
|
260
|
+
for node, name in captures:
|
|
261
|
+
print(f"Found struct: {node.text}")
|
|
262
|
+
"""
|
|
263
|
+
self._ensure_initialized()
|
|
264
|
+
|
|
265
|
+
from tree_sitter import Query
|
|
266
|
+
|
|
267
|
+
query = self._language.query(query_string)
|
|
268
|
+
return query.captures(tree.root_node)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def get_parser() -> Parser:
|
|
272
|
+
"""
|
|
273
|
+
Get a cached Parser instance for better performance.
|
|
274
|
+
|
|
275
|
+
This function returns a thread-safe singleton Parser instance.
|
|
276
|
+
Use this instead of creating new Parser() instances when parsing
|
|
277
|
+
multiple files to avoid repeated tree-sitter initialization overhead.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
A cached Parser instance
|
|
281
|
+
|
|
282
|
+
Usage:
|
|
283
|
+
from yuho.parser import get_parser
|
|
284
|
+
parser = get_parser()
|
|
285
|
+
result = parser.parse_file("statute.yh")
|
|
286
|
+
"""
|
|
287
|
+
global _parser_cache
|
|
288
|
+
|
|
289
|
+
if _parser_cache is not None:
|
|
290
|
+
return _parser_cache
|
|
291
|
+
|
|
292
|
+
with _parser_lock:
|
|
293
|
+
# Double-check after acquiring lock
|
|
294
|
+
if _parser_cache is None:
|
|
295
|
+
_parser_cache = Parser()
|
|
296
|
+
_parser_cache._ensure_initialized()
|
|
297
|
+
|
|
298
|
+
return _parser_cache
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def clear_parser_cache() -> None:
|
|
302
|
+
"""
|
|
303
|
+
Clear the cached parser instance.
|
|
304
|
+
|
|
305
|
+
This is mainly useful for testing or when you need to
|
|
306
|
+
reinitialize the parser (e.g., after grammar changes).
|
|
307
|
+
"""
|
|
308
|
+
global _parser_cache
|
|
309
|
+
|
|
310
|
+
with _parser_lock:
|
|
311
|
+
_parser_cache = None
|
yuho/testing/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Yuho testing module - pytest fixtures and utilities for testing Yuho statute implementations.
|
|
3
|
+
|
|
4
|
+
Usage in your tests:
|
|
5
|
+
from yuho.testing import yuho_parser, yuho_ast, parse_statute
|
|
6
|
+
|
|
7
|
+
Or import fixtures directly in conftest.py:
|
|
8
|
+
pytest_plugins = ["yuho.testing.fixtures"]
|
|
9
|
+
|
|
10
|
+
For coverage tracking:
|
|
11
|
+
from yuho.testing import CoverageTracker, analyze_test_coverage
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from yuho.testing.fixtures import (
|
|
15
|
+
yuho_parser,
|
|
16
|
+
yuho_ast,
|
|
17
|
+
parse_statute,
|
|
18
|
+
parse_file,
|
|
19
|
+
statute_validator,
|
|
20
|
+
StatuteTestCase,
|
|
21
|
+
element_check,
|
|
22
|
+
penalty_check,
|
|
23
|
+
)
|
|
24
|
+
from yuho.testing.coverage import (
|
|
25
|
+
CoverageTracker,
|
|
26
|
+
CoverageReport,
|
|
27
|
+
StatuteCoverage,
|
|
28
|
+
ElementCoverage,
|
|
29
|
+
analyze_test_coverage,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# Fixtures
|
|
34
|
+
"yuho_parser",
|
|
35
|
+
"yuho_ast",
|
|
36
|
+
"parse_statute",
|
|
37
|
+
"parse_file",
|
|
38
|
+
"statute_validator",
|
|
39
|
+
"StatuteTestCase",
|
|
40
|
+
"element_check",
|
|
41
|
+
"penalty_check",
|
|
42
|
+
# Coverage
|
|
43
|
+
"CoverageTracker",
|
|
44
|
+
"CoverageReport",
|
|
45
|
+
"StatuteCoverage",
|
|
46
|
+
"ElementCoverage",
|
|
47
|
+
"analyze_test_coverage",
|
|
48
|
+
]
|