treeing 1.0.1__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.
- treeing/__init__.py +7 -0
- treeing/_release_defaults.py +3 -0
- treeing/assets/icon.icns +0 -0
- treeing/assets/icon.ico +0 -0
- treeing/assets/icon.png +0 -0
- treeing/cli/__init__.py +5 -0
- treeing/cli/confirm.py +197 -0
- treeing/cli/help_text.py +311 -0
- treeing/cli/io.py +72 -0
- treeing/cli/main.py +427 -0
- treeing/cli/report.py +104 -0
- treeing/cli_entry.py +16 -0
- treeing/config.py +205 -0
- treeing/core/__init__.py +15 -0
- treeing/core/constants.py +20 -0
- treeing/core/generator.py +567 -0
- treeing/core/parser.py +407 -0
- treeing/core/preview.py +98 -0
- treeing/gui/__init__.py +5 -0
- treeing/gui/app.py +886 -0
- treeing/gui/dnd.py +124 -0
- treeing/gui/icon.py +69 -0
- treeing/gui/preview.py +9 -0
- treeing/gui/settings.py +80 -0
- treeing/gui/tooltip.py +234 -0
- treeing/gui_entry.py +39 -0
- treeing/main.py +21 -0
- treeing/path_checks.py +87 -0
- treeing/strings.bootstrap.json +7 -0
- treeing/strings.json +222 -0
- treeing-1.0.1.dist-info/METADATA +112 -0
- treeing-1.0.1.dist-info/RECORD +36 -0
- treeing-1.0.1.dist-info/WHEEL +5 -0
- treeing-1.0.1.dist-info/entry_points.txt +3 -0
- treeing-1.0.1.dist-info/licenses/LICENSE +21 -0
- treeing-1.0.1.dist-info/top_level.txt +1 -0
treeing/cli/main.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""
|
|
2
|
+
treeing/cli/main.py
|
|
3
|
+
|
|
4
|
+
Defines the CLI main entry point and command dispatch.
|
|
5
|
+
Handles argument parsing, path validation, the parse/generate flow, warning
|
|
6
|
+
output, JSON results and exit codes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from ..config import get_string
|
|
14
|
+
from ..core.generator import create_from_tree, iter_nodes
|
|
15
|
+
from ..core.parser import build_tree
|
|
16
|
+
from ..core.preview import render_text_tree
|
|
17
|
+
from ..path_checks import verify_output_is_directory, verify_output_writable
|
|
18
|
+
from .confirm import gate_before_write
|
|
19
|
+
from .help_text import build_parser, dispatch_help_argv
|
|
20
|
+
from .io import cli_err, cli_out, cli_warn, configure_stdio
|
|
21
|
+
from .report import (
|
|
22
|
+
DEFAULT_WARN_LIMIT,
|
|
23
|
+
EXIT_FAILURE,
|
|
24
|
+
build_json_result,
|
|
25
|
+
emit_json,
|
|
26
|
+
resolve_exit_code,
|
|
27
|
+
write_warnings_file,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_FORMAT_TEXT = 'text'
|
|
31
|
+
_FORMAT_TREE = 'tree'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ensure_utf8_console() -> None:
|
|
35
|
+
"""
|
|
36
|
+
Switch the Windows console (default legacy codepage) to UTF-8 to avoid garbled output.
|
|
37
|
+
|
|
38
|
+
Best effect under cmd and the PyInstaller exe; under PowerShell prefer the
|
|
39
|
+
scripts/treeing-cli.ps1 launcher, since the host encoding is not controlled
|
|
40
|
+
by this process.
|
|
41
|
+
"""
|
|
42
|
+
if sys.platform != 'win32':
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
# Force the Python standard-stream environment variable.
|
|
46
|
+
if 'PYTHONIOENCODING' not in os.environ:
|
|
47
|
+
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
|
48
|
+
|
|
49
|
+
# Set the current process console code page to UTF-8.
|
|
50
|
+
try:
|
|
51
|
+
import ctypes
|
|
52
|
+
ctypes.windll.kernel32.SetConsoleCP(65001)
|
|
53
|
+
ctypes.windll.kernel32.SetConsoleOutputCP(65001)
|
|
54
|
+
except Exception: # nosec B110 - safe fallback when the console code page switch fails
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _print_warning_sections(
|
|
59
|
+
sections: list[tuple[str, list[str]]],
|
|
60
|
+
*,
|
|
61
|
+
limit: int | None,
|
|
62
|
+
quiet: bool,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Print warning lists by stage (parse / generate).
|
|
66
|
+
|
|
67
|
+
With limit=None nothing is truncated; otherwise truncation is followed by
|
|
68
|
+
an "N more" line.
|
|
69
|
+
"""
|
|
70
|
+
if quiet:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
non_empty = [(title_key, ws) for title_key, ws in sections if ws]
|
|
74
|
+
if not non_empty:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
tagged: list[tuple[str, str]] = []
|
|
78
|
+
for title_key, ws in non_empty:
|
|
79
|
+
for w in ws:
|
|
80
|
+
tagged.append((title_key, w))
|
|
81
|
+
|
|
82
|
+
total = len(tagged)
|
|
83
|
+
effective_limit = total if limit is None else limit
|
|
84
|
+
shown = tagged[:effective_limit]
|
|
85
|
+
if limit is not None and total > len(shown):
|
|
86
|
+
cli_warn(get_string("cli_warning_header_truncated", count=total, shown=len(shown)))
|
|
87
|
+
else:
|
|
88
|
+
cli_warn(get_string("cli_warning_header", count=total))
|
|
89
|
+
|
|
90
|
+
current_section: str | None = None
|
|
91
|
+
section_totals = {tk: len(ws) for tk, ws in non_empty}
|
|
92
|
+
for title_key, warning in shown:
|
|
93
|
+
if title_key != current_section:
|
|
94
|
+
cli_warn(get_string(
|
|
95
|
+
"cli_warning_section",
|
|
96
|
+
title=get_string(title_key),
|
|
97
|
+
count=section_totals[title_key],
|
|
98
|
+
))
|
|
99
|
+
current_section = title_key
|
|
100
|
+
cli_warn(get_string("cli_warning_item", warning=warning))
|
|
101
|
+
|
|
102
|
+
if limit is not None and total > len(shown):
|
|
103
|
+
cli_warn(get_string("cli_warning_more", extra=total - len(shown)))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _warn_display_limit(args) -> int | None:
|
|
107
|
+
"""
|
|
108
|
+
Decide the terminal warning display cap from the arguments.
|
|
109
|
+
|
|
110
|
+
--no-warn-limit means no cap; --warn-limit=0 also means no cap; otherwise
|
|
111
|
+
the default DEFAULT_WARN_LIMIT is used.
|
|
112
|
+
"""
|
|
113
|
+
if args.no_warn_limit:
|
|
114
|
+
return None
|
|
115
|
+
if args.warn_limit is not None:
|
|
116
|
+
return args.warn_limit if args.warn_limit > 0 else None
|
|
117
|
+
return DEFAULT_WARN_LIMIT
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _maybe_write_warnings_file(
|
|
121
|
+
path: str | None,
|
|
122
|
+
parse_warnings: list[str],
|
|
123
|
+
gen_warnings: list[str],
|
|
124
|
+
) -> str | None:
|
|
125
|
+
"""
|
|
126
|
+
When the user passes --warnings-file, write the warnings there.
|
|
127
|
+
|
|
128
|
+
Returns None on success; returns an error message string on write failure.
|
|
129
|
+
"""
|
|
130
|
+
if not path:
|
|
131
|
+
return None
|
|
132
|
+
try:
|
|
133
|
+
write_warnings_file(Path(path), parse_warnings, gen_warnings)
|
|
134
|
+
except OSError as e:
|
|
135
|
+
return get_string('cli_err_warnings_file_write', path=path, error=e)
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _emit_tree_preview(tree: list[dict], *, allow_nested: bool) -> list[str]:
|
|
140
|
+
"""Render the parsed tree into text-tree lines for --format tree."""
|
|
141
|
+
return render_text_tree(tree, allow_nested=allow_nested)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _print_tree_preview(lines: list[str]) -> None:
|
|
145
|
+
"""Print text-tree lines to stdout, one per line."""
|
|
146
|
+
for line in lines:
|
|
147
|
+
cli_out(line)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _emit_failure(
|
|
151
|
+
*,
|
|
152
|
+
args,
|
|
153
|
+
error: str,
|
|
154
|
+
parse_warnings: list[str] | None = None,
|
|
155
|
+
gen_warnings: list[str] | None = None,
|
|
156
|
+
dry_run: bool = False,
|
|
157
|
+
node_count: int = 0,
|
|
158
|
+
tree_preview: list[str] | None = None,
|
|
159
|
+
implicit_output_warning: str | None = None,
|
|
160
|
+
) -> int:
|
|
161
|
+
"""
|
|
162
|
+
Emit a failure result uniformly.
|
|
163
|
+
|
|
164
|
+
Output format follows --json / --quiet; with --warnings-file the warnings
|
|
165
|
+
gathered so far are still written before reporting the failure.
|
|
166
|
+
"""
|
|
167
|
+
parse_warnings = parse_warnings or []
|
|
168
|
+
gen_warnings = gen_warnings or []
|
|
169
|
+
warn_file_err = _maybe_write_warnings_file(args.warnings_file, parse_warnings, gen_warnings)
|
|
170
|
+
exit_code = EXIT_FAILURE
|
|
171
|
+
|
|
172
|
+
if args.json:
|
|
173
|
+
emit_json(build_json_result(
|
|
174
|
+
ok=False,
|
|
175
|
+
dry_run=dry_run,
|
|
176
|
+
node_count=node_count,
|
|
177
|
+
output=args.output,
|
|
178
|
+
parse_warnings=parse_warnings,
|
|
179
|
+
gen_warnings=gen_warnings,
|
|
180
|
+
error=error,
|
|
181
|
+
exit_code=exit_code,
|
|
182
|
+
tree_preview=tree_preview,
|
|
183
|
+
implicit_output_warning=implicit_output_warning,
|
|
184
|
+
))
|
|
185
|
+
else:
|
|
186
|
+
if not args.quiet:
|
|
187
|
+
_print_warning_sections([
|
|
188
|
+
('cli_warning_section_parse', parse_warnings),
|
|
189
|
+
('cli_warning_section_generate', gen_warnings),
|
|
190
|
+
], limit=_warn_display_limit(args), quiet=False)
|
|
191
|
+
cli_err(error)
|
|
192
|
+
if warn_file_err and not args.quiet:
|
|
193
|
+
cli_warn(warn_file_err)
|
|
194
|
+
|
|
195
|
+
return exit_code
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _emit_success(
|
|
199
|
+
*,
|
|
200
|
+
args,
|
|
201
|
+
tree: list[dict],
|
|
202
|
+
parse_warnings: list[str],
|
|
203
|
+
gen_warnings: list[str],
|
|
204
|
+
node_count: int,
|
|
205
|
+
confirm_mode: str | None = None,
|
|
206
|
+
implicit_output_warning: str | None = None,
|
|
207
|
+
) -> int:
|
|
208
|
+
"""
|
|
209
|
+
Emit a success result uniformly.
|
|
210
|
+
|
|
211
|
+
Handles the warnings file, tree preview, success summary, JSON output and
|
|
212
|
+
the exit code 2 from --warn-exit-code.
|
|
213
|
+
"""
|
|
214
|
+
warn_file_err = _maybe_write_warnings_file(args.warnings_file, parse_warnings, gen_warnings)
|
|
215
|
+
has_warnings = bool(parse_warnings or gen_warnings)
|
|
216
|
+
exit_code = resolve_exit_code(
|
|
217
|
+
ok=True,
|
|
218
|
+
has_warnings=has_warnings,
|
|
219
|
+
warn_exit_code=args.warn_exit_code,
|
|
220
|
+
)
|
|
221
|
+
tree_preview = (
|
|
222
|
+
_emit_tree_preview(tree, allow_nested=args.allow_nested_names)
|
|
223
|
+
if args.format == _FORMAT_TREE
|
|
224
|
+
else None
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if args.json:
|
|
228
|
+
emit_json(build_json_result(
|
|
229
|
+
ok=True,
|
|
230
|
+
dry_run=args.dry_run,
|
|
231
|
+
node_count=node_count,
|
|
232
|
+
output=args.output,
|
|
233
|
+
parse_warnings=parse_warnings,
|
|
234
|
+
gen_warnings=gen_warnings,
|
|
235
|
+
exit_code=exit_code,
|
|
236
|
+
tree_preview=tree_preview,
|
|
237
|
+
confirm_mode=confirm_mode,
|
|
238
|
+
implicit_output_warning=implicit_output_warning,
|
|
239
|
+
))
|
|
240
|
+
else:
|
|
241
|
+
_print_warning_sections([
|
|
242
|
+
('cli_warning_section_parse', parse_warnings),
|
|
243
|
+
('cli_warning_section_generate', gen_warnings),
|
|
244
|
+
], limit=_warn_display_limit(args), quiet=args.quiet)
|
|
245
|
+
if tree_preview is not None:
|
|
246
|
+
_print_tree_preview(tree_preview)
|
|
247
|
+
if not args.quiet and args.format == _FORMAT_TEXT:
|
|
248
|
+
if args.dry_run:
|
|
249
|
+
cli_out(get_string("cli_dry_run_msg", count=node_count))
|
|
250
|
+
else:
|
|
251
|
+
cli_out(get_string("cli_generate_success", count=node_count, path=args.output))
|
|
252
|
+
if warn_file_err and not args.quiet:
|
|
253
|
+
cli_warn(warn_file_err)
|
|
254
|
+
|
|
255
|
+
return exit_code
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _resolve_output(args) -> str:
|
|
259
|
+
"""
|
|
260
|
+
Resolve the final output directory.
|
|
261
|
+
|
|
262
|
+
Priority: -o > the directory remembered via --use-settings > the current
|
|
263
|
+
working directory. MING folds the GUI's recent directory in here so the
|
|
264
|
+
CLI and GUI share one set of habits.
|
|
265
|
+
"""
|
|
266
|
+
if args.output is not None:
|
|
267
|
+
return args.output
|
|
268
|
+
if args.use_settings:
|
|
269
|
+
from ..gui.settings import get_last_generate_dir
|
|
270
|
+
return get_last_generate_dir() or '.'
|
|
271
|
+
return '.'
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _implicit_output_warning_message(*, output_explicit: bool, args) -> str | None:
|
|
275
|
+
"""
|
|
276
|
+
Return the warning text when no explicit -o is given and the current working directory will be written to.
|
|
277
|
+
|
|
278
|
+
Covers the --use-settings fallback to the current directory; dry-run does
|
|
279
|
+
not warn.
|
|
280
|
+
"""
|
|
281
|
+
if args.dry_run or output_explicit:
|
|
282
|
+
return None
|
|
283
|
+
try:
|
|
284
|
+
if Path(args.output).resolve() != Path.cwd().resolve():
|
|
285
|
+
return None
|
|
286
|
+
except OSError:
|
|
287
|
+
return None
|
|
288
|
+
return get_string('cli_warn_implicit_output', path=args.output)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def cli_main() -> int:
|
|
292
|
+
"""
|
|
293
|
+
CLI main entry point.
|
|
294
|
+
|
|
295
|
+
Flow: handle help / about -> parse arguments -> read input -> parse tree
|
|
296
|
+
-> validate path -> confirm -> generate filesystem -> emit result / JSON.
|
|
297
|
+
|
|
298
|
+
MING routes every error path through _emit_failure so the output structure
|
|
299
|
+
stays consistent across JSON and non-JSON modes.
|
|
300
|
+
"""
|
|
301
|
+
_ensure_utf8_console()
|
|
302
|
+
configure_stdio()
|
|
303
|
+
|
|
304
|
+
help_code = dispatch_help_argv(sys.argv)
|
|
305
|
+
if help_code is not None:
|
|
306
|
+
return help_code
|
|
307
|
+
|
|
308
|
+
parser = build_parser()
|
|
309
|
+
args = parser.parse_args()
|
|
310
|
+
|
|
311
|
+
if args.strict:
|
|
312
|
+
args.no_fix = True
|
|
313
|
+
args.fail_on_conflict = True
|
|
314
|
+
|
|
315
|
+
output_explicit = args.output is not None
|
|
316
|
+
args.output = _resolve_output(args)
|
|
317
|
+
fail_on_conflict = args.fail_on_conflict or args.fail_on_duplicate
|
|
318
|
+
|
|
319
|
+
output_dir_err = verify_output_is_directory(args.output)
|
|
320
|
+
if output_dir_err:
|
|
321
|
+
return _emit_failure(args=args, error=output_dir_err)
|
|
322
|
+
|
|
323
|
+
if args.paste and args.input:
|
|
324
|
+
parser.error(get_string("cli_error_input_conflict"))
|
|
325
|
+
|
|
326
|
+
if args.confirm and args.json:
|
|
327
|
+
parser.error(get_string("cli_error_confirm_json"))
|
|
328
|
+
|
|
329
|
+
if args.paste:
|
|
330
|
+
if not args.quiet and not args.json:
|
|
331
|
+
cli_err(get_string("cli_prompt_paste"))
|
|
332
|
+
try:
|
|
333
|
+
text = sys.stdin.buffer.read().decode(args.encoding)
|
|
334
|
+
except UnicodeDecodeError as e:
|
|
335
|
+
msg = get_string("cli_reading_stdin_error", error=e)
|
|
336
|
+
return _emit_failure(args=args, error=msg)
|
|
337
|
+
elif args.input:
|
|
338
|
+
try:
|
|
339
|
+
with open(args.input, encoding=args.encoding) as f:
|
|
340
|
+
text = f.read()
|
|
341
|
+
except Exception as e:
|
|
342
|
+
msg = get_string("cli_reading_error", error=e)
|
|
343
|
+
return _emit_failure(args=args, error=msg)
|
|
344
|
+
else:
|
|
345
|
+
parser.print_help()
|
|
346
|
+
return 0
|
|
347
|
+
|
|
348
|
+
lines = text.splitlines()
|
|
349
|
+
tree, parse_warnings = build_tree(
|
|
350
|
+
lines,
|
|
351
|
+
auto_fix=not args.no_fix,
|
|
352
|
+
indent_unit=args.indent_unit,
|
|
353
|
+
strict_dirs=args.strict_dirs,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
if not tree:
|
|
357
|
+
return _emit_failure(
|
|
358
|
+
args=args,
|
|
359
|
+
error=get_string("cli_empty_tree"),
|
|
360
|
+
parse_warnings=parse_warnings,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
implicit_output_warning = _implicit_output_warning_message(
|
|
364
|
+
output_explicit=output_explicit, args=args,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if args.check_writable:
|
|
368
|
+
writable_err = verify_output_writable(args.output)
|
|
369
|
+
if writable_err:
|
|
370
|
+
return _emit_failure(
|
|
371
|
+
args=args,
|
|
372
|
+
error=writable_err,
|
|
373
|
+
parse_warnings=parse_warnings,
|
|
374
|
+
implicit_output_warning=implicit_output_warning,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
node_count = sum(1 for _ in iter_nodes(tree))
|
|
378
|
+
warn_count = len(parse_warnings)
|
|
379
|
+
|
|
380
|
+
if implicit_output_warning and not (args.json and args.quiet):
|
|
381
|
+
cli_warn(implicit_output_warning)
|
|
382
|
+
|
|
383
|
+
early_exit, confirm_mode = gate_before_write(
|
|
384
|
+
args,
|
|
385
|
+
path=args.output,
|
|
386
|
+
node_count=node_count,
|
|
387
|
+
fail_on_conflict=fail_on_conflict,
|
|
388
|
+
warn_count=warn_count,
|
|
389
|
+
)
|
|
390
|
+
if early_exit is not None:
|
|
391
|
+
return early_exit
|
|
392
|
+
|
|
393
|
+
gen_warnings: list[str] = []
|
|
394
|
+
try:
|
|
395
|
+
create_from_tree(
|
|
396
|
+
tree,
|
|
397
|
+
Path(args.output),
|
|
398
|
+
dry_run=args.dry_run,
|
|
399
|
+
warnings=gen_warnings,
|
|
400
|
+
allow_nested_names=args.allow_nested_names,
|
|
401
|
+
fail_on_conflict=fail_on_conflict,
|
|
402
|
+
rollback_on_error=args.rollback_on_error,
|
|
403
|
+
)
|
|
404
|
+
return _emit_success(
|
|
405
|
+
args=args,
|
|
406
|
+
tree=tree,
|
|
407
|
+
parse_warnings=parse_warnings,
|
|
408
|
+
gen_warnings=gen_warnings,
|
|
409
|
+
node_count=node_count,
|
|
410
|
+
confirm_mode=confirm_mode,
|
|
411
|
+
implicit_output_warning=implicit_output_warning,
|
|
412
|
+
)
|
|
413
|
+
except Exception as e:
|
|
414
|
+
return _emit_failure(
|
|
415
|
+
args=args,
|
|
416
|
+
error=get_string("cli_generate_failed", error=e),
|
|
417
|
+
parse_warnings=parse_warnings,
|
|
418
|
+
gen_warnings=gen_warnings,
|
|
419
|
+
dry_run=args.dry_run,
|
|
420
|
+
node_count=sum(1 for _ in iter_nodes(tree)),
|
|
421
|
+
tree_preview=(
|
|
422
|
+
_emit_tree_preview(tree, allow_nested=args.allow_nested_names)
|
|
423
|
+
if args.format == _FORMAT_TREE
|
|
424
|
+
else None
|
|
425
|
+
),
|
|
426
|
+
implicit_output_warning=implicit_output_warning,
|
|
427
|
+
)
|
treeing/cli/report.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
treeing/cli/report.py
|
|
3
|
+
|
|
4
|
+
Defines the CLI result-reporting logic: JSON output, warnings-file writing,
|
|
5
|
+
and exit-code resolution. Provides helpers such as `build_json_result`,
|
|
6
|
+
`emit_json` and `resolve_exit_code`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ..config import get_string
|
|
13
|
+
from .io import cli_out
|
|
14
|
+
|
|
15
|
+
DEFAULT_WARN_LIMIT = 10
|
|
16
|
+
EXIT_SUCCESS = 0
|
|
17
|
+
EXIT_FAILURE = 1
|
|
18
|
+
EXIT_WARNINGS = 2
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolve_exit_code(*, ok: bool, has_warnings: bool, warn_exit_code: bool) -> int:
|
|
22
|
+
"""
|
|
23
|
+
Decide the exit code from the run result and warnings.
|
|
24
|
+
|
|
25
|
+
With warn_exit_code set, a successful run with warnings returns 2
|
|
26
|
+
(EXIT_WARNINGS); a failure always returns 1.
|
|
27
|
+
"""
|
|
28
|
+
if not ok:
|
|
29
|
+
return EXIT_FAILURE
|
|
30
|
+
if has_warnings and warn_exit_code:
|
|
31
|
+
return EXIT_WARNINGS
|
|
32
|
+
return EXIT_SUCCESS
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def write_warnings_file(
|
|
36
|
+
path: Path,
|
|
37
|
+
parse_warnings: list[str],
|
|
38
|
+
gen_warnings: list[str],
|
|
39
|
+
) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Write parse and generation warnings to the given file, prefixed by stage.
|
|
42
|
+
|
|
43
|
+
Edge cases: write failures are caught by the caller; an empty list still
|
|
44
|
+
writes a trailing newline.
|
|
45
|
+
"""
|
|
46
|
+
lines: list[str] = []
|
|
47
|
+
parse_title = get_string('cli_warning_section_parse')
|
|
48
|
+
gen_title = get_string('cli_warning_section_generate')
|
|
49
|
+
for w in parse_warnings:
|
|
50
|
+
lines.append(f'[{parse_title}] {w}')
|
|
51
|
+
for w in gen_warnings:
|
|
52
|
+
lines.append(f'[{gen_title}] {w}')
|
|
53
|
+
path.write_text('\n'.join(lines) + ('\n' if lines else ''), encoding='utf-8')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_json_result(
|
|
57
|
+
*,
|
|
58
|
+
ok: bool,
|
|
59
|
+
dry_run: bool = False,
|
|
60
|
+
node_count: int = 0,
|
|
61
|
+
output: str = '',
|
|
62
|
+
parse_warnings: list[str],
|
|
63
|
+
gen_warnings: list[str],
|
|
64
|
+
error: str | None = None,
|
|
65
|
+
exit_code: int = EXIT_SUCCESS,
|
|
66
|
+
tree_preview: list[str] | None = None,
|
|
67
|
+
confirm_mode: str | None = None,
|
|
68
|
+
implicit_output_warning: str | None = None,
|
|
69
|
+
) -> dict:
|
|
70
|
+
"""
|
|
71
|
+
Assemble the JSON result dictionary for --json output.
|
|
72
|
+
|
|
73
|
+
Optional fields are only included when not None.
|
|
74
|
+
"""
|
|
75
|
+
result: dict = {
|
|
76
|
+
'ok': ok,
|
|
77
|
+
'dry_run': dry_run,
|
|
78
|
+
'node_count': node_count,
|
|
79
|
+
'output': output,
|
|
80
|
+
'parse_warnings': parse_warnings,
|
|
81
|
+
'generate_warnings': gen_warnings,
|
|
82
|
+
'exit_code': exit_code,
|
|
83
|
+
}
|
|
84
|
+
if error is not None:
|
|
85
|
+
result['error'] = error
|
|
86
|
+
if tree_preview is not None:
|
|
87
|
+
result['tree_preview'] = tree_preview
|
|
88
|
+
if confirm_mode is not None:
|
|
89
|
+
result['confirmed'] = True
|
|
90
|
+
result['confirm_mode'] = confirm_mode
|
|
91
|
+
if implicit_output_warning is not None:
|
|
92
|
+
result['implicit_output_warning'] = True
|
|
93
|
+
result['implicit_output_warning_message'] = implicit_output_warning
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def emit_json(data: dict) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Print the result dictionary as a single-line JSON object to stdout.
|
|
100
|
+
|
|
101
|
+
Uses ensure_ascii=False so non-ASCII text stays readable in the JSON
|
|
102
|
+
rather than being escaped to \\uXXXX.
|
|
103
|
+
"""
|
|
104
|
+
cli_out(json.dumps(data, ensure_ascii=False))
|
treeing/cli_entry.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""treeing/cli_entry.py
|
|
2
|
+
|
|
3
|
+
PyInstaller CLI entry point.
|
|
4
|
+
Does not import tkinter, which suits a minimal build; calls cli_main directly and handles ConfigError.
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from treeing.cli.main import cli_main
|
|
9
|
+
from treeing.config import ConfigError
|
|
10
|
+
|
|
11
|
+
if __name__ == '__main__':
|
|
12
|
+
try:
|
|
13
|
+
sys.exit(cli_main())
|
|
14
|
+
except ConfigError as e:
|
|
15
|
+
print(str(e), file=sys.stderr)
|
|
16
|
+
sys.exit(1)
|