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
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Config command - Display and modify Yuho configuration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, List, Dict, Any
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from yuho.config.loader import get_config, load_config, DEFAULT_CONFIG_PATH, create_default_config
|
|
12
|
+
from yuho.cli.error_formatter import Colors, colorize
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfigValidationError(Exception):
|
|
16
|
+
"""Raised when configuration values are invalid."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_config_value(section: str, key: str, value: str) -> Any:
|
|
21
|
+
"""
|
|
22
|
+
Validate and convert a configuration value.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
section: Config section (llm, transpile, lsp, mcp)
|
|
26
|
+
key: Configuration key
|
|
27
|
+
value: String value to validate
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Converted and validated value
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ConfigValidationError: If value is invalid
|
|
34
|
+
"""
|
|
35
|
+
# Type mappings for validation
|
|
36
|
+
int_fields = {
|
|
37
|
+
"llm": ["ollama_port", "max_tokens"],
|
|
38
|
+
"transpile": [],
|
|
39
|
+
"lsp": [],
|
|
40
|
+
"mcp": ["port"],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
float_fields = {
|
|
44
|
+
"llm": ["temperature"],
|
|
45
|
+
"transpile": [],
|
|
46
|
+
"lsp": [],
|
|
47
|
+
"mcp": [],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
bool_fields = {
|
|
51
|
+
"llm": [],
|
|
52
|
+
"transpile": ["include_source_locations"],
|
|
53
|
+
"lsp": ["diagnostic_severity_error", "diagnostic_severity_warning",
|
|
54
|
+
"diagnostic_severity_info", "diagnostic_severity_hint"],
|
|
55
|
+
"mcp": [],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
list_fields = {
|
|
59
|
+
"llm": ["fallback_providers"],
|
|
60
|
+
"transpile": [],
|
|
61
|
+
"lsp": ["completion_trigger_chars"],
|
|
62
|
+
"mcp": ["allowed_origins"],
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Enum validations
|
|
66
|
+
enum_fields = {
|
|
67
|
+
("llm", "provider"): ["ollama", "huggingface", "openai", "anthropic"],
|
|
68
|
+
("transpile", "default_target"): ["json", "jsonld", "english", "mermaid", "alloy", "latex"],
|
|
69
|
+
("transpile", "latex_compiler"): ["pdflatex", "xelatex", "lualatex"],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Range validations
|
|
73
|
+
range_fields = {
|
|
74
|
+
("llm", "ollama_port"): (1, 65535),
|
|
75
|
+
("llm", "max_tokens"): (1, 100000),
|
|
76
|
+
("llm", "temperature"): (0.0, 2.0),
|
|
77
|
+
("mcp", "port"): (1, 65535),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if section not in ["llm", "transpile", "lsp", "mcp"]:
|
|
81
|
+
raise ConfigValidationError(f"Unknown configuration section: '{section}'. Valid sections: llm, transpile, lsp, mcp")
|
|
82
|
+
|
|
83
|
+
# Check if key exists in section
|
|
84
|
+
valid_keys = {
|
|
85
|
+
"llm": ["provider", "model", "ollama_host", "ollama_port", "huggingface_cache",
|
|
86
|
+
"openai_api_key", "anthropic_api_key", "max_tokens", "temperature", "fallback_providers"],
|
|
87
|
+
"transpile": ["default_target", "latex_compiler", "output_dir", "include_source_locations"],
|
|
88
|
+
"lsp": ["diagnostic_severity_error", "diagnostic_severity_warning",
|
|
89
|
+
"diagnostic_severity_info", "diagnostic_severity_hint", "completion_trigger_chars"],
|
|
90
|
+
"mcp": ["host", "port", "allowed_origins", "auth_token"],
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if key not in valid_keys.get(section, []):
|
|
94
|
+
raise ConfigValidationError(
|
|
95
|
+
f"Unknown key '{key}' in section [{section}]. Valid keys: {', '.join(valid_keys[section])}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Type conversion and validation
|
|
99
|
+
if key in int_fields.get(section, []):
|
|
100
|
+
try:
|
|
101
|
+
converted = int(value)
|
|
102
|
+
except ValueError:
|
|
103
|
+
raise ConfigValidationError(f"'{key}' must be an integer, got: '{value}'")
|
|
104
|
+
|
|
105
|
+
# Range check
|
|
106
|
+
if (section, key) in range_fields:
|
|
107
|
+
min_val, max_val = range_fields[(section, key)]
|
|
108
|
+
if not (min_val <= converted <= max_val):
|
|
109
|
+
raise ConfigValidationError(f"'{key}' must be between {min_val} and {max_val}, got: {converted}")
|
|
110
|
+
|
|
111
|
+
return converted
|
|
112
|
+
|
|
113
|
+
elif key in float_fields.get(section, []):
|
|
114
|
+
try:
|
|
115
|
+
converted = float(value)
|
|
116
|
+
except ValueError:
|
|
117
|
+
raise ConfigValidationError(f"'{key}' must be a number, got: '{value}'")
|
|
118
|
+
|
|
119
|
+
# Range check
|
|
120
|
+
if (section, key) in range_fields:
|
|
121
|
+
min_val, max_val = range_fields[(section, key)]
|
|
122
|
+
if not (min_val <= converted <= max_val):
|
|
123
|
+
raise ConfigValidationError(f"'{key}' must be between {min_val} and {max_val}, got: {converted}")
|
|
124
|
+
|
|
125
|
+
return converted
|
|
126
|
+
|
|
127
|
+
elif key in bool_fields.get(section, []):
|
|
128
|
+
lower = value.lower()
|
|
129
|
+
if lower in ("true", "1", "yes", "on"):
|
|
130
|
+
return True
|
|
131
|
+
elif lower in ("false", "0", "no", "off"):
|
|
132
|
+
return False
|
|
133
|
+
else:
|
|
134
|
+
raise ConfigValidationError(f"'{key}' must be a boolean (true/false), got: '{value}'")
|
|
135
|
+
|
|
136
|
+
elif key in list_fields.get(section, []):
|
|
137
|
+
# Parse comma-separated list
|
|
138
|
+
items = [item.strip() for item in value.split(",") if item.strip()]
|
|
139
|
+
if not items:
|
|
140
|
+
raise ConfigValidationError(f"'{key}' must be a non-empty list")
|
|
141
|
+
return items
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
# String field - check enum if applicable
|
|
145
|
+
if (section, key) in enum_fields:
|
|
146
|
+
valid_values = enum_fields[(section, key)]
|
|
147
|
+
if value not in valid_values:
|
|
148
|
+
raise ConfigValidationError(
|
|
149
|
+
f"'{key}' must be one of: {', '.join(valid_values)}. Got: '{value}'"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return value
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def run_config_show(section: Optional[str] = None, format: str = "toml", verbose: bool = False) -> None:
|
|
156
|
+
"""
|
|
157
|
+
Display current configuration.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
section: Optional section to show (llm, transpile, lsp, mcp)
|
|
161
|
+
format: Output format (toml, json)
|
|
162
|
+
verbose: Show additional info
|
|
163
|
+
"""
|
|
164
|
+
config = get_config()
|
|
165
|
+
config_dict = config.to_dict()
|
|
166
|
+
|
|
167
|
+
# Filter to specific section if requested
|
|
168
|
+
if section:
|
|
169
|
+
if section not in config_dict:
|
|
170
|
+
click.echo(colorize(f"error: Unknown section '{section}'", Colors.RED), err=True)
|
|
171
|
+
click.echo(f"Valid sections: {', '.join(config_dict.keys())}", err=True)
|
|
172
|
+
sys.exit(1)
|
|
173
|
+
config_dict = {section: config_dict[section]}
|
|
174
|
+
|
|
175
|
+
if verbose:
|
|
176
|
+
click.echo(colorize(f"# Config loaded from: {DEFAULT_CONFIG_PATH}", Colors.DIM))
|
|
177
|
+
click.echo()
|
|
178
|
+
|
|
179
|
+
if format == "json":
|
|
180
|
+
import json
|
|
181
|
+
click.echo(json.dumps(config_dict, indent=2))
|
|
182
|
+
else:
|
|
183
|
+
# TOML format
|
|
184
|
+
for sect_name, sect_values in config_dict.items():
|
|
185
|
+
click.echo(colorize(f"[{sect_name}]", Colors.CYAN + Colors.BOLD))
|
|
186
|
+
for key, value in sect_values.items():
|
|
187
|
+
if isinstance(value, list):
|
|
188
|
+
value_str = f"[{', '.join(repr(v) for v in value)}]"
|
|
189
|
+
elif isinstance(value, str):
|
|
190
|
+
value_str = f'"{value}"'
|
|
191
|
+
elif isinstance(value, bool):
|
|
192
|
+
value_str = "true" if value else "false"
|
|
193
|
+
elif value is None:
|
|
194
|
+
value_str = colorize("# not set", Colors.DIM)
|
|
195
|
+
else:
|
|
196
|
+
value_str = str(value)
|
|
197
|
+
click.echo(f"{key} = {value_str}")
|
|
198
|
+
click.echo()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def run_config_set(key_path: str, value: str, verbose: bool = False) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Set a configuration value.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
key_path: Key path like "llm.provider" or "mcp.port"
|
|
207
|
+
value: Value to set
|
|
208
|
+
verbose: Enable verbose output
|
|
209
|
+
"""
|
|
210
|
+
# Parse key path
|
|
211
|
+
parts = key_path.split(".")
|
|
212
|
+
if len(parts) != 2:
|
|
213
|
+
click.echo(colorize(
|
|
214
|
+
f"error: Key must be in format 'section.key' (e.g., 'llm.provider')",
|
|
215
|
+
Colors.RED
|
|
216
|
+
), err=True)
|
|
217
|
+
sys.exit(1)
|
|
218
|
+
|
|
219
|
+
section, key = parts
|
|
220
|
+
|
|
221
|
+
# Validate the value
|
|
222
|
+
try:
|
|
223
|
+
validated_value = validate_config_value(section, key, value)
|
|
224
|
+
except ConfigValidationError as e:
|
|
225
|
+
click.echo(colorize(f"error: {e}", Colors.RED), err=True)
|
|
226
|
+
sys.exit(1)
|
|
227
|
+
|
|
228
|
+
# Load or create config file
|
|
229
|
+
if not DEFAULT_CONFIG_PATH.exists():
|
|
230
|
+
if verbose:
|
|
231
|
+
click.echo(colorize(f"Creating config file: {DEFAULT_CONFIG_PATH}", Colors.YELLOW))
|
|
232
|
+
create_default_config()
|
|
233
|
+
|
|
234
|
+
# Read current config
|
|
235
|
+
try:
|
|
236
|
+
import tomllib
|
|
237
|
+
except ImportError:
|
|
238
|
+
try:
|
|
239
|
+
import tomli as tomllib
|
|
240
|
+
except ImportError:
|
|
241
|
+
click.echo(colorize("error: tomllib/tomli not available", Colors.RED), err=True)
|
|
242
|
+
sys.exit(1)
|
|
243
|
+
|
|
244
|
+
with open(DEFAULT_CONFIG_PATH, "rb") as f:
|
|
245
|
+
config_data = tomllib.load(f)
|
|
246
|
+
|
|
247
|
+
# Update value
|
|
248
|
+
if section not in config_data:
|
|
249
|
+
config_data[section] = {}
|
|
250
|
+
config_data[section][key] = validated_value
|
|
251
|
+
|
|
252
|
+
# Write back using tomlkit for formatting preservation
|
|
253
|
+
try:
|
|
254
|
+
import tomlkit
|
|
255
|
+
with open(DEFAULT_CONFIG_PATH, "r") as f:
|
|
256
|
+
doc = tomlkit.parse(f.read())
|
|
257
|
+
|
|
258
|
+
if section not in doc:
|
|
259
|
+
doc[section] = tomlkit.table()
|
|
260
|
+
doc[section][key] = validated_value
|
|
261
|
+
|
|
262
|
+
with open(DEFAULT_CONFIG_PATH, "w") as f:
|
|
263
|
+
f.write(tomlkit.dumps(doc))
|
|
264
|
+
except ImportError:
|
|
265
|
+
# Fall back to basic TOML writing
|
|
266
|
+
_write_config_basic(config_data)
|
|
267
|
+
|
|
268
|
+
if verbose:
|
|
269
|
+
click.echo(colorize(f"Updated {key_path} = {validated_value}", Colors.GREEN))
|
|
270
|
+
else:
|
|
271
|
+
click.echo(f"Set {key_path} = {validated_value}")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def run_config_init(force: bool = False, verbose: bool = False) -> None:
|
|
275
|
+
"""
|
|
276
|
+
Create a default configuration file.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
force: Overwrite existing file
|
|
280
|
+
verbose: Enable verbose output
|
|
281
|
+
"""
|
|
282
|
+
if DEFAULT_CONFIG_PATH.exists() and not force:
|
|
283
|
+
click.echo(colorize(f"Config file already exists: {DEFAULT_CONFIG_PATH}", Colors.YELLOW))
|
|
284
|
+
click.echo("Use --force to overwrite.")
|
|
285
|
+
sys.exit(1)
|
|
286
|
+
|
|
287
|
+
path = create_default_config()
|
|
288
|
+
click.echo(colorize(f"Created config file: {path}", Colors.GREEN))
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _write_config_basic(config_data: Dict[str, Any]) -> None:
|
|
292
|
+
"""Write config using basic TOML formatting."""
|
|
293
|
+
lines = []
|
|
294
|
+
|
|
295
|
+
for section, values in config_data.items():
|
|
296
|
+
lines.append(f"[{section}]")
|
|
297
|
+
for key, value in values.items():
|
|
298
|
+
if isinstance(value, list):
|
|
299
|
+
value_str = f"[{', '.join(repr(v) for v in value)}]"
|
|
300
|
+
elif isinstance(value, str):
|
|
301
|
+
value_str = f'"{value}"'
|
|
302
|
+
elif isinstance(value, bool):
|
|
303
|
+
value_str = "true" if value else "false"
|
|
304
|
+
elif value is None:
|
|
305
|
+
continue # Skip None values
|
|
306
|
+
else:
|
|
307
|
+
value_str = str(value)
|
|
308
|
+
lines.append(f"{key} = {value_str}")
|
|
309
|
+
lines.append("")
|
|
310
|
+
|
|
311
|
+
DEFAULT_CONFIG_PATH.write_text("\n".join(lines))
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Contribute command - validate and package statutes for sharing.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import tarfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from yuho.parser import Parser
|
|
14
|
+
from yuho.ast import ASTBuilder
|
|
15
|
+
from yuho.cli.error_formatter import Colors, colorize
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_contribute(file: str, package: bool = False, output: Optional[str] = None, verbose: bool = False) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Validate a statute file for contribution.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
file: Path to the .yh file
|
|
24
|
+
package: Create distributable package
|
|
25
|
+
output: Output path for package
|
|
26
|
+
verbose: Enable verbose output
|
|
27
|
+
"""
|
|
28
|
+
file_path = Path(file)
|
|
29
|
+
|
|
30
|
+
if verbose:
|
|
31
|
+
click.echo(f"Validating {file_path}...")
|
|
32
|
+
|
|
33
|
+
# Parse and validate
|
|
34
|
+
parser = Parser()
|
|
35
|
+
try:
|
|
36
|
+
result = parser.parse_file(file_path)
|
|
37
|
+
except FileNotFoundError:
|
|
38
|
+
click.echo(colorize(f"error: File not found: {file}", Colors.RED), err=True)
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
|
|
41
|
+
if result.errors:
|
|
42
|
+
click.echo(colorize(f"FAIL: Parse errors in {file}", Colors.RED))
|
|
43
|
+
for err in result.errors:
|
|
44
|
+
click.echo(f" {err.location}: {err.message}")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
builder = ASTBuilder(result.source, str(file_path))
|
|
48
|
+
ast = builder.build(result.root_node)
|
|
49
|
+
|
|
50
|
+
# Check for required metadata
|
|
51
|
+
if not ast.statutes:
|
|
52
|
+
click.echo(colorize("FAIL: No statute definitions found", Colors.RED))
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
# Check for associated tests
|
|
56
|
+
test_paths = [
|
|
57
|
+
file_path.parent / f"test_{file_path.name}",
|
|
58
|
+
file_path.parent / "tests" / f"{file_path.stem}_test.yh",
|
|
59
|
+
]
|
|
60
|
+
test_file = None
|
|
61
|
+
for tp in test_paths:
|
|
62
|
+
if tp.exists():
|
|
63
|
+
test_file = tp
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
if not test_file:
|
|
67
|
+
click.echo(colorize("WARNING: No test file found", Colors.YELLOW))
|
|
68
|
+
click.echo(f" Expected: test_{file_path.name} or tests/{file_path.stem}_test.yh")
|
|
69
|
+
|
|
70
|
+
# Check for metadata.toml
|
|
71
|
+
metadata_path = file_path.parent / "metadata.toml"
|
|
72
|
+
has_metadata = metadata_path.exists()
|
|
73
|
+
if not has_metadata:
|
|
74
|
+
click.echo(colorize("WARNING: No metadata.toml found", Colors.YELLOW))
|
|
75
|
+
|
|
76
|
+
# Summary
|
|
77
|
+
click.echo(colorize(f"OK: {file_path} is valid for contribution", Colors.CYAN + Colors.BOLD))
|
|
78
|
+
click.echo(f" Statutes: {len(ast.statutes)}")
|
|
79
|
+
for statute in ast.statutes:
|
|
80
|
+
title = statute.title.value if statute.title else "(untitled)"
|
|
81
|
+
click.echo(f" - Section {statute.section_number}: {title}")
|
|
82
|
+
|
|
83
|
+
if test_file:
|
|
84
|
+
click.echo(f" Tests: {test_file}")
|
|
85
|
+
if has_metadata:
|
|
86
|
+
click.echo(f" Metadata: {metadata_path}")
|
|
87
|
+
|
|
88
|
+
# Package if requested
|
|
89
|
+
if package:
|
|
90
|
+
_create_package(file_path, test_file, metadata_path if has_metadata else None, output, verbose)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _create_package(
|
|
94
|
+
statute_file: Path,
|
|
95
|
+
test_file: Optional[Path],
|
|
96
|
+
metadata_file: Optional[Path],
|
|
97
|
+
output: Optional[str],
|
|
98
|
+
verbose: bool
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Create a .yhpkg archive."""
|
|
101
|
+
# Determine output path
|
|
102
|
+
if output:
|
|
103
|
+
pkg_path = Path(output)
|
|
104
|
+
else:
|
|
105
|
+
pkg_path = statute_file.parent / f"{statute_file.stem}.yhpkg"
|
|
106
|
+
|
|
107
|
+
if verbose:
|
|
108
|
+
click.echo(f"Creating package: {pkg_path}")
|
|
109
|
+
|
|
110
|
+
with tarfile.open(pkg_path, "w:gz") as tar:
|
|
111
|
+
# Add statute file
|
|
112
|
+
tar.add(statute_file, arcname=statute_file.name)
|
|
113
|
+
|
|
114
|
+
# Add test file if exists
|
|
115
|
+
if test_file:
|
|
116
|
+
tar.add(test_file, arcname=f"tests/{test_file.name}")
|
|
117
|
+
|
|
118
|
+
# Add metadata if exists
|
|
119
|
+
if metadata_file:
|
|
120
|
+
tar.add(metadata_file, arcname="metadata.toml")
|
|
121
|
+
|
|
122
|
+
click.echo(colorize(f"Package created: {pkg_path}", Colors.CYAN + Colors.BOLD))
|