Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,6 @@ pip-wheel-metadata/
# Vendoring Files that generate but we don't want
pipenv/vendor/Misc/NEWS.d/next/Library/2021-05-14-16-06-02.bpo-44095.v_pLwY.rst
pipenv/vendor/markupsafe/_speedups.c

# ignore all vim files
.sw[p,o]
1 change: 1 addition & 0 deletions news/6275.vendor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update pipdeptree to version 2.23.4
30 changes: 23 additions & 7 deletions pipenv/vendor/pipdeptree/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,36 @@
sys.path.append(os.path.dirname(os.path.dirname(pardir)))

from pipenv.vendor.pipdeptree._cli import get_options
from pipenv.vendor.pipdeptree._detect_env import detect_active_interpreter
from pipenv.vendor.pipdeptree._discovery import get_installed_distributions
from pipenv.vendor.pipdeptree._models import PackageDAG
from pipenv.vendor.pipdeptree._render import render
from pipenv.vendor.pipdeptree._validate import validate
from pipenv.vendor.pipdeptree._warning import WarningPrinter, WarningType, get_warning_printer


def main(args: Sequence[str] | None = None) -> None | int:
"""CLI - The main function called as entry point."""
options = get_options(args)

# Warnings are only enabled when using text output.
is_text_output = not any([options.json, options.json_tree, options.output_format])
if not is_text_output:
options.warn = WarningType.SILENCE
warning_printer = get_warning_printer()
warning_printer.warning_type = options.warn

if options.python == "auto":
resolved_path = detect_active_interpreter()
options.python = resolved_path
print(f"(resolved python: {resolved_path})", file=sys.stderr) # noqa: T201

pkgs = get_installed_distributions(
interpreter=options.python, local_only=options.local_only, user_only=options.user_only
)
tree = PackageDAG.from_pkgs(pkgs)
is_text_output = not any([options.json, options.json_tree, options.output_format])

return_code = validate(options, is_text_output, tree)
validate(tree)

# Reverse the tree (if applicable) before filtering, thus ensuring, that the filter will be applied on ReverseTree
if options.reverse:
Expand All @@ -42,14 +55,17 @@ def main(args: Sequence[str] | None = None) -> None | int:
try:
tree = tree.filter_nodes(show_only, exclude)
except ValueError as e:
if options.warn in {"suppress", "fail"}:
print(e, file=sys.stderr) # noqa: T201
return_code |= 1 if options.warn == "fail" else 0
return return_code
if warning_printer.should_warn():
warning_printer.print_single_line(str(e))
return _determine_return_code(warning_printer)

render(options, tree)

return return_code
return _determine_return_code(warning_printer)


def _determine_return_code(warning_printer: WarningPrinter) -> int:
return 1 if warning_printer.has_warned_with_failure() else 0


if __name__ == "__main__":
Expand Down
90 changes: 81 additions & 9 deletions pipenv/vendor/pipdeptree/_cli.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from __future__ import annotations

import enum
import sys
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace
from typing import TYPE_CHECKING, Sequence, cast
from argparse import Action, ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace
from typing import Any, Sequence, cast

from .version import __version__
from pipenv.vendor.pipdeptree._warning import WarningType

if TYPE_CHECKING:
from typing import Literal
from .version import __version__


class Options(Namespace):
Expand All @@ -16,7 +16,7 @@ class Options(Namespace):
all: bool
local_only: bool
user_only: bool
warn: Literal["silence", "suppress", "fail"]
warn: WarningType
reverse: bool
packages: str
exclude: str
Expand All @@ -40,19 +40,26 @@ def build_parser() -> ArgumentParser:
parser.add_argument(
"-w",
"--warn",
action="store",
dest="warn",
type=WarningType,
nargs="?",
default="suppress",
choices=("silence", "suppress", "fail"),
action=EnumAction,
help=(
"warning control: suppress will show warnings but return 0 whether or not they are present; silence will "
"not show warnings at all and always return 0; fail will show warnings and return 1 if any are present"
),
)

select = parser.add_argument_group(title="select", description="choose what to render")
select.add_argument("--python", default=sys.executable, help="Python interpreter to inspect")
select.add_argument(
"--python",
default=sys.executable,
help=(
'Python interpreter to inspect. With "auto", it attempts to detect your virtual environment and fails if'
" it can't."
),
)
select.add_argument(
"-p",
"--packages",
Expand Down Expand Up @@ -154,6 +161,71 @@ def get_options(args: Sequence[str] | None) -> Options:
return cast(Options, parsed_args)


class EnumAction(Action):
"""
Generic action that exists to convert a string into a Enum value that is then added into a `Namespace` object.

This custom action exists because argparse doesn't have support for enums.

References
----------
- https://github.com/python/cpython/issues/69247#issuecomment-1308082792
- https://docs.python.org/3/library/argparse.html#action-classes

"""

def __init__( # noqa: PLR0913, PLR0917
self,
option_strings: list[str],
dest: str,
nargs: str | None = None,
const: Any | None = None,
default: Any | None = None,
type: Any | None = None, # noqa: A002
choices: Any | None = None,
required: bool = False, # noqa: FBT001, FBT002
help: str | None = None, # noqa: A002
metavar: str | None = None,
) -> None:
if not type or not issubclass(type, enum.Enum):
msg = "type must be a subclass of Enum"
raise TypeError(msg)
if not isinstance(default, str):
msg = "default must be defined with a string value"
raise TypeError(msg)

choices = tuple(e.name.lower() for e in type)
if default not in choices:
msg = "default value should be among the enum choices"
raise ValueError(msg)

super().__init__(
option_strings=option_strings,
dest=dest,
nargs=nargs,
const=const,
default=default,
type=None, # We return None here so that we default to str.
choices=choices,
required=required,
help=help,
metavar=metavar,
)

self._enum = type

def __call__(
self,
parser: ArgumentParser, # noqa: ARG002
namespace: Namespace,
value: Any,
option_string: str | None = None, # noqa: ARG002
) -> None:
value = value or self.default
value = next(e for e in self._enum if e.name.lower() == value)
setattr(namespace, self.dest, value)


__all__ = [
"Options",
"get_options",
Expand Down
98 changes: 98 additions & 0 deletions pipenv/vendor/pipdeptree/_detect_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from __future__ import annotations

import os
import platform
import subprocess # noqa: S404
import sys
from pathlib import Path
from typing import Callable


def detect_active_interpreter() -> str:
"""
Attempt to detect a venv, virtualenv, poetry, or conda environment by looking for certain markers.

If it fails to find any, it will fail with a message.
"""
detection_funcs: list[Callable[[], Path | None]] = [
detect_venv_or_virtualenv_interpreter,
detect_conda_env_interpreter,
detect_poetry_env_interpreter,
]
for detect in detection_funcs:
path = detect()
if not path:
continue
if not path.exists():
break
return str(path)

print("Unable to detect virtual environment.", file=sys.stderr) # noqa: T201
raise SystemExit(1)


def detect_venv_or_virtualenv_interpreter() -> Path | None:
# Both virtualenv and venv set this environment variable.
env_var = os.environ.get("VIRTUAL_ENV")
if not env_var:
return None

path = Path(env_var)
path /= determine_bin_dir()

file_name = determine_interpreter_file_name()
return path / file_name if file_name else None


def determine_bin_dir() -> str:
return "Scripts" if os.name == "nt" else "bin"


def detect_conda_env_interpreter() -> Path | None:
# Env var mentioned in https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#saving-environment-variables.
env_var = os.environ.get("CONDA_PREFIX")
if not env_var:
return None

path = Path(env_var)

# On POSIX systems, conda adds the python executable to the /bin directory. On Windows, it resides in the parent
# directory of /bin (i.e. the root directory).
# See https://docs.anaconda.com/free/working-with-conda/configurations/python-path/#examples.
if os.name == "posix": # pragma: posix cover
path /= "bin"

file_name = determine_interpreter_file_name()

return path / file_name if file_name else None


def detect_poetry_env_interpreter() -> Path | None:
# poetry doesn't expose an environment variable like other implementations, so we instead use its CLI to snatch the
# active interpreter.
# See https://python-poetry.org/docs/managing-environments/#displaying-the-environment-information.
try:
result = subprocess.run( # noqa: S603
("poetry", "env", "info", "--executable"),
check=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
)
except Exception: # noqa: BLE001
return None

return Path(result.stdout.strip())


def determine_interpreter_file_name() -> str | None:
impl_name_to_file_name_dict = {"CPython": "python", "PyPy": "pypy"}
name = impl_name_to_file_name_dict.get(platform.python_implementation())
if not name:
return None
if os.name == "nt": # pragma: nt cover
return name + ".exe"
return name


__all__ = ["detect_active_interpreter"]
Loading
Loading