apisec-code-bolt 0.1.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.
- apisec_code_bolt/__init__.py +42 -0
- apisec_code_bolt/__main__.py +11 -0
- apisec_code_bolt/analysis/__init__.py +96 -0
- apisec_code_bolt/analysis/analyzer.py +2309 -0
- apisec_code_bolt/analysis/binding_tracker.py +341 -0
- apisec_code_bolt/analysis/call_graph.py +1197 -0
- apisec_code_bolt/analysis/call_graph_types.py +332 -0
- apisec_code_bolt/analysis/call_resolver.py +988 -0
- apisec_code_bolt/analysis/capability_tagger.py +322 -0
- apisec_code_bolt/analysis/config_scanner.py +197 -0
- apisec_code_bolt/analysis/data_flow.py +1883 -0
- apisec_code_bolt/analysis/dependency_extractor.py +959 -0
- apisec_code_bolt/analysis/flow_analysis.py +1406 -0
- apisec_code_bolt/analysis/hof_catalog.py +61 -0
- apisec_code_bolt/analysis/integration_detector.py +1399 -0
- apisec_code_bolt/analysis/literal_scanner.py +300 -0
- apisec_code_bolt/analysis/path_normalizer.py +55 -0
- apisec_code_bolt/analysis/read_site_detector.py +310 -0
- apisec_code_bolt/analysis/request_patterns.py +162 -0
- apisec_code_bolt/analysis/sensitivity_classifier.py +224 -0
- apisec_code_bolt/analysis/sink_evidence.py +333 -0
- apisec_code_bolt/analysis/url_prefix_resolver.py +338 -0
- apisec_code_bolt/cli/__init__.py +5 -0
- apisec_code_bolt/cli/exit_codes.py +17 -0
- apisec_code_bolt/cli/main.py +1069 -0
- apisec_code_bolt/cloud/__init__.py +1 -0
- apisec_code_bolt/cloud/apisec_client.py +118 -0
- apisec_code_bolt/cloud/client.py +255 -0
- apisec_code_bolt/core/__init__.py +75 -0
- apisec_code_bolt/core/config.py +528 -0
- apisec_code_bolt/core/credentials.py +65 -0
- apisec_code_bolt/core/discovery.py +433 -0
- apisec_code_bolt/core/log_format.py +115 -0
- apisec_code_bolt/core/manifest.py +1009 -0
- apisec_code_bolt/core/repo.py +280 -0
- apisec_code_bolt/core/state.py +59 -0
- apisec_code_bolt/core/telemetry.py +451 -0
- apisec_code_bolt/core/types.py +587 -0
- apisec_code_bolt/fingerprinting/__init__.py +1 -0
- apisec_code_bolt/frameworks/__init__.py +29 -0
- apisec_code_bolt/frameworks/_jwt_common.py +50 -0
- apisec_code_bolt/frameworks/auth_helpers.py +437 -0
- apisec_code_bolt/frameworks/base.py +608 -0
- apisec_code_bolt/frameworks/dotnet/__init__.py +17 -0
- apisec_code_bolt/frameworks/dotnet/_path_helpers.py +43 -0
- apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2546 -0
- apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +559 -0
- apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +545 -0
- apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +732 -0
- apisec_code_bolt/frameworks/dotnet/refit_plugin.py +374 -0
- apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +1239 -0
- apisec_code_bolt/frameworks/java/__init__.py +6 -0
- apisec_code_bolt/frameworks/java/_annotations.py +167 -0
- apisec_code_bolt/frameworks/java/_constraints.py +128 -0
- apisec_code_bolt/frameworks/java/graphql_plugin.py +287 -0
- apisec_code_bolt/frameworks/java/jaxrs_plugin.py +748 -0
- apisec_code_bolt/frameworks/java/jwt_config_extractor.py +361 -0
- apisec_code_bolt/frameworks/java/micronaut_plugin.py +1059 -0
- apisec_code_bolt/frameworks/java/spring_plugin.py +1293 -0
- apisec_code_bolt/frameworks/js/__init__.py +8 -0
- apisec_code_bolt/frameworks/js/express_plugin.py +391 -0
- apisec_code_bolt/frameworks/js/fastify_plugin.py +381 -0
- apisec_code_bolt/frameworks/js/graphql_plugin.py +198 -0
- apisec_code_bolt/frameworks/js/nestjs_plugin.py +423 -0
- apisec_code_bolt/frameworks/python/__init__.py +19 -0
- apisec_code_bolt/frameworks/python/celery_plugin.py +393 -0
- apisec_code_bolt/frameworks/python/click_plugin.py +427 -0
- apisec_code_bolt/frameworks/python/django_plugin.py +867 -0
- apisec_code_bolt/frameworks/python/fastapi/__init__.py +28 -0
- apisec_code_bolt/frameworks/python/fastapi/plugin.py +1390 -0
- apisec_code_bolt/frameworks/python/flask_plugin.py +205 -0
- apisec_code_bolt/frameworks/python/graphql_plugin.py +274 -0
- apisec_code_bolt/frameworks/python/prefect_plugin.py +251 -0
- apisec_code_bolt/frameworks/python/webhook_plugin.py +255 -0
- apisec_code_bolt/parsing/__init__.py +62 -0
- apisec_code_bolt/parsing/base.py +554 -0
- apisec_code_bolt/parsing/csharp/__init__.py +5 -0
- apisec_code_bolt/parsing/csharp/language_services.py +203 -0
- apisec_code_bolt/parsing/csharp/literals.py +72 -0
- apisec_code_bolt/parsing/csharp/parser.py +1158 -0
- apisec_code_bolt/parsing/csharp/type_resolver.py +568 -0
- apisec_code_bolt/parsing/js/__init__.py +5 -0
- apisec_code_bolt/parsing/js/language_services.py +118 -0
- apisec_code_bolt/parsing/js/parser.py +622 -0
- apisec_code_bolt/parsing/jvm/__init__.py +7 -0
- apisec_code_bolt/parsing/jvm/language_services.py +270 -0
- apisec_code_bolt/parsing/jvm/parser.py +774 -0
- apisec_code_bolt/parsing/jvm/type_resolver.py +422 -0
- apisec_code_bolt/parsing/python/__init__.py +150 -0
- apisec_code_bolt/parsing/python/cbv_extractor.py +606 -0
- apisec_code_bolt/parsing/python/constant_resolver.py +500 -0
- apisec_code_bolt/parsing/python/cross_file_resolver.py +1054 -0
- apisec_code_bolt/parsing/python/dynamic_route_detector.py +532 -0
- apisec_code_bolt/parsing/python/expression_utils.py +221 -0
- apisec_code_bolt/parsing/python/extraction_types.py +271 -0
- apisec_code_bolt/parsing/python/language_services.py +487 -0
- apisec_code_bolt/parsing/python/parameter_analyzer.py +789 -0
- apisec_code_bolt/parsing/python/parser.py +719 -0
- apisec_code_bolt/parsing/python/path_resolver.py +576 -0
- apisec_code_bolt/parsing/python/router_registry.py +806 -0
- apisec_code_bolt/parsing/python/type_resolver.py +730 -0
- apisec_code_bolt/parsing/python/visitors.py +1544 -0
- apisec_code_bolt/parsing/services.py +544 -0
- apisec_code_bolt/query/__init__.py +1 -0
- apisec_code_bolt/query/ast_cache.py +182 -0
- apisec_code_bolt/query/executor.py +283 -0
- apisec_code_bolt/query/handlers.py +832 -0
- apisec_code_bolt-0.1.0.dist-info/METADATA +230 -0
- apisec_code_bolt-0.1.0.dist-info/RECORD +111 -0
- apisec_code_bolt-0.1.0.dist-info/WHEEL +4 -0
- apisec_code_bolt-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Click and Typer CLI framework plugin.
|
|
3
|
+
|
|
4
|
+
Detects CLI command entry points from Click and Typer decorator patterns.
|
|
5
|
+
|
|
6
|
+
Supported constructs:
|
|
7
|
+
- @click.command() / @click.group() — standalone commands and groups
|
|
8
|
+
- @app.command() / @grp.command() — commands registered on groups
|
|
9
|
+
- @click.option() / @click.argument() — parameter decorators
|
|
10
|
+
- @click.password_option() — sensitive input patterns
|
|
11
|
+
- @click.pass_context / @click.pass_obj — context injection (skipped in params)
|
|
12
|
+
- Typer @app.command() / @app.callback() — Typer command patterns
|
|
13
|
+
- Typer function parameter defaults (typer.Option, typer.Argument)
|
|
14
|
+
|
|
15
|
+
Not supported (yet):
|
|
16
|
+
- Dynamic command registration via group.add_command()
|
|
17
|
+
- click.MultiCommand subclasses
|
|
18
|
+
- Lazy-loaded command groups
|
|
19
|
+
- Complex group nesting hierarchy reconstruction
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from ...core.types import (
|
|
28
|
+
Confidence,
|
|
29
|
+
Framework,
|
|
30
|
+
HttpMethod,
|
|
31
|
+
Language,
|
|
32
|
+
ParameterLocation,
|
|
33
|
+
)
|
|
34
|
+
from ...parsing.base import ParsedDecorator, ParsedFile, ParsedFunction, ParsedParameter
|
|
35
|
+
from ..base import (
|
|
36
|
+
BaseFrameworkPlugin,
|
|
37
|
+
ExtractedAuthDependency,
|
|
38
|
+
ExtractedAuthScheme,
|
|
39
|
+
ExtractedDependency,
|
|
40
|
+
ExtractedMiddleware,
|
|
41
|
+
ExtractedParameter,
|
|
42
|
+
ExtractedResponse,
|
|
43
|
+
ExtractedRoute,
|
|
44
|
+
FrameworkPluginRegistry,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
CLICK_IMPORTS = frozenset({"click", "typer"})
|
|
48
|
+
|
|
49
|
+
# Decorators that mark a function as a CLI command entry point.
|
|
50
|
+
COMMAND_DECORATORS: set[str] = {
|
|
51
|
+
"command",
|
|
52
|
+
"group",
|
|
53
|
+
"callback",
|
|
54
|
+
"click.command",
|
|
55
|
+
"click.group",
|
|
56
|
+
"typer.command",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Decorators that define CLI parameters (options/arguments).
|
|
60
|
+
OPTION_DECORATORS: set[str] = {
|
|
61
|
+
"option",
|
|
62
|
+
"click.option",
|
|
63
|
+
"password_option",
|
|
64
|
+
"click.password_option",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ARGUMENT_DECORATORS: set[str] = {
|
|
68
|
+
"argument",
|
|
69
|
+
"click.argument",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Decorators that inject Click context objects — the first function parameter
|
|
73
|
+
# they introduce should be skipped during parameter extraction.
|
|
74
|
+
CONTEXT_DECORATORS: set[str] = {
|
|
75
|
+
"pass_context",
|
|
76
|
+
"click.pass_context",
|
|
77
|
+
"pass_obj",
|
|
78
|
+
"click.pass_obj",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Suffixes stripped from function names when deriving the CLI command path.
|
|
82
|
+
_COMMAND_SUFFIXES = ("_command", "_cmd", "_group", "_grp")
|
|
83
|
+
|
|
84
|
+
_CLI_ARGUMENT_LOCATION = ParameterLocation.CLI_ARGUMENT
|
|
85
|
+
_CLI_OPTION_LOCATION = ParameterLocation.CLI_OPTION
|
|
86
|
+
|
|
87
|
+
_STRIP_DASHES_RE = re.compile(r"^-+")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _derive_command_name(func_name: str) -> str:
|
|
91
|
+
"""Derive a CLI command name from a Python function name.
|
|
92
|
+
|
|
93
|
+
Applies Click's default naming convention: lowercase, underscores become
|
|
94
|
+
dashes, and common command/group suffixes are stripped.
|
|
95
|
+
"""
|
|
96
|
+
name = func_name.lower()
|
|
97
|
+
for suffix in _COMMAND_SUFFIXES:
|
|
98
|
+
if name.endswith(suffix):
|
|
99
|
+
name = name[: -len(suffix)]
|
|
100
|
+
break
|
|
101
|
+
return name.replace("_", "-")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _option_name_from_decls(decls: list[Any]) -> str:
|
|
105
|
+
"""Extract the canonical option name from param_decls.
|
|
106
|
+
|
|
107
|
+
Click param_decls are like ("--verbose", "-v"). We pick the longest
|
|
108
|
+
long-option string, strip leading dashes, and normalise.
|
|
109
|
+
"""
|
|
110
|
+
long_opts = [_STRIP_DASHES_RE.sub("", str(d)) for d in decls if str(d).startswith("--")]
|
|
111
|
+
if long_opts:
|
|
112
|
+
return max(long_opts, key=len).replace("-", "_")
|
|
113
|
+
# Fall back to the first positional decl.
|
|
114
|
+
if decls:
|
|
115
|
+
return _STRIP_DASHES_RE.sub("", str(decls[0])).replace("-", "_")
|
|
116
|
+
return "unknown"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _argument_name_from_decls(decls: list[Any]) -> str:
|
|
120
|
+
"""Extract argument name from param_decls (usually a single positional)."""
|
|
121
|
+
if decls:
|
|
122
|
+
return str(decls[0]).strip().replace("-", "_")
|
|
123
|
+
return "unknown"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ClickPlugin(BaseFrameworkPlugin):
|
|
127
|
+
"""Plugin for Click and Typer CLI framework extraction."""
|
|
128
|
+
|
|
129
|
+
FRAMEWORK = Framework.CLICK
|
|
130
|
+
LANGUAGE = Language.PYTHON
|
|
131
|
+
DETECTION_IMPORTS = CLICK_IMPORTS
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
# Detection
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def detect(self, parsed_file: ParsedFile) -> bool:
|
|
138
|
+
for imp in parsed_file.imports:
|
|
139
|
+
mod = (imp.module or "").split(".")[0]
|
|
140
|
+
if mod in ("click", "typer"):
|
|
141
|
+
return True
|
|
142
|
+
for name in imp.names:
|
|
143
|
+
if name in ("click", "typer", "Typer"):
|
|
144
|
+
return True
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
# ------------------------------------------------------------------
|
|
148
|
+
# Route / command extraction
|
|
149
|
+
# ------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
def extract_routes(
|
|
152
|
+
self,
|
|
153
|
+
parsed_file: ParsedFile,
|
|
154
|
+
context: Any = None,
|
|
155
|
+
) -> list[ExtractedRoute]:
|
|
156
|
+
routes: list[ExtractedRoute] = []
|
|
157
|
+
|
|
158
|
+
for func in parsed_file.functions:
|
|
159
|
+
route = self._try_extract_command(func)
|
|
160
|
+
if route:
|
|
161
|
+
routes.append(route)
|
|
162
|
+
|
|
163
|
+
for cls in parsed_file.classes:
|
|
164
|
+
for method in cls.methods:
|
|
165
|
+
route = self._try_extract_command(method)
|
|
166
|
+
if route:
|
|
167
|
+
routes.append(route)
|
|
168
|
+
|
|
169
|
+
return routes
|
|
170
|
+
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
# Stubs — CLI frameworks have no HTTP-level dependency/auth/middleware
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
|
|
176
|
+
return []
|
|
177
|
+
|
|
178
|
+
def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
|
|
179
|
+
return []
|
|
180
|
+
|
|
181
|
+
def extract_auth_dependencies(
|
|
182
|
+
self,
|
|
183
|
+
parsed_file: ParsedFile,
|
|
184
|
+
known_scheme_names: set[str] | None = None,
|
|
185
|
+
**kwargs: Any,
|
|
186
|
+
) -> list[ExtractedAuthDependency]:
|
|
187
|
+
return []
|
|
188
|
+
|
|
189
|
+
def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
|
|
190
|
+
return []
|
|
191
|
+
|
|
192
|
+
# ------------------------------------------------------------------
|
|
193
|
+
# Internal helpers
|
|
194
|
+
# ------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
def _try_extract_command(self, func: ParsedFunction) -> ExtractedRoute | None:
|
|
197
|
+
"""If *func* is decorated as a Click/Typer command, return an ExtractedRoute."""
|
|
198
|
+
cmd_dec = self._find_command_decorator(func)
|
|
199
|
+
if cmd_dec is None:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
command_name = self._resolve_command_name(cmd_dec, func.name)
|
|
203
|
+
has_ctx = self._has_context_decorator(func)
|
|
204
|
+
params = self._extract_parameters(func, has_ctx)
|
|
205
|
+
|
|
206
|
+
return ExtractedRoute(
|
|
207
|
+
kind="cli",
|
|
208
|
+
method=HttpMethod.GET,
|
|
209
|
+
path=command_name,
|
|
210
|
+
handler_function=func.qualified_name,
|
|
211
|
+
handler_location=func.location,
|
|
212
|
+
query_params=[p for p in params if p.location == _CLI_OPTION_LOCATION],
|
|
213
|
+
path_params=[p for p in params if p.location == _CLI_ARGUMENT_LOCATION],
|
|
214
|
+
header_params=[],
|
|
215
|
+
cookie_params=[],
|
|
216
|
+
body=None,
|
|
217
|
+
response=ExtractedResponse(),
|
|
218
|
+
confidence=Confidence.HIGH,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def _find_command_decorator(self, func: ParsedFunction) -> ParsedDecorator | None:
|
|
222
|
+
"""Return the first command/group/callback decorator on *func*."""
|
|
223
|
+
for dec in func.decorators:
|
|
224
|
+
base = self._decorator_base_name(dec)
|
|
225
|
+
if base in COMMAND_DECORATORS:
|
|
226
|
+
return dec
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def _has_context_decorator(self, func: ParsedFunction) -> bool:
|
|
230
|
+
for dec in func.decorators:
|
|
231
|
+
base = self._decorator_base_name(dec)
|
|
232
|
+
if base in CONTEXT_DECORATORS:
|
|
233
|
+
return True
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
def _resolve_command_name(self, dec: ParsedDecorator, func_name: str) -> str:
|
|
237
|
+
"""Get the CLI command path from the decorator or function name."""
|
|
238
|
+
explicit = self._extract_decorator_arg(dec, "name")
|
|
239
|
+
if explicit and isinstance(explicit, str):
|
|
240
|
+
return explicit
|
|
241
|
+
if dec.positional_args:
|
|
242
|
+
first = dec.positional_args[0]
|
|
243
|
+
if isinstance(first, str) and not first.startswith("-"):
|
|
244
|
+
return first
|
|
245
|
+
return _derive_command_name(func_name)
|
|
246
|
+
|
|
247
|
+
# ---- parameter extraction ----
|
|
248
|
+
|
|
249
|
+
def _extract_parameters(
|
|
250
|
+
self,
|
|
251
|
+
func: ParsedFunction,
|
|
252
|
+
has_ctx: bool,
|
|
253
|
+
) -> list[ExtractedParameter]:
|
|
254
|
+
"""Extract CLI parameters from decorators and function signature."""
|
|
255
|
+
params: list[ExtractedParameter] = []
|
|
256
|
+
decorator_param_names: set[str] = set()
|
|
257
|
+
|
|
258
|
+
for dec in func.decorators:
|
|
259
|
+
base = self._decorator_base_name(dec)
|
|
260
|
+
|
|
261
|
+
if base in OPTION_DECORATORS:
|
|
262
|
+
ep = self._param_from_option_decorator(dec, base)
|
|
263
|
+
params.append(ep)
|
|
264
|
+
decorator_param_names.add(ep.name)
|
|
265
|
+
|
|
266
|
+
elif base in ARGUMENT_DECORATORS:
|
|
267
|
+
ep = self._param_from_argument_decorator(dec)
|
|
268
|
+
params.append(ep)
|
|
269
|
+
decorator_param_names.add(ep.name)
|
|
270
|
+
|
|
271
|
+
# Infer remaining function parameters not covered by decorators.
|
|
272
|
+
params.extend(
|
|
273
|
+
self._infer_params_from_signature(
|
|
274
|
+
func.parameters,
|
|
275
|
+
decorator_param_names,
|
|
276
|
+
has_ctx,
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
return params
|
|
281
|
+
|
|
282
|
+
def _param_from_option_decorator(
|
|
283
|
+
self,
|
|
284
|
+
dec: ParsedDecorator,
|
|
285
|
+
base_name: str,
|
|
286
|
+
) -> ExtractedParameter:
|
|
287
|
+
if base_name in ("password_option", "click.password_option"):
|
|
288
|
+
name = (
|
|
289
|
+
_option_name_from_decls(dec.positional_args) if dec.positional_args else "password"
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
name = (
|
|
293
|
+
_option_name_from_decls(dec.positional_args) if dec.positional_args else "unknown"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
required = dec.arguments.get("required", False)
|
|
297
|
+
default = dec.arguments.get("default")
|
|
298
|
+
type_hint = dec.arguments.get("type")
|
|
299
|
+
|
|
300
|
+
return ExtractedParameter(
|
|
301
|
+
name=name,
|
|
302
|
+
location=_CLI_OPTION_LOCATION,
|
|
303
|
+
type_annotation=str(type_hint) if type_hint is not None else None,
|
|
304
|
+
required=bool(required),
|
|
305
|
+
default_value=str(default) if default is not None else None,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def _param_from_argument_decorator(self, dec: ParsedDecorator) -> ExtractedParameter:
|
|
309
|
+
name = _argument_name_from_decls(dec.positional_args) if dec.positional_args else "unknown"
|
|
310
|
+
required = dec.arguments.get("required", True)
|
|
311
|
+
default = dec.arguments.get("default")
|
|
312
|
+
type_hint = dec.arguments.get("type")
|
|
313
|
+
|
|
314
|
+
return ExtractedParameter(
|
|
315
|
+
name=name,
|
|
316
|
+
location=_CLI_ARGUMENT_LOCATION,
|
|
317
|
+
type_annotation=str(type_hint) if type_hint is not None else None,
|
|
318
|
+
required=bool(required),
|
|
319
|
+
default_value=str(default) if default is not None else None,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
def _infer_params_from_signature(
|
|
323
|
+
self,
|
|
324
|
+
parameters: list[ParsedParameter],
|
|
325
|
+
already_extracted: set[str],
|
|
326
|
+
has_ctx: bool,
|
|
327
|
+
) -> list[ExtractedParameter]:
|
|
328
|
+
"""Infer CLI params from the function signature.
|
|
329
|
+
|
|
330
|
+
Handles Typer-style defaults (``typer.Option(...)``, ``typer.Argument(...)``).
|
|
331
|
+
"""
|
|
332
|
+
results: list[ExtractedParameter] = []
|
|
333
|
+
skip_first = has_ctx
|
|
334
|
+
|
|
335
|
+
for param in parameters:
|
|
336
|
+
if param.is_variadic or param.is_keyword_variadic:
|
|
337
|
+
continue
|
|
338
|
+
if skip_first:
|
|
339
|
+
skip_first = False
|
|
340
|
+
continue
|
|
341
|
+
if param.name in already_extracted:
|
|
342
|
+
continue
|
|
343
|
+
if param.name in ("self", "cls"):
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
default = param.default_value
|
|
347
|
+
if default and "typer.Option" in default:
|
|
348
|
+
results.append(
|
|
349
|
+
ExtractedParameter(
|
|
350
|
+
name=param.name,
|
|
351
|
+
location=_CLI_OPTION_LOCATION,
|
|
352
|
+
type_annotation=param.type_annotation,
|
|
353
|
+
required=False,
|
|
354
|
+
default_value=None,
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
elif default and "typer.Argument" in default or default is None:
|
|
358
|
+
results.append(
|
|
359
|
+
ExtractedParameter(
|
|
360
|
+
name=param.name,
|
|
361
|
+
location=_CLI_ARGUMENT_LOCATION,
|
|
362
|
+
type_annotation=param.type_annotation,
|
|
363
|
+
required=True,
|
|
364
|
+
default_value=None,
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
else:
|
|
368
|
+
results.append(
|
|
369
|
+
ExtractedParameter(
|
|
370
|
+
name=param.name,
|
|
371
|
+
location=_CLI_OPTION_LOCATION,
|
|
372
|
+
type_annotation=param.type_annotation,
|
|
373
|
+
required=False,
|
|
374
|
+
default_value=default,
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
return results
|
|
379
|
+
|
|
380
|
+
# ---- decorator name helpers ----
|
|
381
|
+
|
|
382
|
+
@staticmethod
|
|
383
|
+
def _decorator_base_name(dec: ParsedDecorator) -> str:
|
|
384
|
+
"""Normalise a decorator to a lookup key.
|
|
385
|
+
|
|
386
|
+
Handles both ``click.command`` (qualified) and ``command`` (bare import)
|
|
387
|
+
as well as ``app.command`` / ``grp.group`` attribute forms.
|
|
388
|
+
"""
|
|
389
|
+
if dec.qualified_name:
|
|
390
|
+
qn = dec.qualified_name.full
|
|
391
|
+
parts = qn.rsplit(".", 1)
|
|
392
|
+
if len(parts) == 2:
|
|
393
|
+
mod, attr = parts
|
|
394
|
+
if mod in ("click", "typer"):
|
|
395
|
+
return f"{mod}.{attr}"
|
|
396
|
+
if attr in (
|
|
397
|
+
"command",
|
|
398
|
+
"group",
|
|
399
|
+
"callback",
|
|
400
|
+
"option",
|
|
401
|
+
"argument",
|
|
402
|
+
"password_option",
|
|
403
|
+
"pass_context",
|
|
404
|
+
"pass_obj",
|
|
405
|
+
):
|
|
406
|
+
return attr
|
|
407
|
+
return qn
|
|
408
|
+
|
|
409
|
+
name = dec.name
|
|
410
|
+
if "." in name:
|
|
411
|
+
_obj, _, attr = name.rpartition(".")
|
|
412
|
+
if attr in (
|
|
413
|
+
"command",
|
|
414
|
+
"group",
|
|
415
|
+
"callback",
|
|
416
|
+
"option",
|
|
417
|
+
"argument",
|
|
418
|
+
"password_option",
|
|
419
|
+
"pass_context",
|
|
420
|
+
"pass_obj",
|
|
421
|
+
):
|
|
422
|
+
return attr
|
|
423
|
+
return name
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
_click_plugin = ClickPlugin()
|
|
427
|
+
FrameworkPluginRegistry.register(_click_plugin)
|