main
py 863 lines 27.6 KB
Raw
1 from __future__ import annotations
2
3 import argparse
4 import os
5 from collections.abc import Iterable
6 from contextlib import contextmanager
7 from dataclasses import dataclass, field
8 from pathlib import Path
9 import sys
10 import time
11 from typing import Any, Callable, Dict, List, Optional
12
13 try:
14 import pytest # type: ignore
15 except ImportError: # pragma: no cover
16 pytest = None
17
18 if pytest is not None:
19 pytestmark = pytest.mark.skip(reason="Visualization utility; excluded from automated test runs.")
20
21
22 REPO_ROOT = Path(__file__).resolve().parents[1]
23 if str(REPO_ROOT) not in sys.path:
24 sys.path.insert(0, str(REPO_ROOT))
25
26 from helpers.file_tree import (
27 OUTPUT_MODE_FLAT,
28 OUTPUT_MODE_NESTED,
29 OUTPUT_MODE_STRING,
30 SORT_ASC,
31 SORT_BY_CREATED,
32 SORT_BY_MODIFIED,
33 SORT_BY_NAME,
34 SORT_DESC,
35 file_tree,
36 )
37 from helpers.files import create_dir, delete_dir, get_abs_path, write_file
38
39
40 BASE_TEMP_ROOT = "tmp/tests/file_tree/visualize"
41
42
43 @dataclass(slots=True)
44 class Config:
45 label: str
46 params: Dict[str, Any]
47
48
49 SetupHook = Optional[Callable[[str], None]]
50
51
52 @dataclass(slots=True)
53 class Scenario:
54 name: str
55 description: str
56 structure: Dict[str, Any]
57 configs: List[Config] = field(default_factory=list)
58 ignore_content: Optional[str] = None
59 setup: SetupHook = None
60
61
62 def materialize_structure(base_rel: str, structure: Dict[str, Any]) -> None:
63 for entry, value in structure.items():
64 rel = os.path.join(base_rel, entry)
65 if isinstance(value, dict):
66 create_dir(rel)
67 materialize_structure(rel, value)
68 else:
69 write_file(rel, "" if value is None else str(value))
70
71
72 def ensure_ignore_file(base_rel: str, content: str) -> None:
73 write_file(os.path.join(base_rel, ".treeignore"), content.strip() + "\n")
74
75
76 def print_header(title: str, char: str = "=") -> None:
77 print(char * 80)
78 print(title)
79 print(char * 80)
80
81
82 def print_flat(items: List[Dict[str, Any]]) -> None:
83 print("level type name text")
84 print("-" * 80)
85 for item in items:
86 level = item["level"]
87 item_type = item["type"]
88 name = item["name"]
89 text = item["text"]
90 print(f"{level:<5} {item_type:<7} {name:<20} {text}")
91
92
93 def print_nested(items: List[Dict[str, Any]], root_label: str) -> None:
94 print(root_label)
95
96 def recurse(nodes: List[Dict[str, Any]], prefix: str) -> None:
97 total = len(nodes)
98 for index, node in enumerate(nodes):
99 is_last = index == total - 1
100 connector = "└── " if is_last else "├── "
101 label = node["name"] + ("/" if node["type"] == "folder" else "")
102 print(f"{prefix}{connector}{label} [{node['type']}]")
103 children = node.get("items") or []
104 if children:
105 child_prefix = prefix + (" " if is_last else "")
106 recurse(children, child_prefix)
107
108 recurse(items, "")
109
110
111 @contextmanager
112 def scenario_directory(name: str) -> Iterable[str]:
113 rel_path = os.path.join(BASE_TEMP_ROOT, name)
114 delete_dir(rel_path)
115 create_dir(rel_path)
116 try:
117 yield rel_path
118 finally:
119 delete_dir(rel_path)
120
121
122 def _set_entry_times(relative_path: str, timestamp: float) -> None:
123 abs_path = get_abs_path(relative_path)
124 os.utime(abs_path, (timestamp, timestamp))
125 time.sleep(0.01)
126
127
128 def _apply_timestamps(base_rel: str, paths: List[str], base_ts: Optional[float] = None) -> None:
129 if base_ts is None:
130 base_ts = time.time()
131 for offset, rel in enumerate(paths, start=1):
132 _set_entry_times(os.path.join(base_rel, rel), base_ts + offset)
133
134
135 def list_scenarios(scenarios: List[Scenario]) -> None:
136 print("Available scenarios:")
137 for scenario in scenarios:
138 print(f" - {scenario.name}: {scenario.description}")
139
140
141 def run_scenarios(selected: List[Scenario]) -> None:
142 create_dir(BASE_TEMP_ROOT)
143 for scenario in selected:
144 print_header(f"Scenario: {scenario.name}{scenario.description}")
145 with scenario_directory(scenario.name) as base_rel:
146 materialize_structure(base_rel, scenario.structure)
147
148 if scenario.ignore_content:
149 ensure_ignore_file(base_rel, scenario.ignore_content)
150
151 if scenario.setup:
152 scenario.setup(base_rel)
153
154 for config in scenario.configs:
155 print_header(f"Configuration: {config.label}", "-")
156 params = {
157 "relative_path": base_rel,
158 "max_depth": 0,
159 "max_lines": 0,
160 "folders_first": True,
161 "max_folders": None,
162 "max_files": None,
163 "sort": (SORT_BY_MODIFIED, SORT_DESC),
164 **config.params,
165 }
166 output_mode = params.setdefault("output_mode", OUTPUT_MODE_STRING)
167 print("Parameters:")
168 print(f" output_mode : {output_mode}")
169 print(f" folders_first : {params['folders_first']}")
170 sort_key, sort_dir = params["sort"]
171 print(f" sort : key={sort_key}, direction={sort_dir}")
172 print(f" max_depth : {params['max_depth']}")
173 print(f" max_lines : {params['max_lines']}")
174 print(f" max_folders : {params['max_folders']}")
175 print(f" max_files : {params['max_files']}")
176 print(f" ignore : {params.get('ignore')}")
177 print()
178 result = file_tree(**params)
179
180 if output_mode == OUTPUT_MODE_STRING:
181 print(result)
182 elif output_mode == OUTPUT_MODE_FLAT:
183 print_flat(result) # type: ignore[arg-type]
184 elif output_mode == OUTPUT_MODE_NESTED:
185 print_nested(result, f"{scenario.name}/")
186 else:
187 print(f"(Unhandled output mode {output_mode!r})")
188
189 print()
190
191
192 def build_scenarios() -> List[Scenario]:
193 scenarios: List[Scenario] = []
194
195 scenarios.append(
196 Scenario(
197 name="basic_breadth_first",
198 description="Default breadth-first traversal with mixed folders/files",
199 structure={
200 "alpha": {"alpha_file.txt": "alpha", "nested": {"inner.txt": "inner"}},
201 "beta": {"beta_file.txt": "beta"},
202 "zeta": {},
203 "a.txt": "A",
204 "b.txt": "B",
205 },
206 configs=[
207 Config(
208 "string • folders-first (name asc)",
209 {
210 "output_mode": OUTPUT_MODE_STRING,
211 "folders_first": True,
212 "sort": (SORT_BY_NAME, SORT_ASC),
213 },
214 ),
215 Config(
216 "string • folders-first disabled",
217 {
218 "output_mode": OUTPUT_MODE_STRING,
219 "folders_first": False,
220 "sort": (SORT_BY_NAME, SORT_ASC),
221 },
222 ),
223 Config(
224 "flat • folders-first",
225 {
226 "output_mode": OUTPUT_MODE_FLAT,
227 "folders_first": True,
228 "sort": (SORT_BY_NAME, SORT_ASC),
229 },
230 ),
231 Config(
232 "nested • folders-first",
233 {
234 "output_mode": OUTPUT_MODE_NESTED,
235 "folders_first": True,
236 "sort": (SORT_BY_NAME, SORT_ASC),
237 },
238 ),
239 ],
240 )
241 )
242
243 def setup_sorting(base_rel: str) -> None:
244 entries = [
245 "folder_alpha",
246 "folder_beta",
247 "file_first.txt",
248 "file_second.txt",
249 "file_third.txt",
250 ]
251 for index, entry in enumerate(entries, start=1):
252 abs_path = get_abs_path(os.path.join(base_rel, entry))
253 timestamp = 200_000_0000 + index
254 os.utime(abs_path, (timestamp, timestamp))
255
256 scenarios.append(
257 Scenario(
258 name="sorting_variants",
259 description="Demonstrate sorting by name and timestamp with folders/files",
260 structure={
261 "folder_alpha": {},
262 "folder_beta": {},
263 "file_first.txt": "",
264 "file_second.txt": "",
265 "file_third.txt": "",
266 },
267 configs=[
268 Config(
269 "string • sort by name asc",
270 {
271 "output_mode": OUTPUT_MODE_STRING,
272 "folders_first": True,
273 "sort": (SORT_BY_NAME, SORT_ASC),
274 },
275 ),
276 Config(
277 "string • sort by created desc",
278 {
279 "output_mode": OUTPUT_MODE_STRING,
280 "folders_first": True,
281 "sort": (SORT_BY_CREATED, SORT_DESC),
282 },
283 ),
284 Config(
285 "flat • sort by modified asc",
286 {
287 "output_mode": OUTPUT_MODE_FLAT,
288 "folders_first": True,
289 "sort": (SORT_BY_MODIFIED, SORT_ASC),
290 },
291 ),
292 ],
293 setup=setup_sorting,
294 )
295 )
296
297 scenarios.append(
298 Scenario(
299 name="ignore_and_limits",
300 description="Ignore file semantics with max_folders/max_files summaries",
301 structure={
302 "src": {
303 "main.py": "print('hello')",
304 "utils.py": "pass",
305 "tmp.tmp": "",
306 "cache": {"cached.txt": "", "keep.txt": ""},
307 "modules": {"a.py": "", "b.py": "", "c.py": ""},
308 "pkg": {"alpha.py": "", "beta.py": "", "gamma.py": ""},
309 },
310 "logs": {"2024.log": "", "2025.log": ""},
311 "notes.md": "",
312 "guide.md": "",
313 "todo.md": "",
314 "build.tmp": "",
315 "archive": {},
316 "assets": {},
317 "sandbox": {},
318 "vendor": {},
319 },
320 ignore_content="\n".join(
321 ["*.tmp", "cache/", "!src/cache/keep.txt", "logs/", "!logs/2025.log"]
322 ),
323 configs=[
324 Config(
325 "string • folders-first with summaries",
326 {
327 "output_mode": OUTPUT_MODE_STRING,
328 "folders_first": False,
329 "sort": (SORT_BY_NAME, SORT_ASC),
330 "max_folders": 1,
331 "max_files": 2,
332 "max_lines": 12,
333 "ignore": "file:.treeignore",
334 },
335 ),
336 Config(
337 "nested • inspect truncated branches & comments",
338 {
339 "output_mode": OUTPUT_MODE_NESTED,
340 "folders_first": False,
341 "sort": (SORT_BY_NAME, SORT_ASC),
342 "max_folders": 1,
343 "max_files": 2,
344 "max_lines": 12,
345 "ignore": "file:.treeignore",
346 },
347 ),
348 ],
349 )
350 )
351
352 scenarios.append(
353 Scenario(
354 name="limits_exact_match",
355 description="Per-directory limits exactly met (no summary comments)",
356 structure={
357 "pkg": {
358 "a.py": "",
359 "b.py": "",
360 "dir1": {},
361 "dir2": {},
362 }
363 },
364 configs=[
365 Config(
366 "string • exact matches (no summaries)",
367 {
368 "output_mode": OUTPUT_MODE_STRING,
369 "folders_first": True,
370 "sort": (SORT_BY_NAME, SORT_ASC),
371 "max_folders": 2,
372 "max_files": 2,
373 },
374 ),
375 Config(
376 "flat • exact matches (no summaries)",
377 {
378 "output_mode": OUTPUT_MODE_FLAT,
379 "folders_first": True,
380 "sort": (SORT_BY_NAME, SORT_ASC),
381 "max_folders": 2,
382 "max_files": 2,
383 },
384 ),
385 ],
386 )
387 )
388
389 scenarios.append(
390 Scenario(
391 name="single_overflow",
392 description="Single overflow entries promoted instead of summary comment",
393 structure={
394 "pkg": {
395 "dir_a": {},
396 "dir_b": {},
397 "file_a.txt": "",
398 }
399 },
400 configs=[
401 Config(
402 "string • single folder overflow",
403 {
404 "output_mode": OUTPUT_MODE_STRING,
405 "folders_first": True,
406 "sort": (SORT_BY_NAME, SORT_ASC),
407 "max_folders": 1,
408 },
409 ),
410 Config(
411 "string • single file overflow",
412 {
413 "output_mode": OUTPUT_MODE_STRING,
414 "folders_first": False,
415 "sort": (SORT_BY_NAME, SORT_ASC),
416 "max_files": 1,
417 },
418 ),
419 Config(
420 "flat • folders-first",
421 {
422 "output_mode": OUTPUT_MODE_FLAT,
423 "folders_first": True,
424 "sort": (SORT_BY_NAME, SORT_ASC),
425 "max_folders": 1,
426 },
427 ),
428 ],
429 )
430 )
431
432 scenarios.append(
433 Scenario(
434 name="global_max_lines",
435 description="Global max_lines finishing current depth before truncation",
436 structure={
437 "layer1_a": {
438 "layer2_a": {
439 "layer3_a": {
440 "layer4_a": {"layer5_a.txt": ""},
441 }
442 }
443 },
444 "layer1_b": {
445 "layer2_b": {
446 "layer3_b": {
447 "layer4_b": {"layer5_b.txt": ""},
448 }
449 }
450 },
451 "root_file.txt": "",
452 },
453 configs=[
454 Config(
455 "string • max_lines=6",
456 {
457 "output_mode": OUTPUT_MODE_STRING,
458 "max_lines": 6,
459 "sort": (SORT_BY_NAME, SORT_ASC),
460 },
461 ),
462 Config(
463 "nested • max_lines=6",
464 {
465 "output_mode": OUTPUT_MODE_NESTED,
466 "max_lines": 6,
467 "folders_first": True,
468 "sort": (SORT_BY_NAME, SORT_ASC),
469 },
470 ),
471 ],
472 )
473 )
474
475 scenarios.append(
476 Scenario(
477 name="flat_files_first_limits",
478 description="Flat output with files-first ordering and per-directory summaries",
479 structure={
480 "dir1": {},
481 "dir2": {},
482 "dir3": {},
483 "dir4": {},
484 "a.txt": "",
485 "b.txt": "",
486 "c.txt": "",
487 },
488 configs=[
489 Config(
490 "flat • files-first with limits",
491 {
492 "output_mode": OUTPUT_MODE_FLAT,
493 "folders_first": False,
494 "sort": (SORT_BY_NAME, SORT_ASC),
495 "max_folders": 1,
496 "max_files": 1,
497 },
498 )
499 ],
500 )
501 )
502
503 scenarios.append(
504 Scenario(
505 name="flat_sort_created_max_lines",
506 description="Flat output sorted by created time with global max_lines",
507 structure={
508 "dirA": {"inner.txt": ""},
509 "file1.txt": "",
510 "file2.txt": "",
511 "file3.txt": "",
512 },
513 setup=lambda base_rel: _apply_timestamps(
514 base_rel,
515 [
516 "dirA",
517 os.path.join("dirA", "inner.txt"),
518 "file1.txt",
519 "file2.txt",
520 "file3.txt",
521 ],
522 base_ts=2_000_001_000,
523 ),
524 configs=[
525 Config(
526 "flat • sort by created desc, max_lines=4",
527 {
528 "output_mode": OUTPUT_MODE_FLAT,
529 "folders_first": True,
530 "sort": (SORT_BY_CREATED, SORT_DESC),
531 "max_lines": 4,
532 },
533 )
534 ],
535 )
536 )
537
538 scenarios.append(
539 Scenario(
540 name="nested_files_first_limits",
541 description="Nested output with files-first ordering and per-directory summaries",
542 structure={
543 "dir": {"a.py": "", "b.py": "", "c.py": ""},
544 "folder_a": {"inner.txt": ""},
545 "folder_b": {},
546 "folder_c": {},
547 },
548 configs=[
549 Config(
550 "nested • files-first with limits",
551 {
552 "output_mode": OUTPUT_MODE_NESTED,
553 "folders_first": False,
554 "sort": (SORT_BY_NAME, SORT_ASC),
555 "max_folders": 1,
556 "max_files": 1,
557 },
558 )
559 ],
560 )
561 )
562
563 scenarios.append(
564 Scenario(
565 name="nested_max_depth_sort",
566 description="Nested output with created-time ordering and depth pruning",
567 structure={
568 "root": {
569 "branch": {
570 "leaf_a.txt": "",
571 "leaf_b.txt": "",
572 }
573 },
574 "alpha.txt": "",
575 },
576 setup=lambda base_rel: _apply_timestamps(
577 base_rel,
578 [
579 "root",
580 os.path.join("root", "branch"),
581 os.path.join("root", "branch", "leaf_a.txt"),
582 os.path.join("root", "branch", "leaf_b.txt"),
583 "alpha.txt",
584 ],
585 base_ts=2_000_010_000,
586 ),
587 configs=[
588 Config(
589 "nested • sort by created asc, max_depth=2",
590 {
591 "output_mode": OUTPUT_MODE_NESTED,
592 "folders_first": True,
593 "sort": (SORT_BY_CREATED, SORT_ASC),
594 "max_depth": 2,
595 },
596 )
597 ],
598 )
599 )
600
601 scenarios.append(
602 Scenario(
603 name="string_additional_limits",
604 description="String output exercising files-first+max_lines and zero-limit semantics",
605 structure={
606 "dir": {"inner_a.txt": "", "inner_b.txt": ""},
607 "alpha.txt": "",
608 "beta.txt": "",
609 "gamma.txt": "",
610 },
611 setup=lambda base_rel: _apply_timestamps(
612 base_rel,
613 [
614 "dir",
615 os.path.join("dir", "inner_a.txt"),
616 os.path.join("dir", "inner_b.txt"),
617 "alpha.txt",
618 "beta.txt",
619 "gamma.txt",
620 ],
621 base_ts=2_000_020_000,
622 ),
623 configs=[
624 Config(
625 "string • files-first, sort=modified desc, max_lines=4",
626 {
627 "output_mode": OUTPUT_MODE_STRING,
628 "folders_first": False,
629 "sort": (SORT_BY_MODIFIED, SORT_DESC),
630 "max_lines": 4,
631 },
632 ),
633 Config(
634 "string • zero file limit acts unlimited",
635 {
636 "output_mode": OUTPUT_MODE_STRING,
637 "folders_first": True,
638 "sort": (SORT_BY_NAME, SORT_ASC),
639 "max_folders": 2,
640 "max_files": 0,
641 },
642 ),
643 ],
644 )
645 )
646
647 stress_structure = {
648 "level1_a": {
649 "level2_a1": {
650 "leaf_a1_1.txt": "",
651 "leaf_a1_2.txt": "",
652 "leaf_a1_3.txt": "",
653 },
654 "level2_a2": {
655 "leaf_a2_1.txt": "",
656 "leaf_a2_2.txt": "",
657 "leaf_a2_3.txt": "",
658 },
659 "level2_a3": {
660 "subfolder_a3": {
661 "deep_a3_1.txt": "",
662 "deep_a3_2.txt": "",
663 "deep_a3_3.txt": "",
664 "subsubfolder_a3": {
665 "deep_a3_4.txt": "",
666 "deep_a3_5.txt": "",
667 },
668 "subsubfolder_a3_extra": {
669 "deep_a3_extra_1.txt": "",
670 "deep_a3_extra_2.txt": "",
671 },
672 },
673 "subfolder_a3_extra": {
674 "deep_extra_1.txt": "",
675 "deep_extra_2.txt": "",
676 },
677 "subfolder_a3_more": {
678 "deep_more_1.txt": "",
679 },
680 },
681 },
682 "level1_b": {
683 "level2_b1": {
684 "leaf_b1_1.txt": "",
685 "leaf_b1_2.txt": "",
686 },
687 "level2_b2": {
688 "leaf_b2_1.txt": "",
689 "leaf_b2_2.txt": "",
690 "leaf_b2_3.txt": "",
691 "leaf_b2_4.txt": "",
692 "leaf_b2_5.txt": "",
693 },
694 "level2_b3": {
695 "subfolder_b3": {
696 "deep_b3_1.txt": "",
697 "deep_b3_2.txt": "",
698 "deep_b3_3.txt": "",
699 "deep_b3_4.txt": "",
700 },
701 "subfolder_b3_extra": {
702 "deeper_b3_extra.txt": "",
703 "deeper_b3_extra_2.txt": "",
704 },
705 },
706 },
707 "level1_c": {
708 "level2_c1": {
709 "leaf_c1_1.txt": "",
710 "leaf_c1_2.txt": "",
711 "leaf_c1_3.txt": "",
712 "leaf_c1_4.txt": "",
713 "leaf_c1_5.txt": "",
714 },
715 "level2_c2": {
716 "subfolder_c2": {
717 "deep_c2_1.txt": "",
718 "deep_c2_2.txt": "",
719 },
720 "subfolder_c2_extra": {
721 "deep_c2_extra_1.txt": "",
722 },
723 },
724 },
725 "level1_d": {
726 "level2_d1": {
727 "leaf_d1_1.txt": "",
728 "leaf_d1_2.txt": "",
729 "leaf_d1_3.txt": "",
730 },
731 "level2_d2": {
732 "subfolder_d2": {
733 "deep_d2_1.txt": "",
734 "deep_d2_2.txt": "",
735 },
736 },
737 },
738 "root_file.txt": "",
739 "root_notes.md": "",
740 "root_file_2.txt": "",
741 "root_file_3.txt": "",
742 }
743
744 scenarios.append(
745 Scenario(
746 name="mixed_limits_baseline",
747 description="Full structure without truncation for comparison",
748 structure=stress_structure,
749 configs=[
750 Config(
751 "string • no limits baseline",
752 {
753 "output_mode": OUTPUT_MODE_STRING,
754 "folders_first": True,
755 "sort": (SORT_BY_NAME, SORT_ASC),
756 },
757 ),
758 Config(
759 "flat • no limits baseline",
760 {
761 "output_mode": OUTPUT_MODE_FLAT,
762 "folders_first": True,
763 "sort": (SORT_BY_NAME, SORT_ASC),
764 },
765 ),
766 Config(
767 "nested • no limits baseline",
768 {
769 "output_mode": OUTPUT_MODE_NESTED,
770 "folders_first": True,
771 "sort": (SORT_BY_NAME, SORT_ASC),
772 },
773 ),
774 ],
775 )
776 )
777
778 scenarios.append(
779 Scenario(
780 name="mixed_limits_stress",
781 description="Same structure with local and global limits applied",
782 structure=stress_structure,
783 configs=[
784 Config(
785 "string • mixed local/global limits stress",
786 {
787 "output_mode": OUTPUT_MODE_STRING,
788 "folders_first": True,
789 "sort": (SORT_BY_NAME, SORT_ASC),
790 "max_folders": 2,
791 "max_files": 2,
792 "max_lines": 19,
793 },
794 ),
795 Config(
796 "flat • mixed limits stress",
797 {
798 "output_mode": OUTPUT_MODE_FLAT,
799 "folders_first": True,
800 "sort": (SORT_BY_NAME, SORT_ASC),
801 "max_folders": 2,
802 "max_files": 2,
803 "max_lines": 19,
804 },
805 ),
806 Config(
807 "nested • mixed limits stress",
808 {
809 "output_mode": OUTPUT_MODE_NESTED,
810 "folders_first": True,
811 "sort": (SORT_BY_NAME, SORT_ASC),
812 "max_folders": 2,
813 "max_files": 2,
814 "max_lines": 19,
815 },
816 ),
817 ],
818 )
819 )
820
821 return scenarios
822
823
824 def parse_args() -> argparse.Namespace:
825 parser = argparse.ArgumentParser(
826 description="Visualize file_tree() outputs across configurations."
827 )
828 parser.add_argument(
829 "--scenario",
830 action="append",
831 dest="scenarios",
832 help="Scenario name to run (repeat for multiple). Default: run all.",
833 )
834 parser.add_argument(
835 "--list",
836 action="store_true",
837 help="List available scenarios and exit.",
838 )
839 return parser.parse_args()
840
841
842 def main() -> None:
843 scenarios = build_scenarios()
844 args = parse_args()
845
846 if args.list:
847 list_scenarios(scenarios)
848 return
849
850 if args.scenarios:
851 name_map = {scenario.name: scenario for scenario in scenarios}
852 unknown = [name for name in args.scenarios if name not in name_map]
853 if unknown:
854 raise SystemExit(f"Unknown scenario(s): {', '.join(unknown)}")
855 selected = [name_map[name] for name in args.scenarios]
856 else:
857 selected = scenarios
858
859 run_scenarios(selected)
860
861
862 if __name__ == "__main__":
863 main()