echolang 0.8.9__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.
- echo/__init__.py +3 -0
- echo/__main__.py +4 -0
- echo/cli/__init__.py +3 -0
- echo/cli/main.py +470 -0
- echo/cli/test_runner.py +287 -0
- echo/core/__init__.py +1 -0
- echo/core/hashes.py +45 -0
- echo/core/jsonutil.py +74 -0
- echo/core/lists.py +103 -0
- echo/core/strings.py +152 -0
- echo/errors.py +113 -0
- echo/formatter.py +563 -0
- echo/frontend/__init__.py +5 -0
- echo/frontend/ast/__init__.py +1 -0
- echo/frontend/ast/nodes.py +456 -0
- echo/frontend/lexer.py +332 -0
- echo/frontend/parser.py +1376 -0
- echo/frontend/tokens.py +163 -0
- echo/linter.py +543 -0
- echo/modules/__init__.py +6 -0
- echo/modules/graph.py +88 -0
- echo/modules/loader.py +194 -0
- echo/modules/records.py +27 -0
- echo/modules/resolver.py +31 -0
- echo/runtime/__init__.py +3 -0
- echo/runtime/builtin_types.py +107 -0
- echo/runtime/builtins.py +1130 -0
- echo/runtime/class_registry.py +47 -0
- echo/runtime/context.py +182 -0
- echo/runtime/errors.py +17 -0
- echo/runtime/freeze.py +37 -0
- echo/runtime/functions.py +198 -0
- echo/runtime/host.py +85 -0
- echo/runtime/instances.py +9 -0
- echo/runtime/interpreter.py +1744 -0
- echo/runtime/operators.py +150 -0
- echo/runtime/testing.py +59 -0
- echo/runtime/values.py +589 -0
- echo/semantics/__init__.py +4 -0
- echo/semantics/analyzer.py +1814 -0
- echo/semantics/errors.py +3 -0
- echo/semantics/modules.py +15 -0
- echo/semantics/scope.py +67 -0
- echo/semantics/symbols.py +30 -0
- echo_cli.py +5 -0
- echolang-0.8.9.dist-info/METADATA +106 -0
- echolang-0.8.9.dist-info/RECORD +52 -0
- echolang-0.8.9.dist-info/WHEEL +5 -0
- echolang-0.8.9.dist-info/entry_points.txt +4 -0
- echolang-0.8.9.dist-info/licenses/LICENSE +21 -0
- echolang-0.8.9.dist-info/top_level.txt +3 -0
- main.py +9 -0
echo/__init__.py
ADDED
echo/__main__.py
ADDED
echo/cli/__init__.py
ADDED
echo/cli/main.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from echo import __version__
|
|
8
|
+
from echo.errors import (
|
|
9
|
+
ArgumentError,
|
|
10
|
+
EchoError,
|
|
11
|
+
EchoExit,
|
|
12
|
+
EchoIndexError,
|
|
13
|
+
EchoNameError,
|
|
14
|
+
EchoTypeError,
|
|
15
|
+
LexError,
|
|
16
|
+
ModuleLoadError,
|
|
17
|
+
MutationError,
|
|
18
|
+
ParseError,
|
|
19
|
+
SemanticError,
|
|
20
|
+
SourceLocation,
|
|
21
|
+
format_diagnostic,
|
|
22
|
+
)
|
|
23
|
+
from echo.formatter import format_source
|
|
24
|
+
from echo.linter import LintFinding, lint_source
|
|
25
|
+
from echo.frontend.ast.nodes import ImportDeclaration, Program
|
|
26
|
+
from echo.frontend.lexer import Lexer
|
|
27
|
+
from echo.frontend.parser import Parser
|
|
28
|
+
from echo.frontend.tokens import TokenType
|
|
29
|
+
from echo.modules.loader import ModuleLoader
|
|
30
|
+
from echo.modules.records import Module
|
|
31
|
+
from echo.runtime.context import Environment
|
|
32
|
+
from echo.runtime.functions import EchoFunction
|
|
33
|
+
from echo.runtime.host import Host
|
|
34
|
+
from echo.runtime.interpreter import Interpreter
|
|
35
|
+
from echo.semantics.analyzer import SemanticAnalyzer
|
|
36
|
+
from echo.semantics.modules import ModuleSymbols
|
|
37
|
+
from echo.semantics.scope import Scope
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
from rich.console import Console
|
|
41
|
+
from rich.panel import Panel
|
|
42
|
+
except ImportError:
|
|
43
|
+
Console = None
|
|
44
|
+
Panel = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run_source(source: str, filename: str = "<input>", *, plain: bool = True, host: Host | None = None) -> int:
|
|
48
|
+
try:
|
|
49
|
+
tokens = Lexer().tokenize(source, filename=filename)
|
|
50
|
+
program = Parser(tokens).parse()
|
|
51
|
+
SemanticAnalyzer().analyze(program)
|
|
52
|
+
Interpreter(host=host).execute(program)
|
|
53
|
+
return 0
|
|
54
|
+
except EchoExit as exc:
|
|
55
|
+
return exc.code
|
|
56
|
+
except EchoError as exc:
|
|
57
|
+
_print_error(exc, source, plain)
|
|
58
|
+
return 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def check_file(source_path: str, plain: bool = False) -> int:
|
|
62
|
+
file_path = Path(source_path).expanduser().resolve()
|
|
63
|
+
if not file_path.exists() or not file_path.is_file():
|
|
64
|
+
_print_plain_error("Error", f"source file not found: {file_path}", plain)
|
|
65
|
+
return 1
|
|
66
|
+
source = file_path.read_text(encoding="utf-8")
|
|
67
|
+
try:
|
|
68
|
+
tokens = Lexer().tokenize(source, filename=str(file_path))
|
|
69
|
+
program = Parser(tokens).parse()
|
|
70
|
+
if _has_imports(program):
|
|
71
|
+
ModuleLoader().check(file_path)
|
|
72
|
+
else:
|
|
73
|
+
SemanticAnalyzer().analyze(program)
|
|
74
|
+
return 0
|
|
75
|
+
except EchoError as exc:
|
|
76
|
+
_print_error(exc, _error_source(exc, source), plain)
|
|
77
|
+
return 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def run_file(source_path: str, plain: bool = False, host: Host | None = None) -> int:
|
|
81
|
+
file_path = Path(source_path).expanduser().resolve()
|
|
82
|
+
if not file_path.exists() or not file_path.is_file():
|
|
83
|
+
_print_plain_error("Error", f"source file not found: {file_path}", plain)
|
|
84
|
+
return 1
|
|
85
|
+
source = file_path.read_text(encoding="utf-8")
|
|
86
|
+
try:
|
|
87
|
+
tokens = Lexer().tokenize(source, filename=str(file_path))
|
|
88
|
+
program = Parser(tokens).parse()
|
|
89
|
+
if _has_imports(program):
|
|
90
|
+
ModuleLoader().load(file_path, host=host)
|
|
91
|
+
else:
|
|
92
|
+
SemanticAnalyzer().analyze(program)
|
|
93
|
+
Interpreter(host=host).execute(program)
|
|
94
|
+
return 0
|
|
95
|
+
except EchoExit as exc:
|
|
96
|
+
return exc.code
|
|
97
|
+
except EchoError as exc:
|
|
98
|
+
_print_error(exc, _error_source(exc, source), plain)
|
|
99
|
+
return 1
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _has_imports(program: Program) -> bool:
|
|
103
|
+
return any(isinstance(statement, ImportDeclaration) for statement in program.statements)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _error_source(error: EchoError, fallback: str) -> str:
|
|
107
|
+
if error.location is None or error.location.filename is None:
|
|
108
|
+
return fallback
|
|
109
|
+
origin = Path(error.location.filename)
|
|
110
|
+
if not origin.is_file():
|
|
111
|
+
return fallback
|
|
112
|
+
return origin.read_text(encoding="utf-8")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _category(error: EchoError) -> str:
|
|
116
|
+
if isinstance(error, LexError):
|
|
117
|
+
return "Syntax Error"
|
|
118
|
+
if isinstance(error, ParseError):
|
|
119
|
+
return "Syntax Error"
|
|
120
|
+
if isinstance(error, SemanticError):
|
|
121
|
+
return "Semantic Error"
|
|
122
|
+
if isinstance(error, EchoNameError):
|
|
123
|
+
return "Name Error"
|
|
124
|
+
if isinstance(error, EchoTypeError):
|
|
125
|
+
return "Type Error"
|
|
126
|
+
if isinstance(error, ArgumentError):
|
|
127
|
+
return "Argument Error"
|
|
128
|
+
if isinstance(error, EchoIndexError):
|
|
129
|
+
return "Index Error"
|
|
130
|
+
if isinstance(error, MutationError):
|
|
131
|
+
return "Mutation Error"
|
|
132
|
+
return "Execution Error"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _print_error(error: EchoError, source: str, plain: bool) -> None:
|
|
136
|
+
help_text = error.help_text
|
|
137
|
+
error.help_text = None
|
|
138
|
+
message = format_diagnostic(error, source)
|
|
139
|
+
error.help_text = help_text
|
|
140
|
+
title = _category(error)
|
|
141
|
+
_print_plain_error(title, message, plain)
|
|
142
|
+
if help_text:
|
|
143
|
+
_print_plain_error("Hint", help_text, plain)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _print_plain_error(title: str, message: str, plain: bool) -> None:
|
|
147
|
+
if not plain and Console is not None and Panel is not None:
|
|
148
|
+
Console().print(Panel(message, title=title, border_style="red", expand=False))
|
|
149
|
+
return
|
|
150
|
+
print(f"{title}: {message}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def main(argv: list[str] | None = None) -> int:
|
|
154
|
+
raw = list(sys.argv[1:] if argv is None else argv)
|
|
155
|
+
if raw[:1] == ["check"]:
|
|
156
|
+
return _main_check(raw[1:])
|
|
157
|
+
if raw[:1] == ["test"]:
|
|
158
|
+
return _main_test(raw[1:])
|
|
159
|
+
if raw[:1] == ["fmt"]:
|
|
160
|
+
return _main_fmt(raw[1:])
|
|
161
|
+
if raw[:1] == ["lint"]:
|
|
162
|
+
return _main_lint(raw[1:])
|
|
163
|
+
parser = argparse.ArgumentParser(description="Run an Echo source file")
|
|
164
|
+
parser.add_argument("source", nargs="?", help="Path to .echo source file")
|
|
165
|
+
parser.add_argument("--plain", action="store_true", help="Disable Rich styling and use plain text output")
|
|
166
|
+
parser.add_argument("--version", action="store_true", help="Print the Echo version and exit")
|
|
167
|
+
if "--" in raw:
|
|
168
|
+
split_at = raw.index("--")
|
|
169
|
+
interpreter_argv, program_args = raw[:split_at], raw[split_at + 1 :]
|
|
170
|
+
args, unknown = parser.parse_known_args(interpreter_argv)
|
|
171
|
+
if unknown:
|
|
172
|
+
parser.error(f"unrecognized arguments: {' '.join(unknown)}")
|
|
173
|
+
else:
|
|
174
|
+
args, program_args = parser.parse_known_args(raw)
|
|
175
|
+
|
|
176
|
+
if args.version:
|
|
177
|
+
print(f"Echo {__version__}")
|
|
178
|
+
return 0
|
|
179
|
+
host = Host(args=list(program_args))
|
|
180
|
+
if not args.source:
|
|
181
|
+
return run_repl(plain=args.plain, host=host)
|
|
182
|
+
return run_file(args.source, plain=args.plain, host=host)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def run_repl(*, plain: bool = True, host: Host | None = None) -> int:
|
|
186
|
+
host = host or Host()
|
|
187
|
+
interpreter = Interpreter(host=host)
|
|
188
|
+
env = Environment()
|
|
189
|
+
session_scope = SemanticAnalyzer.module_scope(SourceLocation(1, 1, "<repl>"))
|
|
190
|
+
loader = ModuleLoader()
|
|
191
|
+
print(f"Echo {__version__}")
|
|
192
|
+
buffer: list[str] = []
|
|
193
|
+
while True:
|
|
194
|
+
prompt = "echo> " if not buffer else "... "
|
|
195
|
+
try:
|
|
196
|
+
line = input(prompt)
|
|
197
|
+
except EOFError:
|
|
198
|
+
print()
|
|
199
|
+
return 0
|
|
200
|
+
except KeyboardInterrupt:
|
|
201
|
+
print()
|
|
202
|
+
buffer.clear()
|
|
203
|
+
continue
|
|
204
|
+
if not buffer and not line.strip():
|
|
205
|
+
continue
|
|
206
|
+
buffer.append(line)
|
|
207
|
+
source = "\n".join(buffer)
|
|
208
|
+
if _repl_source_incomplete(source):
|
|
209
|
+
continue
|
|
210
|
+
buffer.clear()
|
|
211
|
+
try:
|
|
212
|
+
session_scope = _run_repl_snippet(source, interpreter, env, session_scope, loader)
|
|
213
|
+
except EchoExit as exc:
|
|
214
|
+
return exc.code
|
|
215
|
+
except EchoError as exc:
|
|
216
|
+
_print_error(exc, source, plain)
|
|
217
|
+
except KeyboardInterrupt:
|
|
218
|
+
print()
|
|
219
|
+
except Exception:
|
|
220
|
+
_print_plain_error("Execution Error", "unexpected error", plain)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _run_repl_snippet(
|
|
224
|
+
source: str,
|
|
225
|
+
interpreter: Interpreter,
|
|
226
|
+
env: Environment,
|
|
227
|
+
session_scope: Scope,
|
|
228
|
+
loader: ModuleLoader,
|
|
229
|
+
) -> Scope:
|
|
230
|
+
tokens = Lexer().tokenize(source, filename="<repl>")
|
|
231
|
+
program = Parser(tokens).parse()
|
|
232
|
+
snippet_scope = session_scope.copy()
|
|
233
|
+
dependencies = _repl_import_dependencies(program, loader)
|
|
234
|
+
SemanticAnalyzer().analyze(program, dependencies=dependencies, scope=snippet_scope)
|
|
235
|
+
_repl_bind_imports(program, loader, env, interpreter.host)
|
|
236
|
+
interpreter.execute(program, env)
|
|
237
|
+
return snippet_scope
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _repl_import_dependencies(program: Program, loader: ModuleLoader) -> dict[str, ModuleSymbols]:
|
|
241
|
+
if not _has_imports(program):
|
|
242
|
+
return {}
|
|
243
|
+
importer = Path.cwd() / "<repl>"
|
|
244
|
+
dependencies: dict[str, ModuleSymbols] = {}
|
|
245
|
+
for statement in program.statements:
|
|
246
|
+
if not isinstance(statement, ImportDeclaration) or statement.module in dependencies:
|
|
247
|
+
continue
|
|
248
|
+
path = loader.resolver.resolve(importer, statement.module)
|
|
249
|
+
module = loader.check(path)
|
|
250
|
+
dependencies[statement.module] = SemanticAnalyzer().collect_symbols(module.ast)
|
|
251
|
+
return dependencies
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _repl_bind_imports(program: Program, loader: ModuleLoader, env: Environment, host: Host) -> None:
|
|
255
|
+
if not _has_imports(program):
|
|
256
|
+
return
|
|
257
|
+
importer = Path.cwd() / "<repl>"
|
|
258
|
+
loaded: dict[str, Module] = {}
|
|
259
|
+
for statement in program.statements:
|
|
260
|
+
if not isinstance(statement, ImportDeclaration):
|
|
261
|
+
continue
|
|
262
|
+
if statement.module not in loaded:
|
|
263
|
+
path = loader.resolver.resolve(importer, statement.module)
|
|
264
|
+
loaded[statement.module] = loader.load(path, host=host)
|
|
265
|
+
dependency = loaded[statement.module]
|
|
266
|
+
if statement.name in dependency.class_exports:
|
|
267
|
+
continue
|
|
268
|
+
value = _repl_export_value(dependency, statement.name)
|
|
269
|
+
if isinstance(value, EchoFunction):
|
|
270
|
+
env.define_function(statement.name, value)
|
|
271
|
+
module_env = dependency.env
|
|
272
|
+
is_const = bool(module_env.const.get(statement.name, False)) if module_env is not None else False
|
|
273
|
+
env.define(statement.name, value, mutable=False, const=is_const)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _repl_export_value(module: Module, name: str) -> object:
|
|
277
|
+
module_env = module.env
|
|
278
|
+
if module_env is None:
|
|
279
|
+
raise ModuleLoadError(
|
|
280
|
+
f"module '{module.path.name}' is not fully initialized",
|
|
281
|
+
code="E3005",
|
|
282
|
+
)
|
|
283
|
+
if name in module_env.values:
|
|
284
|
+
return module_env.values[name]
|
|
285
|
+
function = module_env.functions.get(name)
|
|
286
|
+
if function is not None:
|
|
287
|
+
return function
|
|
288
|
+
raise ModuleLoadError(
|
|
289
|
+
f"module '{module.path.name}' has no export '{name}'",
|
|
290
|
+
code="E3005",
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _repl_source_incomplete(source: str) -> bool:
|
|
295
|
+
try:
|
|
296
|
+
tokens = Lexer().tokenize(source, filename="<repl>")
|
|
297
|
+
except LexError as exc:
|
|
298
|
+
return "closing \"\"\"" in exc.message or "closing '''" in exc.message
|
|
299
|
+
except EchoError:
|
|
300
|
+
return False
|
|
301
|
+
braces = 0
|
|
302
|
+
parens = 0
|
|
303
|
+
brackets = 0
|
|
304
|
+
for token in tokens:
|
|
305
|
+
if token.type is TokenType.COMMENT:
|
|
306
|
+
continue
|
|
307
|
+
if token.type is TokenType.LEFT_BRACE:
|
|
308
|
+
braces += 1
|
|
309
|
+
elif token.type is TokenType.RIGHT_BRACE:
|
|
310
|
+
braces -= 1
|
|
311
|
+
if braces < 0:
|
|
312
|
+
return False
|
|
313
|
+
elif token.type is TokenType.LEFT_PAREN:
|
|
314
|
+
parens += 1
|
|
315
|
+
elif token.type is TokenType.RIGHT_PAREN:
|
|
316
|
+
parens -= 1
|
|
317
|
+
if parens < 0:
|
|
318
|
+
return False
|
|
319
|
+
elif token.type is TokenType.LEFT_BRACKET:
|
|
320
|
+
brackets += 1
|
|
321
|
+
elif token.type is TokenType.RIGHT_BRACKET:
|
|
322
|
+
brackets -= 1
|
|
323
|
+
if brackets < 0:
|
|
324
|
+
return False
|
|
325
|
+
return braces > 0 or parens > 0 or brackets > 0
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _main_check(argv: list[str]) -> int:
|
|
329
|
+
parser = argparse.ArgumentParser(prog="echo check", description="Analyze Echo source files without running them")
|
|
330
|
+
parser.add_argument("paths", nargs="*", help="Files or directories of .echo sources")
|
|
331
|
+
parser.add_argument("--plain", action="store_true", help="Disable Rich styling and use plain text output")
|
|
332
|
+
args = parser.parse_args(argv)
|
|
333
|
+
if not args.paths:
|
|
334
|
+
parser.print_help()
|
|
335
|
+
return 2
|
|
336
|
+
status = 0
|
|
337
|
+
for raw_path in args.paths:
|
|
338
|
+
collected = _collect_echo_files(raw_path, args.plain)
|
|
339
|
+
if isinstance(collected, int):
|
|
340
|
+
return collected
|
|
341
|
+
for file_path in collected:
|
|
342
|
+
code = check_file(str(file_path), plain=args.plain)
|
|
343
|
+
if code != 0:
|
|
344
|
+
status = code
|
|
345
|
+
return status
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def format_file(source_path: str, *, check: bool = False, plain: bool = False) -> int:
|
|
349
|
+
file_path = Path(source_path).expanduser().resolve()
|
|
350
|
+
if not file_path.exists() or not file_path.is_file():
|
|
351
|
+
_print_plain_error("Error", f"source file not found: {file_path}", plain)
|
|
352
|
+
return 1
|
|
353
|
+
source = file_path.read_text(encoding="utf-8")
|
|
354
|
+
try:
|
|
355
|
+
formatted = format_source(source, filename=str(file_path))
|
|
356
|
+
except EchoError as exc:
|
|
357
|
+
_print_error(exc, _error_source(exc, source), plain)
|
|
358
|
+
return 1
|
|
359
|
+
if formatted == source:
|
|
360
|
+
return 0
|
|
361
|
+
if check:
|
|
362
|
+
print(file_path)
|
|
363
|
+
return 1
|
|
364
|
+
file_path.write_text(formatted, encoding="utf-8")
|
|
365
|
+
return 0
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _collect_echo_files(source_path: str, plain: bool) -> list[Path] | int:
|
|
369
|
+
path = Path(source_path).expanduser().resolve()
|
|
370
|
+
if path.is_file():
|
|
371
|
+
return [path]
|
|
372
|
+
if path.is_dir():
|
|
373
|
+
return sorted(item for item in path.rglob("*.echo") if item.is_file())
|
|
374
|
+
_print_plain_error("Error", f"source file not found: {path}", plain)
|
|
375
|
+
return 1
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _main_fmt(argv: list[str]) -> int:
|
|
379
|
+
parser = argparse.ArgumentParser(prog="echo fmt", description="Format Echo source files")
|
|
380
|
+
parser.add_argument("paths", nargs="*", help="Files or directories of .echo sources")
|
|
381
|
+
parser.add_argument("--check", action="store_true", help="Exit 1 if any file would change")
|
|
382
|
+
parser.add_argument("--plain", action="store_true", help="Disable Rich styling and use plain text output")
|
|
383
|
+
args = parser.parse_args(argv)
|
|
384
|
+
if not args.paths:
|
|
385
|
+
parser.print_help()
|
|
386
|
+
return 2
|
|
387
|
+
status = 0
|
|
388
|
+
for raw_path in args.paths:
|
|
389
|
+
collected = _collect_echo_files(raw_path, args.plain)
|
|
390
|
+
if isinstance(collected, int):
|
|
391
|
+
return collected
|
|
392
|
+
for file_path in collected:
|
|
393
|
+
code = format_file(str(file_path), check=args.check, plain=args.plain)
|
|
394
|
+
if code != 0:
|
|
395
|
+
status = code
|
|
396
|
+
return status
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def lint_file(source_path: str, *, plain: bool = False) -> tuple[int, list[LintFinding]]:
|
|
400
|
+
file_path = Path(source_path).expanduser().resolve()
|
|
401
|
+
if not file_path.exists() or not file_path.is_file():
|
|
402
|
+
_print_plain_error("Error", f"source file not found: {file_path}", plain)
|
|
403
|
+
return 1, []
|
|
404
|
+
source = file_path.read_text(encoding="utf-8")
|
|
405
|
+
try:
|
|
406
|
+
findings = lint_source(source, filename=str(file_path))
|
|
407
|
+
except EchoError as exc:
|
|
408
|
+
_print_error(exc, _error_source(exc, source), plain)
|
|
409
|
+
return 1, []
|
|
410
|
+
return (1 if findings else 0), findings
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _main_lint(argv: list[str]) -> int:
|
|
414
|
+
parser = argparse.ArgumentParser(prog="echo lint", description="Lint Echo source files for style and convention")
|
|
415
|
+
parser.add_argument("paths", nargs="*", help="Files or directories of .echo sources")
|
|
416
|
+
parser.add_argument("--plain", action="store_true", help="Disable Rich styling and use plain text output")
|
|
417
|
+
args = parser.parse_args(argv)
|
|
418
|
+
if not args.paths:
|
|
419
|
+
parser.print_help()
|
|
420
|
+
return 2
|
|
421
|
+
status = 0
|
|
422
|
+
for raw_path in args.paths:
|
|
423
|
+
collected = _collect_echo_files(raw_path, args.plain)
|
|
424
|
+
if isinstance(collected, int):
|
|
425
|
+
return collected
|
|
426
|
+
for file_path in collected:
|
|
427
|
+
code, findings = lint_file(str(file_path), plain=args.plain)
|
|
428
|
+
for finding in findings:
|
|
429
|
+
print(finding.format())
|
|
430
|
+
if code != 0:
|
|
431
|
+
status = code
|
|
432
|
+
return status
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _main_test(argv: list[str]) -> int:
|
|
436
|
+
parser = argparse.ArgumentParser(prog="echo test", description="Run Echo tests")
|
|
437
|
+
parser.add_argument("paths", nargs="*", help="Test files or directories of *_test.echo files")
|
|
438
|
+
parser.add_argument(
|
|
439
|
+
"-run",
|
|
440
|
+
"--run",
|
|
441
|
+
metavar="PATTERN",
|
|
442
|
+
dest="run",
|
|
443
|
+
help="Run only testXxx units whose names match glob PATTERN (e.g. testAdd or *Add*)",
|
|
444
|
+
)
|
|
445
|
+
parser.add_argument(
|
|
446
|
+
"--json",
|
|
447
|
+
action="store_true",
|
|
448
|
+
help="Write a machine-readable JSON report to stdout instead of the human summary",
|
|
449
|
+
)
|
|
450
|
+
parser.add_argument("--plain", action="store_true", help="Disable Rich styling and use plain text output")
|
|
451
|
+
args = parser.parse_args(argv)
|
|
452
|
+
if not args.paths:
|
|
453
|
+
parser.print_help()
|
|
454
|
+
return 2
|
|
455
|
+
from echo.cli.test_runner import print_json_report, print_summary, run_tests
|
|
456
|
+
|
|
457
|
+
code, results = run_tests(args.paths, plain=args.plain, run=args.run, json_output=args.json)
|
|
458
|
+
if args.json:
|
|
459
|
+
if not results and code != 0:
|
|
460
|
+
return code
|
|
461
|
+
print_json_report(results)
|
|
462
|
+
return code
|
|
463
|
+
if not results and code != 0:
|
|
464
|
+
return code
|
|
465
|
+
print_summary(results)
|
|
466
|
+
return code
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
if __name__ == "__main__":
|
|
470
|
+
raise SystemExit(main())
|