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,239 @@
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")