updates
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .main import Pip
|
||||
|
||||
__all__ = ["Pip"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,116 @@
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
import logging
|
||||
import typer
|
||||
|
||||
from safety.models import ToolResult
|
||||
from .parser import PipParser
|
||||
|
||||
from ..base import BaseCommand
|
||||
from safety_schemas.models.events.types import ToolType
|
||||
from ..environment_diff import EnvironmentDiffTracker, PipEnvironmentDiffTracker
|
||||
from ..mixins import InstallationAuditMixin
|
||||
from .main import Pip
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..environment_diff import EnvironmentDiffTracker
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PipCommand(BaseCommand):
|
||||
"""
|
||||
Main class for hooks into pip commands.
|
||||
"""
|
||||
|
||||
def get_tool_type(self) -> ToolType:
|
||||
return ToolType.PIP
|
||||
|
||||
def get_command_name(self) -> List[str]:
|
||||
"""
|
||||
This uses command alias if available, with this we support
|
||||
pip3.13, pip3.12, etc.
|
||||
"""
|
||||
|
||||
cmd_name = ["pip"]
|
||||
|
||||
if self._command_alias_used:
|
||||
cmd_name = [self._command_alias_used]
|
||||
|
||||
return cmd_name
|
||||
|
||||
def get_diff_tracker(self) -> "EnvironmentDiffTracker":
|
||||
return PipEnvironmentDiffTracker()
|
||||
|
||||
@classmethod
|
||||
def from_args(cls, args: List[str], **kwargs):
|
||||
parser = PipParser()
|
||||
|
||||
if intention := parser.parse(args):
|
||||
kwargs["intention"] = intention
|
||||
|
||||
if intention.modifies_packages():
|
||||
return AuditablePipCommand(args, **kwargs)
|
||||
|
||||
if intention.queries_packages():
|
||||
return SearchCommand(args, **kwargs)
|
||||
|
||||
return PipCommand(args, **kwargs)
|
||||
|
||||
|
||||
class PipIndexEnvMixin:
|
||||
"""
|
||||
Mixin to inject Safety's default index URL into pip's environment.
|
||||
Expects implementers to define `self._index_url` (Optional[str]).
|
||||
"""
|
||||
|
||||
def env(self, ctx: typer.Context) -> dict:
|
||||
env = super().env(ctx) # pyright: ignore[reportAttributeAccessIssue]
|
||||
default_index_url = Pip.build_index_url(ctx, getattr(self, "_index_url", None))
|
||||
env["PIP_INDEX_URL"] = default_index_url
|
||||
env["PIP_PYPI_URL"] = default_index_url
|
||||
return env
|
||||
|
||||
|
||||
class SearchCommand(PipIndexEnvMixin, PipCommand):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self._index_url = None
|
||||
|
||||
|
||||
class AuditablePipCommand(PipIndexEnvMixin, PipCommand, 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)
|
||||
@@ -0,0 +1,150 @@
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
from safety.tool.constants import (
|
||||
PYPI_PUBLIC_REPOSITORY_URL,
|
||||
PYPI_ORGANIZATION_REPOSITORY_URL,
|
||||
PYPI_PROJECT_REPOSITORY_URL,
|
||||
)
|
||||
from safety.tool.resolver import get_unwrapped_command
|
||||
from safety.utils.pyapp_utils import get_path, get_env
|
||||
|
||||
from safety.console import main_console
|
||||
from safety.tool.auth import build_index_url
|
||||
from ...encoding import detect_encoding
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Pip:
|
||||
@classmethod
|
||||
def is_installed(cls) -> bool:
|
||||
"""
|
||||
Checks if the PIP program is installed
|
||||
|
||||
Returns:
|
||||
True if PIP is installed on system, or false otherwise
|
||||
"""
|
||||
return shutil.which("pip", path=get_path()) is not None
|
||||
|
||||
@classmethod
|
||||
def configure_requirements(
|
||||
cls,
|
||||
file: Path,
|
||||
org_slug: Optional[str],
|
||||
project_id: Optional[str],
|
||||
console: Console = main_console,
|
||||
) -> Optional[Path]:
|
||||
"""
|
||||
Configures Safety index url for specified requirements file.
|
||||
|
||||
Args:
|
||||
file (Path): Path to requirements.txt file.
|
||||
org_slug (str): Organization slug.
|
||||
project_id (str): Project identifier.
|
||||
console (Console): Console instance.
|
||||
"""
|
||||
|
||||
with open(file, "r+", encoding=detect_encoding(file)) as f:
|
||||
content = f.read()
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
index_config = f"-i {repository_url}\n"
|
||||
if content.find(index_config) == -1:
|
||||
f.seek(0)
|
||||
f.write(index_config + content)
|
||||
|
||||
logger.info(f"Configured {file} file")
|
||||
return file
|
||||
else:
|
||||
logger.info(f"{file} is already configured. Skipping.")
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def configure_system(
|
||||
cls, org_slug: Optional[str], console: Console = main_console
|
||||
) -> Optional[Path]:
|
||||
"""
|
||||
Configures PIP 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
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
get_unwrapped_command(name="pip"),
|
||||
"config",
|
||||
"--user",
|
||||
"set",
|
||||
"global.index-url",
|
||||
repository_url,
|
||||
],
|
||||
capture_output=True,
|
||||
env=get_env(),
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(
|
||||
f"Failed to configure PIP global settings: {result.stderr.decode('utf-8')}"
|
||||
)
|
||||
return None
|
||||
|
||||
output = result.stdout.decode("utf-8")
|
||||
match = re.search(r"Writing to (.+)", output)
|
||||
|
||||
if match:
|
||||
config_file_path = match.group(1).strip()
|
||||
return Path(config_file_path)
|
||||
|
||||
logger.error("Failed to match the config file path written by pip.")
|
||||
return Path()
|
||||
except Exception:
|
||||
logger.exception("Failed to configure PIP global settings.")
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def reset_system(cls, console: Console = main_console):
|
||||
# TODO: Move this logic and implement it in a more robust way
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
get_unwrapped_command(name="pip"),
|
||||
"config",
|
||||
"--user",
|
||||
"unset",
|
||||
"global.index-url",
|
||||
],
|
||||
capture_output=True,
|
||||
env=get_env(),
|
||||
)
|
||||
except Exception:
|
||||
console.print("Failed to reset PIP global settings.")
|
||||
|
||||
@classmethod
|
||||
def default_index_url(cls) -> str:
|
||||
return "https://pypi.org/simple/"
|
||||
|
||||
@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,105 @@
|
||||
from typing import Dict
|
||||
|
||||
from ..base import ToolCommandLineParser
|
||||
from ..intents import ToolIntentionType
|
||||
|
||||
from typing import Union, Set
|
||||
|
||||
|
||||
class PipParser(ToolCommandLineParser):
|
||||
def get_tool_name(self) -> str:
|
||||
return "pip"
|
||||
|
||||
def get_command_hierarchy(self) -> Dict[str, Union[ToolIntentionType, Dict]]:
|
||||
"""
|
||||
Context for command hierarchy parsing
|
||||
"""
|
||||
return {
|
||||
"install": ToolIntentionType.ADD_PACKAGE,
|
||||
"uninstall": ToolIntentionType.REMOVE_PACKAGE,
|
||||
"download": ToolIntentionType.DOWNLOAD_PACKAGE,
|
||||
"wheel": ToolIntentionType.DOWNLOAD_PACKAGE,
|
||||
"query": ToolIntentionType.SEARCH_PACKAGES,
|
||||
"index": {
|
||||
"versions": ToolIntentionType.SEARCH_PACKAGES,
|
||||
},
|
||||
}
|
||||
|
||||
def get_known_flags(self) -> Dict[str, Set[str]]:
|
||||
"""
|
||||
Define flags that DON'T take values to avoid consuming packages
|
||||
"""
|
||||
return {
|
||||
# Global flags (available for all commands)
|
||||
"global": {
|
||||
"help",
|
||||
"h",
|
||||
"debug",
|
||||
"isolated",
|
||||
"require-virtualenv",
|
||||
"verbose",
|
||||
"v",
|
||||
"version",
|
||||
"V",
|
||||
"quiet",
|
||||
"q",
|
||||
"no-input",
|
||||
"no-cache-dir",
|
||||
"disable-pip-version-check",
|
||||
"no-color",
|
||||
# Index specific
|
||||
"no-index",
|
||||
},
|
||||
# install-specific flags
|
||||
"install": {
|
||||
"no-deps",
|
||||
"pre",
|
||||
"dry-run",
|
||||
"user",
|
||||
"upgrade",
|
||||
"U",
|
||||
"force-reinstall",
|
||||
"ignore-installed",
|
||||
"I",
|
||||
"ignore-requires-python",
|
||||
"no-build-isolation",
|
||||
"use-pep517",
|
||||
"no-use-pep517",
|
||||
"check-build-dependencies",
|
||||
"break-system-packages",
|
||||
"compile",
|
||||
"no-compile",
|
||||
"no-warn-script-location",
|
||||
"no-warn-conflicts",
|
||||
"prefer-binary",
|
||||
"require-hashes",
|
||||
"no-clean",
|
||||
},
|
||||
# uninstall-specific flags
|
||||
"uninstall": {
|
||||
"yes",
|
||||
"y",
|
||||
"break-system-packages",
|
||||
},
|
||||
# download-specific flags
|
||||
"download": {
|
||||
"no-deps",
|
||||
"no-binary",
|
||||
"only-binary",
|
||||
"prefer-binary",
|
||||
"pre",
|
||||
"require-hashes",
|
||||
"no-build-isolation",
|
||||
"use-pep517",
|
||||
"no-use-pep517",
|
||||
"check-build-dependencies",
|
||||
"ignore-requires-python",
|
||||
"no-clean",
|
||||
},
|
||||
"index.versions": {
|
||||
"ignore-requires-python",
|
||||
"pre",
|
||||
"json",
|
||||
"no-index",
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user