Repository URL to install this package:
Version:
1.10.1 ▾
|
from __future__ import annotations
from abc import ABC, abstractmethod
from importlib import import_module
from importlib.metadata import PackageNotFoundError, version
from inspect import ismodule
from typing import TYPE_CHECKING
from pip._vendor.pkg_resources import Requirement
if TYPE_CHECKING:
from pip._internal.metadata import BaseDistribution
from pip._vendor.pkg_resources import DistInfoDistribution
class Package(ABC):
"""Abstract class for wrappers around objects that pip returns."""
def __init__(self, obj: DistInfoDistribution) -> None:
self._obj: DistInfoDistribution = obj
@property
def key(self) -> str:
return self._obj.key # type: ignore[no-any-return]
@property
def project_name(self) -> str:
return self._obj.project_name # type: ignore[no-any-return]
@abstractmethod
def render_as_root(self, *, frozen: bool) -> str:
raise NotImplementedError
@abstractmethod
def render_as_branch(self, *, frozen: bool) -> str:
raise NotImplementedError
@abstractmethod
def as_dict(self) -> dict[str, str | None]:
raise NotImplementedError
@property
def version_spec(self) -> None | str:
return None
def render(
self,
parent: DistPackage | ReqPackage | None = None,
*,
frozen: bool = False,
) -> str:
render = self.render_as_branch if parent else self.render_as_root
return render(frozen=frozen)
@staticmethod
def as_frozen_repr(obj: DistInfoDistribution) -> str:
# The `pip._internal.metadata` modules were introduced in 21.1.1
# and the `pip._internal.operations.freeze.FrozenRequirement`
# class now expects dist to be a subclass of
# `pip._internal.metadata.BaseDistribution`, however the
# `pip._internal.utils.misc.get_installed_distributions` continues
# to return objects of type
# pip._vendor.pkg_resources.DistInfoDistribution.
#
# This is a hacky backward compatible (with older versions of pip) fix.
try:
from pip._internal.operations.freeze import FrozenRequirement
except ImportError:
from pip import FrozenRequirement # type: ignore[attr-defined, no-redef]
try:
from pip._internal import metadata
except ImportError:
our_dist: BaseDistribution = obj # type: ignore[assignment]
else:
our_dist = metadata.pkg_resources.Distribution(obj)
try:
fr = FrozenRequirement.from_dist(our_dist)
except TypeError:
fr = FrozenRequirement.from_dist(our_dist, []) # type: ignore[call-arg]
return str(fr).strip()
def requires(self) -> list[Requirement]:
return self._obj.requires() # type: ignore[no-untyped-call,no-any-return]
@property
def version(self) -> str:
return self._obj.version # type: ignore[no-any-return]
def __repr__(self) -> str:
return f'<{self.__class__.__name__}("{self.key}")>'
def __lt__(self, rhs: Package) -> bool:
return self.key < rhs.key
class DistPackage(Package):
"""
Wrapper class for pkg_resources.Distribution instances.
:param obj: pkg_resources.Distribution to wrap over
:param req: optional ReqPackage object to associate this DistPackage with. This is useful for displaying the tree
in reverse
"""
def __init__(self, obj: DistInfoDistribution, req: ReqPackage | None = None) -> None:
super().__init__(obj)
self.req = req
def render_as_root(self, *, frozen: bool) -> str:
if not frozen:
return f"{self.project_name}=={self.version}"
return self.as_frozen_repr(self._obj)
def render_as_branch(self, *, frozen: bool) -> str:
assert self.req is not None
if not frozen:
parent_ver_spec = self.req.version_spec
parent_str = self.req.project_name
if parent_ver_spec:
parent_str += parent_ver_spec
return f"{self.project_name}=={self.version} [requires: {parent_str}]"
return self.render_as_root(frozen=frozen)
def as_requirement(self) -> ReqPackage:
"""Return a ReqPackage representation of this DistPackage."""
return ReqPackage(self._obj.as_requirement(), dist=self) # type: ignore[no-untyped-call]
def as_parent_of(self, req: ReqPackage | None) -> DistPackage:
"""
Return a DistPackage instance associated to a requirement. This association is necessary for reversing the
PackageDAG.
If `req` is None, and the `req` attribute of the current instance is also None, then the same instance will be
returned.
:param ReqPackage req: the requirement to associate with
:returns: DistPackage instance
"""
if req is None and self.req is None:
return self
return self.__class__(self._obj, req)
def as_dict(self) -> dict[str, str | None]:
return {"key": self.key, "package_name": self.project_name, "installed_version": self.version}
class ReqPackage(Package):
"""
Wrapper class for Requirements instance.
:param obj: The `Requirements` instance to wrap over
:param dist: optional `pkg_resources.Distribution` instance for this requirement
"""
UNKNOWN_VERSION = "?"
def __init__(self, obj: Requirement, dist: DistPackage | None = None) -> None:
super().__init__(obj)
self.dist = dist
def render_as_root(self, *, frozen: bool) -> str:
if not frozen:
return f"{self.project_name}=={self.installed_version}"
if self.dist:
return self.as_frozen_repr(self.dist._obj) # noqa: SLF001
return self.project_name
def render_as_branch(self, *, frozen: bool) -> str:
if not frozen:
req_ver = self.version_spec if self.version_spec else "Any"
return f"{self.project_name} [required: {req_ver}, installed: {self.installed_version}]"
return self.render_as_root(frozen=frozen)
@property
def version_spec(self) -> str | None:
specs = sorted(self._obj.specs, reverse=True) # `reverse` makes '>' prior to '<'
return ",".join(["".join(sp) for sp in specs]) if specs else None
@property
def installed_version(self) -> str:
if not self.dist:
try:
return version(self.key)
except PackageNotFoundError:
pass
# Avoid AssertionError with setuptools, see https://github.com/tox-dev/pipdeptree/issues/162
if self.key in {"setuptools"}:
return self.UNKNOWN_VERSION
try:
m = import_module(self.key)
except ImportError:
return self.UNKNOWN_VERSION
else:
v = getattr(m, "__version__", self.UNKNOWN_VERSION)
if ismodule(v):
return getattr(v, "__version__", self.UNKNOWN_VERSION)
return v
return self.dist.version
@property
def is_missing(self) -> bool:
return self.installed_version == self.UNKNOWN_VERSION
def is_conflicting(self) -> bool:
"""If installed version conflicts with required version."""
# unknown installed version is also considered conflicting
if self.installed_version == self.UNKNOWN_VERSION:
return True
ver_spec = self.version_spec if self.version_spec else ""
req_version_str = f"{self.project_name}{ver_spec}"
req_obj = Requirement.parse(req_version_str) # type: ignore[no-untyped-call]
return self.installed_version not in req_obj
def as_dict(self) -> dict[str, str | None]:
return {
"key": self.key,
"package_name": self.project_name,
"installed_version": self.installed_version,
"required_version": self.version_spec,
}
__all__ = [
"DistPackage",
"ReqPackage",
]