frob 0.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.
Files changed (118) hide show
  1. frob/__init__.py +0 -0
  2. frob/__main__.py +296 -0
  3. frob/_compat.py +18 -0
  4. frob/app/__init__.py +4 -0
  5. frob/app/app.py +68 -0
  6. frob/app/arch_runner.py +43 -0
  7. frob/app/bind_runner.py +73 -0
  8. frob/app/bundle_runner.py +28 -0
  9. frob/app/config.py +211 -0
  10. frob/app/cycle_runner.py +67 -0
  11. frob/app/docs_runner.py +79 -0
  12. frob/app/dup_runner.py +36 -0
  13. frob/app/init_runner.py +31 -0
  14. frob/app/inspect_runner.py +85 -0
  15. frob/app/map_runner.py +18 -0
  16. frob/app/outline_runner.py +26 -0
  17. frob/app/parse_runner.py +84 -0
  18. frob/app/stub_runner.py +28 -0
  19. frob/app/tokens_runner.py +18 -0
  20. frob/app/xref_runner.py +29 -0
  21. frob/arch/__init__.py +566 -0
  22. frob/ast/__init__.py +0 -0
  23. frob/ast/avr.py +0 -0
  24. frob/ast/common.py +49 -0
  25. frob/ast/cpp.py +155 -0
  26. frob/ast/python.py +201 -0
  27. frob/bind/__init__.py +95 -0
  28. frob/bundle/__init__.py +226 -0
  29. frob/cycle/__init__.py +0 -0
  30. frob/cycle/graph.py +70 -0
  31. frob/docs/__init__.py +270 -0
  32. frob/dup/__init__.py +644 -0
  33. frob/init/__init__.py +0 -0
  34. frob/init/data/README.md.j2 +1 -0
  35. frob/init/data/cpp_library.CMakeLists.txt.j2 +51 -0
  36. frob/init/data/cpp_library.cmake.Config.cmake.in.j2 +4 -0
  37. frob/init/data/cpp_library.include.h.j2 +3 -0
  38. frob/init/data/cpp_library.src.cpp.j2 +3 -0
  39. frob/init/data/cpp_library.tests.CMakeLists.txt.j2 +16 -0
  40. frob/init/data/cpp_library.tests.cpp.j2 +7 -0
  41. frob/init/data/cpp_shared.Makefile.j2 +72 -0
  42. frob/init/data/cpp_shared.README.md.j2 +16 -0
  43. frob/init/data/cpp_shared.cmake.toolchain-linux-arm64.cmake.j2 +9 -0
  44. frob/init/data/cpp_shared.docs.index.md.j2 +30 -0
  45. frob/init/data/cpp_shared.github.branch-protection.yml.j2 +34 -0
  46. frob/init/data/cpp_shared.github.ci.yml.j2 +71 -0
  47. frob/init/data/cpp_shared.github.release.yml.j2 +99 -0
  48. frob/init/data/cpp_shared.gitignore.j2 +11 -0
  49. frob/init/data/cpp_shared.tests.CMakeLists.txt.j2 +41 -0
  50. frob/init/data/cpp_tool.CMakeLists.txt.j2 +22 -0
  51. frob/init/data/cpp_tool.include.h.j2 +3 -0
  52. frob/init/data/cpp_tool.src.cpp.j2 +3 -0
  53. frob/init/data/cpp_tool.src.main.cpp.j2 +7 -0
  54. frob/init/data/cpp_tool.tests.CMakeLists.txt.j2 +16 -0
  55. frob/init/data/cpp_tool.tests.cpp.j2 +7 -0
  56. frob/init/data/pybind11_library.CMakeLists.txt.j2 +16 -0
  57. frob/init/data/pybind11_library.Makefile.j2 +21 -0
  58. frob/init/data/pybind11_library.github.ci.yml.j2 +30 -0
  59. frob/init/data/pybind11_library.include.h.j2 +3 -0
  60. frob/init/data/pybind11_library.pyproject.toml.j2 +21 -0
  61. frob/init/data/pybind11_library.python.__init__.py.j2 +3 -0
  62. frob/init/data/pybind11_library.src.bindings.cpp.j2 +12 -0
  63. frob/init/data/pybind11_library.src.cpp.j2 +5 -0
  64. frob/init/data/pybind11_library.tests.test_bindings.py.j2 +6 -0
  65. frob/init/data/pybind11_shared.gitignore.j2 +12 -0
  66. frob/init/data/pyo3_library.Cargo.toml.j2 +11 -0
  67. frob/init/data/pyo3_library.Makefile.j2 +22 -0
  68. frob/init/data/pyo3_library.github.ci.yml.j2 +36 -0
  69. frob/init/data/pyo3_library.pyproject.toml.j2 +23 -0
  70. frob/init/data/pyo3_library.python.__init__.py.j2 +3 -0
  71. frob/init/data/pyo3_library.src.lib.rs.j2 +13 -0
  72. frob/init/data/pyo3_library.tests.test_bindings.py.j2 +6 -0
  73. frob/init/data/pyo3_shared.gitignore.j2 +7 -0
  74. frob/init/data/pyproject.toml.j2 +50 -0
  75. frob/init/data/python_lib.__init__.py.j2 +1 -0
  76. frob/init/data/python_shared.Makefile.j2 +67 -0
  77. frob/init/data/python_shared.docs.index.md.j2 +25 -0
  78. frob/init/data/python_shared.github.branch-protection.yml.j2 +32 -0
  79. frob/init/data/python_shared.github.ci.yml.j2 +39 -0
  80. frob/init/data/python_shared.gitignore.j2 +11 -0
  81. frob/init/data/python_shared.logging.__init__.py.j2 +3 -0
  82. frob/init/data/python_shared.logging.config.toml.j2 +30 -0
  83. frob/init/data/python_shared.logging.filter.py.j2 +14 -0
  84. frob/init/data/python_shared.logging.formatter.py.j2 +17 -0
  85. frob/init/data/python_shared.logging.logger.py.j2 +32 -0
  86. frob/init/data/python_shared.tests.conftest.py.j2 +4 -0
  87. frob/init/data/python_shared.tests.system.test_build.py.j2 +37 -0
  88. frob/init/data/python_shared.tests.unit.test_placeholder.py.j2 +2 -0
  89. frob/init/data/python_tool.__init__.py.j2 +1 -0
  90. frob/init/data/python_tool.__main__.py.j2 +18 -0
  91. frob/init/data/python_tool.app.py.j2 +10 -0
  92. frob/init/data/python_tool.config.py.j2 +26 -0
  93. frob/init/project.py +266 -0
  94. frob/inspect/__init__.py +119 -0
  95. frob/logging/__init__.py +3 -0
  96. frob/logging/config.toml +30 -0
  97. frob/logging/filter.py +12 -0
  98. frob/logging/formatter.py +18 -0
  99. frob/logging/handler.py +0 -0
  100. frob/logging/logger.py +25 -0
  101. frob/map/__init__.py +138 -0
  102. frob/outline/__init__.py +228 -0
  103. frob/process/__init__.py +34 -0
  104. frob/process/parsers/__init__.py +23 -0
  105. frob/process/parsers/clang.py +66 -0
  106. frob/process/parsers/common.py +122 -0
  107. frob/process/parsers/junit.py +86 -0
  108. frob/process/parsers/pycharm.py +148 -0
  109. frob/process/parsers/pytest.py +126 -0
  110. frob/process/parsers/ruff.py +104 -0
  111. frob/process/parsers/ty.py +92 -0
  112. frob/stub/__init__.py +59 -0
  113. frob/tokens/__init__.py +168 -0
  114. frob/xref/__init__.py +205 -0
  115. frob-0.0.0.dist-info/METADATA +206 -0
  116. frob-0.0.0.dist-info/RECORD +118 -0
  117. frob-0.0.0.dist-info/WHEEL +5 -0
  118. frob-0.0.0.dist-info/top_level.txt +1 -0
frob/app/config.py ADDED
@@ -0,0 +1,211 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import enum
5
+ from pathlib import Path
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from frob._compat import Self, toml
10
+
11
+
12
+ class Subcommand(str, enum.Enum):
13
+ init = "init"
14
+ cycle = "cycle"
15
+ stub = "stub"
16
+ outline = "outline"
17
+ map = "map"
18
+ xref = "xref"
19
+ tokens = "tokens"
20
+ bundle = "bundle"
21
+ parse = "parse"
22
+ dup = "dup"
23
+ arch = "arch"
24
+ inspect = "inspect"
25
+ docs = "docs"
26
+ bind = "bind"
27
+
28
+
29
+ class AppConfig(BaseModel):
30
+ subcommand: Subcommand | None = None
31
+
32
+ # init
33
+ init_command: str | None = None # "list" or "new"
34
+ init_type: str | None = None
35
+ init_name: str | None = None
36
+ init_output: Path | None = None
37
+ init_force: bool = False
38
+
39
+ # cycle
40
+ cycle_path: Path | None = None
41
+ cycle_lang: str | None = None
42
+ cycle_suggest: bool = False
43
+
44
+ # stub
45
+ stub_file: Path | None = None
46
+ stub_target: str | None = None
47
+ stub_output: Path | None = None
48
+
49
+ # outline
50
+ outline_file: Path | None = None
51
+ outline_json: bool = False
52
+
53
+ # map
54
+ map_path: Path | None = None
55
+ map_json: bool = False
56
+ map_depth: int | None = None
57
+
58
+ # xref
59
+ xref_symbol: str | None = None
60
+ xref_path: Path | None = None
61
+ xref_lang: str | None = None
62
+ xref_json: bool = False
63
+
64
+ # tokens
65
+ tokens_paths: list[Path] = []
66
+ tokens_detail: bool = False
67
+ tokens_json: bool = False
68
+
69
+ # bundle
70
+ bundle_file: Path | None = None
71
+ bundle_target: str | None = None
72
+ bundle_depth: int = 1
73
+ bundle_format: str = "markdown"
74
+
75
+ # dup
76
+ dup_path: Path | None = None
77
+ dup_min_lines: int = 6
78
+ dup_json: bool = False
79
+
80
+ # arch
81
+ arch_path: Path | None = None
82
+ arch_json: bool = False
83
+ arch_max_function_lines: int = 30
84
+ arch_max_class_methods: int = 12
85
+
86
+ # inspect
87
+ inspect_project: Path | None = None
88
+ inspect_pycharm: Path | None = None
89
+ inspect_profile: Path | None = None
90
+ inspect_output_dir: Path | None = None
91
+ inspect_scope: str | None = None
92
+ inspect_json: bool = False
93
+
94
+ # docs
95
+ docs_path: Path | None = None
96
+ docs_symbol: str | None = None
97
+ docs_overview: bool = False
98
+ docs_search: str | None = None
99
+ docs_json: bool = False
100
+
101
+ # parse
102
+ parse_tool: str | None = None
103
+ parse_input: Path | None = None
104
+ parse_exit_code: int = 0
105
+ parse_json: bool = False
106
+ parse_verbose: bool = False
107
+ parse_passthrough: bool = False
108
+
109
+ @classmethod
110
+ def from_external(cls, args: argparse.Namespace, file: Path) -> Self:
111
+ file_cfg: dict = {}
112
+ if file.exists():
113
+ with file.open("rb") as f:
114
+ data = toml.load(f)
115
+ file_cfg = data.get("tool", {}).get("frob", {})
116
+
117
+ d: dict = {**file_cfg}
118
+
119
+ sub = getattr(args, "subcommand", None)
120
+ if sub is not None:
121
+ d["subcommand"] = Subcommand(sub)
122
+
123
+ for field in (
124
+ "init_command",
125
+ "init_type",
126
+ "init_name",
127
+ "cycle_lang",
128
+ "stub_target",
129
+ "xref_symbol",
130
+ "xref_lang",
131
+ "bundle_target",
132
+ "bundle_format",
133
+ "parse_tool",
134
+ "inspect_scope",
135
+ "docs_symbol",
136
+ "docs_search",
137
+ ):
138
+ val = getattr(args, field, None)
139
+ if val is not None:
140
+ d[field] = val
141
+
142
+ for path_field in (
143
+ "init_output",
144
+ "cycle_path",
145
+ "stub_file",
146
+ "stub_output",
147
+ "outline_file",
148
+ "map_path",
149
+ "xref_path",
150
+ "bundle_file",
151
+ "parse_input",
152
+ "dup_path",
153
+ "arch_path",
154
+ "docs_path",
155
+ "inspect_project",
156
+ "inspect_pycharm",
157
+ "inspect_profile",
158
+ "inspect_output_dir",
159
+ ):
160
+ val = getattr(args, path_field, None)
161
+ if val is not None:
162
+ d[path_field] = Path(val)
163
+
164
+ # Multi-path field
165
+ token_paths = getattr(args, "tokens_paths", None)
166
+ if token_paths:
167
+ d["tokens_paths"] = [Path(p) for p in token_paths]
168
+
169
+ # Int fields
170
+ for int_field in (
171
+ "map_depth",
172
+ "bundle_depth",
173
+ "dup_min_lines",
174
+ "arch_max_function_lines",
175
+ "arch_max_class_methods",
176
+ ):
177
+ val = getattr(args, int_field, None)
178
+ if val is not None:
179
+ d[int_field] = val
180
+
181
+ # Int fields
182
+ parse_ec = getattr(args, "parse_exit_code", None)
183
+ if parse_ec is not None:
184
+ d["parse_exit_code"] = int(parse_ec)
185
+
186
+ # Bool flags: only override when explicitly True
187
+ for flag in (
188
+ "init_force",
189
+ "cycle_suggest",
190
+ "outline_json",
191
+ "map_json",
192
+ "xref_json",
193
+ "tokens_detail",
194
+ "tokens_json",
195
+ "parse_json",
196
+ "parse_verbose",
197
+ "parse_passthrough",
198
+ "dup_json",
199
+ "arch_json",
200
+ "inspect_json",
201
+ "docs_json",
202
+ "docs_overview",
203
+ ):
204
+ if getattr(args, flag, False):
205
+ d[flag] = True
206
+
207
+ return cls(**d)
208
+
209
+ @classmethod
210
+ def from_args(cls, args: argparse.Namespace) -> Self:
211
+ return cls.from_external(args, Path("pyproject.toml"))
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from frob.app.config import AppConfig
7
+ from frob.ast.common import ModuleTag
8
+ from frob.cycle.graph import DependencyGraph, find_cycles
9
+ from frob.logging import get_logger
10
+
11
+ _log = get_logger(__name__)
12
+
13
+
14
+ def run(cfg: AppConfig) -> None:
15
+ if cfg.cycle_path is None:
16
+ _log.error("frob cycle requires <path>")
17
+ sys.exit(1)
18
+
19
+ graph, errors = _build_graph(cfg.cycle_path, cfg.cycle_lang)
20
+
21
+ for err in errors:
22
+ _log.warning(err)
23
+
24
+ cycles = find_cycles(graph)
25
+ if not cycles:
26
+ _log.info("no cycles found")
27
+ return
28
+
29
+ for cycle in cycles:
30
+ nodes = " -> ".join(cycle + [cycle[0]])
31
+ _log.info("cycle (%d nodes): %s", len(cycle), nodes)
32
+ if cfg.cycle_suggest:
33
+ _log.info(" suggestion: extract shared symbols into a new module")
34
+
35
+
36
+ def _build_graph(root: Path, lang: str | None) -> tuple[DependencyGraph, list[str]]:
37
+ from frob.ast import cpp as _cpp
38
+ from frob.ast import python as _py
39
+
40
+ graph = DependencyGraph()
41
+ errors: list[str] = []
42
+
43
+ files = [root] if root.is_file() else list(root.rglob("*"))
44
+ scan_root = root.parent if root.is_file() else root
45
+
46
+ for path in files:
47
+ if not path.is_file():
48
+ continue
49
+ ext = path.suffix.lower()
50
+ try:
51
+ rel = str(path.relative_to(scan_root))
52
+ except ValueError:
53
+ continue
54
+
55
+ graph.add_node(rel)
56
+
57
+ try:
58
+ if ext == ".py" and lang in (None, "python"):
59
+ for imp in _py.get_imports(ModuleTag(rel), scan_root):
60
+ graph.add_edge(rel, imp)
61
+ elif ext in _cpp.ALL_EXTS and lang in (None, "cpp", "c"):
62
+ for imp in _cpp.get_imports(ModuleTag(rel), scan_root):
63
+ graph.add_edge(rel, imp)
64
+ except Exception as exc:
65
+ errors.append(f"parse error in {rel}: {exc}")
66
+
67
+ return graph, errors
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from frob.app.config import AppConfig
8
+ from frob.docs import extract_docstrings, find_docs_dir, overview, search
9
+ from frob.logging import get_logger
10
+
11
+ _log = get_logger(__name__)
12
+
13
+
14
+ def run(cfg: AppConfig) -> None:
15
+ path = cfg.docs_path
16
+ if path is None:
17
+ _log.error("frob docs requires <path>")
18
+ sys.exit(1)
19
+ if not path.exists():
20
+ _log.error(f"error: {path} does not exist")
21
+ sys.exit(1)
22
+
23
+ if cfg.docs_search:
24
+ docs_dir = find_docs_dir(path)
25
+ if not docs_dir:
26
+ _log.error("error: no docs/ directory found")
27
+ sys.exit(1)
28
+ matches = search(cfg.docs_search, docs_dir)
29
+ if cfg.docs_json:
30
+ import json
31
+
32
+ _log.info(json.dumps([m.model_dump() for m in matches], indent=2))
33
+ else:
34
+ if not matches:
35
+ _log.info("no matches found")
36
+ for m in matches:
37
+ _log.info(f"{m.file}:{m.line} [{m.heading}]")
38
+ _log.info(f" {m.excerpt}")
39
+ return
40
+
41
+ if cfg.docs_overview:
42
+ docs_dir = find_docs_dir(path)
43
+ if not docs_dir:
44
+ _log.info("no docs/ directory found")
45
+ return
46
+ entries = overview(path, cfg.docs_symbol)
47
+ if cfg.docs_json:
48
+ import json
49
+
50
+ _log.info(json.dumps([e.model_dump() for e in entries], indent=2))
51
+ else:
52
+ for e in entries:
53
+ _log.info(f"## {e.heading} ({e.file}:{e.line})")
54
+ if e.summary:
55
+ _log.info(f" {e.summary}")
56
+ return
57
+
58
+ if path.is_dir():
59
+ results = []
60
+ for root, _, files in os.walk(path):
61
+ for f in files:
62
+ if f.endswith(".py"):
63
+ results.extend(extract_docstrings(Path(root) / f, cfg.docs_symbol))
64
+ else:
65
+ results = extract_docstrings(path, cfg.docs_symbol)
66
+
67
+ if cfg.docs_json:
68
+ import json
69
+
70
+ _log.info(json.dumps([d.model_dump() for d in results], indent=2))
71
+ else:
72
+ if not results:
73
+ _log.info("no docstrings found")
74
+ return
75
+ for d in results:
76
+ _log.info(f"[L{d.line}] {d.symbol} ({d.kind})")
77
+ for line in d.text.splitlines():
78
+ _log.info(f" {line}")
79
+ _log.info("")
frob/app/dup_runner.py ADDED
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from frob.app.config import AppConfig
7
+ from frob.dup import find_duplicates
8
+ from frob.logging import get_logger
9
+
10
+ _log = get_logger(__name__)
11
+
12
+ # AppConfig fields used by this runner (add to AppConfig when wiring CLI):
13
+ # dup_path: Path | None -- directory to scan (required)
14
+ # dup_min_lines: int -- minimum function body size (default 6)
15
+ # dup_json: bool -- emit JSON instead of human-readable text
16
+
17
+
18
+ def run(cfg: AppConfig) -> None:
19
+ dup_path: Path | None = getattr(cfg, "dup_path", None)
20
+ if dup_path is None:
21
+ _log.error("frob dup requires <path>")
22
+ sys.exit(1)
23
+
24
+ if not dup_path.exists():
25
+ _log.error("path does not exist: %s", dup_path)
26
+ sys.exit(1)
27
+
28
+ min_lines: int = getattr(cfg, "dup_min_lines", 6) or 6
29
+ dup_json: bool = getattr(cfg, "dup_json", False)
30
+
31
+ result = find_duplicates(dup_path, min_lines=min_lines)
32
+
33
+ if dup_json:
34
+ _log.info("%s", result.as_json())
35
+ else:
36
+ _log.info("%s", result.as_text())
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from frob.app.config import AppConfig
7
+ from frob.init.project import list_project_types, render_project
8
+ from frob.logging import get_logger
9
+
10
+ _log = get_logger(__name__)
11
+
12
+
13
+ def run(cfg: AppConfig) -> None:
14
+ if cfg.init_command == "list" or cfg.init_command is None:
15
+ for t in list_project_types():
16
+ _log.info(t)
17
+ return
18
+
19
+ if cfg.init_type is None or cfg.init_name is None:
20
+ _log.error("frob init new requires <type> and <name>")
21
+ sys.exit(1)
22
+
23
+ out_dir = cfg.init_output or Path(".")
24
+ result = render_project(cfg.init_type, cfg.init_name, out_dir, force=cfg.init_force)
25
+
26
+ if result.is_err:
27
+ _log.error(result.danger_err.value)
28
+ sys.exit(1)
29
+
30
+ for p in result.danger_ok:
31
+ _log.info("created %s", p)
@@ -0,0 +1,85 @@
1
+ """
2
+ frob inspect -- run PyCharm headless inspection and display results.
3
+
4
+ Usage:
5
+ frob inspect [--pycharm PATH] [--profile PATH] [--output-dir DIR] [--scope DIR]
6
+ <project_dir>
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ import tempfile
13
+ from pathlib import Path
14
+
15
+ from frob.app.config import AppConfig
16
+ from frob.logging import get_logger
17
+
18
+ _log = get_logger(__name__)
19
+
20
+
21
+ def run(cfg: AppConfig) -> None:
22
+ from frob.inspect import find_pycharm, run_inspection
23
+ from frob.process.parsers import parse_pycharm_dir
24
+
25
+ project = cfg.inspect_project
26
+ if project is None:
27
+ _log.error("frob inspect requires <project_dir>")
28
+ sys.exit(1)
29
+
30
+ if not project.exists():
31
+ _log.error("project directory does not exist: %s", project)
32
+ sys.exit(1)
33
+
34
+ # Resolve PyCharm binary
35
+ pycharm = cfg.inspect_pycharm
36
+ if pycharm is None:
37
+ pycharm = find_pycharm()
38
+ if pycharm is None:
39
+ _log.error(
40
+ "cannot find PyCharm inspect.bat -- use --pycharm PATH to specify it"
41
+ )
42
+ sys.exit(1)
43
+
44
+ # Resolve profile
45
+ profile = cfg.inspect_profile
46
+ if profile is None:
47
+ _log.error("--profile PATH is required (path to inspection profile XML)")
48
+ sys.exit(1)
49
+
50
+ if not profile.exists():
51
+ _log.error("profile file does not exist: %s", profile)
52
+ sys.exit(1)
53
+
54
+ # Resolve output directory
55
+ _tmp_dir = None
56
+ if cfg.inspect_output_dir is not None:
57
+ output_dir = cfg.inspect_output_dir
58
+ else:
59
+ _tmp_dir = tempfile.mkdtemp(prefix="frob_pycharm_")
60
+ output_dir = Path(_tmp_dir)
61
+
62
+ _log.info("output dir: %s", output_dir)
63
+
64
+ success, err = run_inspection(
65
+ pycharm,
66
+ project,
67
+ profile,
68
+ output_dir,
69
+ scope=cfg.inspect_scope,
70
+ )
71
+
72
+ if not success:
73
+ assert err is not None
74
+ _log.error("inspection failed: %s", err)
75
+ sys.exit(1)
76
+
77
+ result = parse_pycharm_dir(output_dir, project_root=project)
78
+
79
+ if cfg.inspect_json:
80
+ _log.info(result.as_json())
81
+ else:
82
+ _log.info(result.as_text())
83
+
84
+ if result.error_count:
85
+ sys.exit(1)
frob/app/map_runner.py ADDED
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from frob.app.config import AppConfig
6
+ from frob.logging import get_logger
7
+ from frob.map import map_project
8
+
9
+ _log = get_logger(__name__)
10
+
11
+
12
+ def run(cfg: AppConfig) -> None:
13
+ root = cfg.map_path or Path(".")
14
+ result = map_project(root, depth=cfg.map_depth)
15
+ if cfg.map_json:
16
+ _log.info(result.as_json())
17
+ else:
18
+ _log.info(result.as_text())
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from frob.app.config import AppConfig
6
+ from frob.logging import get_logger
7
+ from frob.outline import outline_file
8
+
9
+ _log = get_logger(__name__)
10
+
11
+
12
+ def run(cfg: AppConfig) -> None:
13
+ if cfg.outline_file is None:
14
+ _log.error("frob outline requires <file>")
15
+ sys.exit(1)
16
+
17
+ result = outline_file(cfg.outline_file)
18
+ if result.is_err:
19
+ _log.error(result.danger_err.value)
20
+ sys.exit(1)
21
+
22
+ ol = result.danger_ok
23
+ if cfg.outline_json:
24
+ _log.info(ol.as_json())
25
+ else:
26
+ _log.info(ol.as_text())
@@ -0,0 +1,84 @@
1
+ """
2
+ frob parse -- read tool output from stdin or a file and emit a compact summary.
3
+
4
+ Usage:
5
+ pytest ... 2>&1 | frob parse pytest
6
+ frob parse ruff < ruff_output.txt
7
+ frob parse ty --json < ty_output.txt
8
+ frob parse clang --exit-code 1 < build.log
9
+ frob parse junit < test_results.xml
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import sys
15
+
16
+ from frob.app.config import AppConfig
17
+ from frob.logging import get_logger
18
+ from frob.process.parsers import (
19
+ parse_clang,
20
+ parse_junit_xml,
21
+ parse_pycharm_dir,
22
+ parse_pytest,
23
+ parse_ruff,
24
+ parse_ty,
25
+ )
26
+
27
+ _log = get_logger(__name__)
28
+
29
+ _PARSERS = {
30
+ "pytest": lambda text, rc: parse_pytest(text, exit_code=rc),
31
+ "ruff": lambda text, rc: parse_ruff(text, exit_code=rc),
32
+ "ty": lambda text, rc: parse_ty(text, exit_code=rc),
33
+ "clang": lambda text, rc: parse_clang(text, exit_code=rc, tool="clang"),
34
+ "clang++": lambda text, rc: parse_clang(text, exit_code=rc, tool="clang++"),
35
+ "gcc": lambda text, rc: parse_clang(text, exit_code=rc, tool="gcc"),
36
+ "g++": lambda text, rc: parse_clang(text, exit_code=rc, tool="g++"),
37
+ "junit": lambda text, rc: parse_junit_xml(text, tool="junit"),
38
+ "gtest": lambda text, rc: parse_junit_xml(text, tool="gtest"),
39
+ "catch2": lambda text, rc: parse_junit_xml(text, tool="catch2"),
40
+ }
41
+
42
+
43
+ def run(cfg: AppConfig) -> None:
44
+ tool = cfg.parse_tool
45
+ if tool is None:
46
+ _log.error("frob parse requires <tool>")
47
+ sys.exit(1)
48
+
49
+ _ALL_TOOLS = set(_PARSERS) | {"pycharm"}
50
+ if tool not in _ALL_TOOLS:
51
+ known = ", ".join(sorted(_ALL_TOOLS))
52
+ _log.error("unknown tool %r -- known: %s", tool, known)
53
+ sys.exit(1)
54
+
55
+ # pycharm takes a directory, not stdin
56
+ if tool == "pycharm":
57
+ if cfg.parse_input is None:
58
+ _log.error("frob parse pycharm requires a directory argument")
59
+ sys.exit(1)
60
+ d = cfg.parse_input
61
+ if not d.is_dir():
62
+ _log.error("frob parse pycharm: %s is not a directory", d)
63
+ sys.exit(1)
64
+ result = parse_pycharm_dir(d)
65
+ else:
66
+ # Read from file or stdin
67
+ if cfg.parse_input is not None:
68
+ try:
69
+ text = cfg.parse_input.read_text()
70
+ except OSError as exc:
71
+ _log.error("cannot read %s: %s", cfg.parse_input, exc)
72
+ sys.exit(1)
73
+ else:
74
+ text = sys.stdin.read()
75
+ result = _PARSERS[tool](text, cfg.parse_exit_code)
76
+
77
+ if cfg.parse_json:
78
+ _log.info(result.as_json())
79
+ else:
80
+ _log.info(result.as_text(verbose=cfg.parse_verbose))
81
+
82
+ # Propagate the tool's exit code so pipelines work correctly
83
+ if cfg.parse_passthrough and not result.passed:
84
+ sys.exit(result.exit_code or 1)
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from frob.app.config import AppConfig
6
+ from frob.logging import get_logger
7
+ from frob.stub import stub_file
8
+
9
+ _log = get_logger(__name__)
10
+
11
+
12
+ def run(cfg: AppConfig) -> None:
13
+ if cfg.stub_file is None or cfg.stub_target is None:
14
+ _log.error("frob stub requires <file> and <target>")
15
+ sys.exit(1)
16
+
17
+ result = stub_file(cfg.stub_file, cfg.stub_target)
18
+
19
+ if result.is_err:
20
+ _log.error(result.danger_err.value)
21
+ sys.exit(1)
22
+
23
+ out = result.danger_ok
24
+ if cfg.stub_output:
25
+ cfg.stub_output.write_text(out)
26
+ _log.info("wrote %s", cfg.stub_output)
27
+ else:
28
+ _log.info(out)
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from frob.app.config import AppConfig
6
+ from frob.logging import get_logger
7
+ from frob.tokens import count_paths
8
+
9
+ _log = get_logger(__name__)
10
+
11
+
12
+ def run(cfg: AppConfig) -> None:
13
+ paths = cfg.tokens_paths or [Path(".")]
14
+ result = count_paths(paths, detail=cfg.tokens_detail)
15
+ if cfg.tokens_json:
16
+ _log.info(result.as_json())
17
+ else:
18
+ _log.info(result.as_text(detail=cfg.tokens_detail))