updates
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,121 @@
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
|
||||
from .main import Uv
|
||||
import typer
|
||||
from safety.tool.auth import index_credentials
|
||||
from ..base import BaseCommand
|
||||
from ..environment_diff import EnvironmentDiffTracker, PipEnvironmentDiffTracker
|
||||
from ..mixins import InstallationAuditMixin
|
||||
from safety_schemas.models.events.types import ToolType
|
||||
from safety.models import ToolResult
|
||||
from .parser import UvParser
|
||||
|
||||
|
||||
class UvCommand(BaseCommand):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_command_name(self) -> List[str]:
|
||||
return ["uv"]
|
||||
|
||||
def get_diff_tracker(self) -> "EnvironmentDiffTracker":
|
||||
return PipEnvironmentDiffTracker()
|
||||
|
||||
def get_tool_type(self) -> ToolType:
|
||||
return ToolType.UV
|
||||
|
||||
def get_package_list_command(self) -> List[str]:
|
||||
# uv --active flag would ignore the uv project virtual environment,
|
||||
# by passing the --active flag then we can list the packages for the
|
||||
# correct environment.
|
||||
active = (
|
||||
["--active"]
|
||||
if self._intention and self._intention.options.get("active")
|
||||
else []
|
||||
)
|
||||
list_pkgs = Path(__file__).parent / "list_pkgs.py"
|
||||
|
||||
# --no-project flag is used to avoid uv to create the venv or lock file if it doesn't exist
|
||||
return [
|
||||
*self.get_command_name(),
|
||||
"run",
|
||||
*active,
|
||||
"--no-sync",
|
||||
"python",
|
||||
str(list_pkgs),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def from_args(cls, args: List[str], **kwargs):
|
||||
if uv_intention := UvParser().parse(args):
|
||||
kwargs["intention"] = uv_intention
|
||||
|
||||
if uv_intention.modifies_packages():
|
||||
return AuditableUvCommand(args, **kwargs)
|
||||
|
||||
return UvCommand(args, **kwargs)
|
||||
|
||||
|
||||
class AuditableUvCommand(UvCommand, InstallationAuditMixin):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__index_url = None
|
||||
|
||||
def before(self, ctx: typer.Context):
|
||||
super().before(ctx)
|
||||
args: List[Optional[str]] = self._args.copy() # type: ignore
|
||||
|
||||
if self._intention:
|
||||
if index_opt := self._intention.options.get(
|
||||
"index-url"
|
||||
) or self._intention.options.get("i"):
|
||||
index_value = index_opt["value"]
|
||||
|
||||
if index_value and index_value.startswith("https://pkgs.safetycli.com"):
|
||||
self.__index_url = index_value
|
||||
|
||||
arg_index = index_opt["arg_index"]
|
||||
value_index = index_opt["value_index"]
|
||||
|
||||
if (
|
||||
arg_index
|
||||
and value_index
|
||||
and arg_index < len(args)
|
||||
and value_index < len(args)
|
||||
):
|
||||
args[arg_index] = None
|
||||
args[value_index] = None
|
||||
|
||||
self._args = [arg for arg in args if arg is not None]
|
||||
|
||||
def after(self, ctx: typer.Context, result: ToolResult):
|
||||
super().after(ctx, result)
|
||||
self.handle_installation_audit(ctx, result)
|
||||
|
||||
def env(self, ctx: typer.Context) -> dict:
|
||||
env = super().env(ctx)
|
||||
|
||||
default_index_url = Uv.build_index_url(ctx, self.__index_url)
|
||||
# uv config precedence:
|
||||
# 1. Command line args -> We rewrite the args if the a default index is provided via command line args.
|
||||
# 2. Environment variables -> We set the default index to the Safety index
|
||||
# 3. Config files
|
||||
|
||||
env.update(
|
||||
{
|
||||
# Default index URL
|
||||
# When the package manager is wrapped, we provide a default index so the search always falls back to the Safety index
|
||||
# UV_INDEX_URL is deprecated by UV, we comment it out to avoid a anoying warning, UV_DEFAULT_INDEX is available since uv 0.4.23
|
||||
# So we decided to support only UV_DEFAULT_INDEX, as we don't inject the uv version in the command pipeline yet.
|
||||
#
|
||||
# "UV_INDEX_URL": default_index_url,
|
||||
#
|
||||
"UV_DEFAULT_INDEX": default_index_url,
|
||||
# Credentials for the named index in case of being set in the pyproject.toml
|
||||
"UV_INDEX_SAFETY_USERNAME": "user",
|
||||
"UV_INDEX_SAFETY_PASSWORD": index_credentials(ctx),
|
||||
}
|
||||
)
|
||||
|
||||
return env
|
||||
@@ -0,0 +1,40 @@
|
||||
import importlib.metadata as md
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
def get_package_location(dist):
|
||||
"""
|
||||
Get the installation location of a package distribution.
|
||||
"""
|
||||
try:
|
||||
if hasattr(dist, "locate_file") and callable(dist.locate_file):
|
||||
root = dist.locate_file("")
|
||||
if root:
|
||||
return os.path.abspath(str(root))
|
||||
except (AttributeError, OSError, TypeError):
|
||||
pass
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""
|
||||
List all installed packages with their versions and locations.
|
||||
"""
|
||||
packages = []
|
||||
for dist in md.distributions():
|
||||
packages.append(
|
||||
{
|
||||
"name": dist.metadata.get("Name", ""),
|
||||
"version": dist.version,
|
||||
"location": get_package_location(dist),
|
||||
}
|
||||
)
|
||||
|
||||
print(json.dumps(packages, separators=(",", ":")))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
239
Backend/venv/lib/python3.12/site-packages/safety/tool/uv/main.py
Normal file
239
Backend/venv/lib/python3.12/site-packages/safety/tool/uv/main.py
Normal 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")
|
||||
@@ -0,0 +1,160 @@
|
||||
from typing import Dict, Union, Set
|
||||
|
||||
from ..base import ToolCommandLineParser
|
||||
from ..intents import ToolIntentionType
|
||||
|
||||
|
||||
UV_CACHE_FLAGS = {
|
||||
"no-cache",
|
||||
"n",
|
||||
"refresh",
|
||||
}
|
||||
|
||||
UV_PYTHON_FLAGS = {
|
||||
"managed-python",
|
||||
"no-managed-python",
|
||||
"no-python-downloads",
|
||||
}
|
||||
|
||||
|
||||
UV_INDEX_FLAGS = {
|
||||
"no-index",
|
||||
}
|
||||
|
||||
UV_RESOLVER_FLAGS = {
|
||||
"upgrade",
|
||||
"U",
|
||||
"no-sources",
|
||||
}
|
||||
|
||||
UV_INSTALLER_FLAGS = {
|
||||
"reinstall",
|
||||
"compile-bytecode",
|
||||
}
|
||||
|
||||
UV_BUILD_FLAGS = {
|
||||
"no-build-isolation",
|
||||
"no-build",
|
||||
"no-binary",
|
||||
}
|
||||
|
||||
UV_GLOBAL_FLAGS = {
|
||||
"quiet",
|
||||
"q",
|
||||
"verbose",
|
||||
"v",
|
||||
"native-tls",
|
||||
"offline",
|
||||
"no-progress",
|
||||
"no-config",
|
||||
"help",
|
||||
"h",
|
||||
"version",
|
||||
"V",
|
||||
}
|
||||
|
||||
UV_PIP_INSTALL_FLAGS = {
|
||||
"all-extras",
|
||||
"no-deps",
|
||||
"require-hashes",
|
||||
"no-verify-hashes",
|
||||
"system",
|
||||
"break-system-packages",
|
||||
"no-break-system-packages",
|
||||
"no-build",
|
||||
"exact",
|
||||
"strict",
|
||||
"dry-run",
|
||||
"user",
|
||||
}
|
||||
|
||||
UV_PIP_UNINSTALL_FLAGS = {
|
||||
"system",
|
||||
"break-system-packages",
|
||||
"no-break-system-packages",
|
||||
"dry-run",
|
||||
}
|
||||
|
||||
|
||||
UV_KNOWN_FLAGS: Dict[str, Set[str]] = {
|
||||
"global": UV_GLOBAL_FLAGS
|
||||
| UV_CACHE_FLAGS
|
||||
| UV_PYTHON_FLAGS
|
||||
| UV_INDEX_FLAGS
|
||||
| UV_RESOLVER_FLAGS
|
||||
| UV_INSTALLER_FLAGS
|
||||
| UV_BUILD_FLAGS,
|
||||
# 2-level commands
|
||||
"add": {
|
||||
# From `uv add --help`
|
||||
"dev",
|
||||
"editable",
|
||||
"raw",
|
||||
"no-sync",
|
||||
"locked",
|
||||
"frozen",
|
||||
"active",
|
||||
"workspace",
|
||||
"no-workspace",
|
||||
"no-install-project",
|
||||
"no-install-workspace",
|
||||
},
|
||||
"remove": {
|
||||
"dev",
|
||||
"no-sync",
|
||||
"active",
|
||||
"locked",
|
||||
"frozen",
|
||||
},
|
||||
"sync": {
|
||||
"all-extras",
|
||||
"no-dev",
|
||||
"only-dev",
|
||||
"no-default-groups",
|
||||
"all-groups",
|
||||
"no-editable",
|
||||
"inexact",
|
||||
"active",
|
||||
"no-install-project",
|
||||
"no-install-workspace",
|
||||
"locked",
|
||||
"frozen",
|
||||
"dry-run",
|
||||
"all-packages",
|
||||
"check",
|
||||
},
|
||||
# 3-level pip commands
|
||||
"pip.install": UV_PIP_INSTALL_FLAGS,
|
||||
"pip.uninstall": UV_PIP_UNINSTALL_FLAGS,
|
||||
}
|
||||
|
||||
|
||||
class UvParser(ToolCommandLineParser):
|
||||
def get_tool_name(self) -> str:
|
||||
return "uv"
|
||||
|
||||
def get_command_hierarchy(self) -> Dict[str, Union[ToolIntentionType, Dict]]:
|
||||
"""
|
||||
Context for command hierarchy parsing
|
||||
"""
|
||||
return {
|
||||
# 2-level commands
|
||||
"add": ToolIntentionType.ADD_PACKAGE,
|
||||
"remove": ToolIntentionType.REMOVE_PACKAGE,
|
||||
"build": ToolIntentionType.BUILD_PROJECT,
|
||||
"sync": ToolIntentionType.SYNC_PACKAGES,
|
||||
# 3-level commands
|
||||
"pip": {
|
||||
"install": ToolIntentionType.ADD_PACKAGE,
|
||||
"uninstall": ToolIntentionType.REMOVE_PACKAGE,
|
||||
"download": ToolIntentionType.DOWNLOAD_PACKAGE,
|
||||
"list": ToolIntentionType.LIST_PACKAGES,
|
||||
},
|
||||
}
|
||||
|
||||
def get_known_flags(self) -> Dict[str, Set[str]]:
|
||||
"""
|
||||
Define flags that DON'T take values for uv.
|
||||
These were derived from `uv --help` and subcommand helps.
|
||||
"""
|
||||
return UV_KNOWN_FLAGS
|
||||
Reference in New Issue
Block a user