import logging import os from pathlib import Path import shutil import sys from typing import Any, Dict, Optional import tomlkit import typer from rich.console import Console from safety.console import main_console from safety.tool.auth import build_index_url from safety.tool.constants import ( PYPI_ORGANIZATION_REPOSITORY_URL, PYPI_PUBLIC_REPOSITORY_URL, PYPI_PROJECT_REPOSITORY_URL, ) from safety.utils.pyapp_utils import get_path if sys.version_info >= (3, 11): import tomllib else: import tomli as tomllib logger = logging.getLogger(__name__) def backup_file(path: Path) -> None: """ Create backup of file if it exists """ if path.exists(): backup_path = path.with_name(f"{path.name}.backup") shutil.copy2(path, backup_path) class Uv: @classmethod def is_installed(cls) -> bool: """ Checks if the UV program is installed Returns: True if UV is installed on system, or false otherwise """ return shutil.which("uv", path=get_path()) is not None @classmethod def is_uv_project_file(cls, file: Path) -> bool: try: cfg = tomllib.loads(file.read_text()) return ( cfg.get("tool", {}).get("uv") is not None or (file.parent / "uv.lock").exists() ) except (IOError, ValueError): return False @classmethod def configure_pyproject( cls, file: Path, org_slug: Optional[str], project_id: Optional[str] = None, console: Console = main_console, ) -> Optional[Path]: """ Configures index url for specified pyproject.toml file. Args: file (Path): Path to pyproject.toml file. org_slug (Optional[str]): Organization slug. project_id (Optional[str]): Project ID. console (Console): Console instance. """ if not cls.is_installed(): logger.error("UV is not installed.") return None repository_url = ( PYPI_PROJECT_REPOSITORY_URL.format(org_slug, project_id) if project_id and org_slug else ( PYPI_ORGANIZATION_REPOSITORY_URL.format(org_slug) if org_slug else PYPI_PUBLIC_REPOSITORY_URL ) ) try: content = file.read_text() doc: Dict[str, Any] = tomlkit.loads(content) if "tool" not in doc: doc["tool"] = tomlkit.table() if "uv" not in doc["tool"]: # type: ignore doc["tool"]["uv"] = tomlkit.table() # type: ignore if "index" not in doc["tool"]["uv"]: # type: ignore doc["tool"]["uv"]["index"] = tomlkit.aot() # type: ignore index_container = doc["tool"]["uv"] # type: ignore cls.filter_out_safety_index(index_container) safety_index = { "name": "safety", "url": repository_url, # In UV default: # True = lowest priority # False = highest priority "default": False, } non_safety_indexes = ( doc.get("tool", {}).get("uv", {}).get("index", tomlkit.aot()) ) # Add safety index as first priority index_container["index"] = tomlkit.aot() # type: ignore index_container["index"].append(safety_index) # type: ignore index_container["index"].extend(non_safety_indexes) # type: ignore # Write back to file file.write_text(tomlkit.dumps(doc)) return file except (IOError, ValueError, Exception) as e: logger.error(f"Failed to configure {file} file: {e}") return None @classmethod def get_user_config_path(cls) -> Path: """ Returns the path to the user config file for UV. This logic is based on the uv documentation: https://docs.astral.sh/uv/configuration/files/ "uv will also discover user-level configuration at ~/.config/uv/uv.toml (or $XDG_CONFIG_HOME/uv/uv.toml) on macOS and Linux, or %APPDATA%\\uv\\uv.toml on Windows; ..." Returns: Path: The path to the user config file. """ if sys.platform == "win32": return Path(os.environ.get("APPDATA", ""), "uv", "uv.toml") else: xdg_config_home = os.environ.get("XDG_CONFIG_HOME") if xdg_config_home: return Path(xdg_config_home, "uv", "uv.toml") else: return Path(Path.home(), ".config", "uv", "uv.toml") @classmethod def filter_out_safety_index(cls, index_container: Any): if "index" not in index_container: return indexes = list(index_container["index"]) index_container["index"] = tomlkit.aot() for index in indexes: index_url = index.get("url", "") if ".safetycli.com" in index_url: continue index_container["index"].append(index) @classmethod def configure_system( cls, org_slug: Optional[str], console: Console = main_console ) -> Optional[Path]: """ Configures UV system to use to Safety index url. """ try: repository_url = ( PYPI_ORGANIZATION_REPOSITORY_URL.format(org_slug) if org_slug else PYPI_PUBLIC_REPOSITORY_URL ) user_config_path = cls.get_user_config_path() if not user_config_path.exists(): user_config_path.parent.mkdir(parents=True, exist_ok=True) content = "" else: backup_file(user_config_path) content = user_config_path.read_text() doc = tomlkit.loads(content) if "index" not in doc: doc["index"] = tomlkit.aot() cls.filter_out_safety_index(index_container=doc) safety_index = tomlkit.aot() safety_index.append( { "name": "safety", "url": repository_url, # In UV default: # True = lowest priority # False = highest priority "default": False, } ) non_safety_indexes = doc.get("index", tomlkit.aot()) # Add safety index as first priority doc["index"] = tomlkit.aot() doc.append("index", safety_index) doc.append("index", non_safety_indexes) user_config_path.write_text(tomlkit.dumps(doc)) return user_config_path except Exception as e: logger.error(f"Failed to configure UV system: {e}") return None @classmethod def reset_system(cls, console: Console = main_console): try: user_config_path = cls.get_user_config_path() if user_config_path.exists(): backup_file(user_config_path) content = user_config_path.read_text() doc = tomlkit.loads(content) cls.filter_out_safety_index(index_container=doc) user_config_path.write_text(tomlkit.dumps(doc)) except Exception as e: msg = "Failed to reset UV global settings" logger.error(f"{msg}: {e}") @classmethod def build_index_url(cls, ctx: typer.Context, index_url: Optional[str]) -> str: return build_index_url(ctx, index_url, "pypi")