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.
- frob/__init__.py +0 -0
- frob/__main__.py +296 -0
- frob/_compat.py +18 -0
- frob/app/__init__.py +4 -0
- frob/app/app.py +68 -0
- frob/app/arch_runner.py +43 -0
- frob/app/bind_runner.py +73 -0
- frob/app/bundle_runner.py +28 -0
- frob/app/config.py +211 -0
- frob/app/cycle_runner.py +67 -0
- frob/app/docs_runner.py +79 -0
- frob/app/dup_runner.py +36 -0
- frob/app/init_runner.py +31 -0
- frob/app/inspect_runner.py +85 -0
- frob/app/map_runner.py +18 -0
- frob/app/outline_runner.py +26 -0
- frob/app/parse_runner.py +84 -0
- frob/app/stub_runner.py +28 -0
- frob/app/tokens_runner.py +18 -0
- frob/app/xref_runner.py +29 -0
- frob/arch/__init__.py +566 -0
- frob/ast/__init__.py +0 -0
- frob/ast/avr.py +0 -0
- frob/ast/common.py +49 -0
- frob/ast/cpp.py +155 -0
- frob/ast/python.py +201 -0
- frob/bind/__init__.py +95 -0
- frob/bundle/__init__.py +226 -0
- frob/cycle/__init__.py +0 -0
- frob/cycle/graph.py +70 -0
- frob/docs/__init__.py +270 -0
- frob/dup/__init__.py +644 -0
- frob/init/__init__.py +0 -0
- frob/init/data/README.md.j2 +1 -0
- frob/init/data/cpp_library.CMakeLists.txt.j2 +51 -0
- frob/init/data/cpp_library.cmake.Config.cmake.in.j2 +4 -0
- frob/init/data/cpp_library.include.h.j2 +3 -0
- frob/init/data/cpp_library.src.cpp.j2 +3 -0
- frob/init/data/cpp_library.tests.CMakeLists.txt.j2 +16 -0
- frob/init/data/cpp_library.tests.cpp.j2 +7 -0
- frob/init/data/cpp_shared.Makefile.j2 +72 -0
- frob/init/data/cpp_shared.README.md.j2 +16 -0
- frob/init/data/cpp_shared.cmake.toolchain-linux-arm64.cmake.j2 +9 -0
- frob/init/data/cpp_shared.docs.index.md.j2 +30 -0
- frob/init/data/cpp_shared.github.branch-protection.yml.j2 +34 -0
- frob/init/data/cpp_shared.github.ci.yml.j2 +71 -0
- frob/init/data/cpp_shared.github.release.yml.j2 +99 -0
- frob/init/data/cpp_shared.gitignore.j2 +11 -0
- frob/init/data/cpp_shared.tests.CMakeLists.txt.j2 +41 -0
- frob/init/data/cpp_tool.CMakeLists.txt.j2 +22 -0
- frob/init/data/cpp_tool.include.h.j2 +3 -0
- frob/init/data/cpp_tool.src.cpp.j2 +3 -0
- frob/init/data/cpp_tool.src.main.cpp.j2 +7 -0
- frob/init/data/cpp_tool.tests.CMakeLists.txt.j2 +16 -0
- frob/init/data/cpp_tool.tests.cpp.j2 +7 -0
- frob/init/data/pybind11_library.CMakeLists.txt.j2 +16 -0
- frob/init/data/pybind11_library.Makefile.j2 +21 -0
- frob/init/data/pybind11_library.github.ci.yml.j2 +30 -0
- frob/init/data/pybind11_library.include.h.j2 +3 -0
- frob/init/data/pybind11_library.pyproject.toml.j2 +21 -0
- frob/init/data/pybind11_library.python.__init__.py.j2 +3 -0
- frob/init/data/pybind11_library.src.bindings.cpp.j2 +12 -0
- frob/init/data/pybind11_library.src.cpp.j2 +5 -0
- frob/init/data/pybind11_library.tests.test_bindings.py.j2 +6 -0
- frob/init/data/pybind11_shared.gitignore.j2 +12 -0
- frob/init/data/pyo3_library.Cargo.toml.j2 +11 -0
- frob/init/data/pyo3_library.Makefile.j2 +22 -0
- frob/init/data/pyo3_library.github.ci.yml.j2 +36 -0
- frob/init/data/pyo3_library.pyproject.toml.j2 +23 -0
- frob/init/data/pyo3_library.python.__init__.py.j2 +3 -0
- frob/init/data/pyo3_library.src.lib.rs.j2 +13 -0
- frob/init/data/pyo3_library.tests.test_bindings.py.j2 +6 -0
- frob/init/data/pyo3_shared.gitignore.j2 +7 -0
- frob/init/data/pyproject.toml.j2 +50 -0
- frob/init/data/python_lib.__init__.py.j2 +1 -0
- frob/init/data/python_shared.Makefile.j2 +67 -0
- frob/init/data/python_shared.docs.index.md.j2 +25 -0
- frob/init/data/python_shared.github.branch-protection.yml.j2 +32 -0
- frob/init/data/python_shared.github.ci.yml.j2 +39 -0
- frob/init/data/python_shared.gitignore.j2 +11 -0
- frob/init/data/python_shared.logging.__init__.py.j2 +3 -0
- frob/init/data/python_shared.logging.config.toml.j2 +30 -0
- frob/init/data/python_shared.logging.filter.py.j2 +14 -0
- frob/init/data/python_shared.logging.formatter.py.j2 +17 -0
- frob/init/data/python_shared.logging.logger.py.j2 +32 -0
- frob/init/data/python_shared.tests.conftest.py.j2 +4 -0
- frob/init/data/python_shared.tests.system.test_build.py.j2 +37 -0
- frob/init/data/python_shared.tests.unit.test_placeholder.py.j2 +2 -0
- frob/init/data/python_tool.__init__.py.j2 +1 -0
- frob/init/data/python_tool.__main__.py.j2 +18 -0
- frob/init/data/python_tool.app.py.j2 +10 -0
- frob/init/data/python_tool.config.py.j2 +26 -0
- frob/init/project.py +266 -0
- frob/inspect/__init__.py +119 -0
- frob/logging/__init__.py +3 -0
- frob/logging/config.toml +30 -0
- frob/logging/filter.py +12 -0
- frob/logging/formatter.py +18 -0
- frob/logging/handler.py +0 -0
- frob/logging/logger.py +25 -0
- frob/map/__init__.py +138 -0
- frob/outline/__init__.py +228 -0
- frob/process/__init__.py +34 -0
- frob/process/parsers/__init__.py +23 -0
- frob/process/parsers/clang.py +66 -0
- frob/process/parsers/common.py +122 -0
- frob/process/parsers/junit.py +86 -0
- frob/process/parsers/pycharm.py +148 -0
- frob/process/parsers/pytest.py +126 -0
- frob/process/parsers/ruff.py +104 -0
- frob/process/parsers/ty.py +92 -0
- frob/stub/__init__.py +59 -0
- frob/tokens/__init__.py +168 -0
- frob/xref/__init__.py +205 -0
- frob-0.0.0.dist-info/METADATA +206 -0
- frob-0.0.0.dist-info/RECORD +118 -0
- frob-0.0.0.dist-info/WHEEL +5 -0
- 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"))
|
frob/app/cycle_runner.py
ADDED
|
@@ -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
|
frob/app/docs_runner.py
ADDED
|
@@ -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())
|
frob/app/init_runner.py
ADDED
|
@@ -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())
|
frob/app/parse_runner.py
ADDED
|
@@ -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)
|
frob/app/stub_runner.py
ADDED
|
@@ -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))
|