This commit is contained in:
Iliyan Angelov
2025-12-01 06:50:10 +02:00
parent 91f51bc6fe
commit 62c1fe5951
4682 changed files with 544807 additions and 31208 deletions

View File

@@ -0,0 +1,185 @@
"""
Factory for creating and registering package manager commands.
"""
import importlib
import logging
from pathlib import Path
import sys
from typing import TYPE_CHECKING, cast
import typer
from safety.decorators import notify
from safety.error_handlers import handle_cmd_exception
from safety.tool.decorators import prepare_tool_execution
from .definitions import TOOLS, ToolCommandModel
try:
from typing import Annotated # type: ignore[import]
except ImportError:
from typing_extensions import Annotated
if TYPE_CHECKING:
from safety.cli_util import SafetyCLILegacyGroup
from safety.tool import ToolResult
from safety.cli_util import CustomContext
logger = logging.getLogger(__name__)
class ToolCommandFactory:
"""
Factory for creating command apps per tool.
"""
def _get_command_class_name(self, pkg_name: str) -> str:
"""
Get the command class name for a package manager.
Args:
pkg_name: Name of the package manager
Returns:
str: Command class name
"""
return f"{pkg_name.capitalize()}Command"
def _create_tool_group(
self,
*,
tool_command: ToolCommandModel,
command_class_name: str,
) -> typer.Typer:
"""
Create a standard app for a package manager based on tool command model.
Args:
tool_command: Tool command model with configuration
command_class_name: Name of the command class
Returns:
typer.Typer: The created Typer group
"""
# Get command settings from the tool command model
cmd_settings = tool_command.get_command_settings()
from safety.cli_util import SafetyCLICommand, SafetyCLISubGroup
app = typer.Typer(rich_markup_mode="rich", cls=SafetyCLISubGroup)
# Main command
@app.command(
cls=SafetyCLICommand,
help=cmd_settings.help,
name=cmd_settings.name,
options_metavar=cmd_settings.options_metavar,
context_settings=cmd_settings.context_settings.as_dict(),
)
@handle_cmd_exception
@prepare_tool_execution
@notify
def tool_main_command(
ctx: typer.Context,
target: Annotated[
Path,
typer.Option(
exists=True,
file_okay=False,
dir_okay=True,
writable=False,
readable=True,
resolve_path=True,
show_default=False,
), # type: ignore
] = Path("."),
):
"""
Base command handler that forwards to the appropriate command class.
Args:
ctx: Typer context
"""
# Get the command class directly using importlib
module_name = f"safety.tool.{tool_command.name}.command"
try:
module = importlib.import_module(module_name)
command_class = getattr(module, command_class_name, None)
except ImportError:
logger.error(f"Could not import {module_name}")
command_class = None
if not command_class:
typer.echo(f"Command class {command_class_name} not found")
return
parent_ctx = cast("CustomContext", ctx.parent)
command = command_class.from_args(
ctx.args,
command_alias_used=parent_ctx.command_alias_used,
)
if not command.is_installed():
typer.echo(f"Tool {tool_command.name} is not installed.")
sys.exit(1)
result: "ToolResult" = command.execute(ctx)
if result.process.returncode != 0:
sys.exit(result.process.returncode)
# We can support subcommands in the future
return app
def auto_register_tools(self, group: "SafetyCLILegacyGroup") -> None:
"""
Auto-register commands from the definitions configuration.
Args:
group: The main Safety CLI group
Returns:
Dict[str, typer.Typer]: Dictionary of registered apps
"""
for tool_command_config in TOOLS:
tool_name = tool_command_config.name
# Get the command class name
command_class_name = self._get_command_class_name(tool_name)
tool_app = None
# First check if custom_app is specified in the tool model
if tool_command_config.custom_app:
try:
module_path, attr_name = tool_command_config.custom_app.rsplit(
".", 1
)
module = importlib.import_module(module_path)
tool_app = getattr(module, attr_name, None)
if not tool_app:
logger.error(
f"Custom app {attr_name} not found in {module_path}"
)
except (ImportError, AttributeError, ValueError) as e:
logger.exception(
f"Failed to import custom app for {tool_name}: {e}"
)
# If no custom_app or it failed, create the tool app
if not tool_app:
tool_app = self._create_tool_group(
tool_command=tool_command_config,
command_class_name=command_class_name,
)
# We can support subcommands in the future
# Register the tool app
group.add_command(typer.main.get_command(tool_app), name=tool_name)
logger.info(f"Registered auto-generated command for {tool_name}")
tool_commands = ToolCommandFactory()