pipdeptree-2.2.0/ 0000755 0001750 0001750 00000000000 14131341150 013173 5 ustar nilesh nilesh pipdeptree-2.2.0/README.rst 0000644 0001750 0001750 00000036250 14131341150 014670 0 ustar nilesh nilesh pipdeptree
==========
.. image:: https://github.com/naiquevin/pipdeptree/workflows/check/badge.svg
:target: https://github.com/naiquevin/pipdeptree/actions
``pipdeptree`` is a command line utility for displaying the installed
python packages in form of a dependency tree. It works for packages
installed globally on a machine as well as in a virtualenv. Since
``pip freeze`` shows all dependencies as a flat list, finding out
which are the top level packages and which packages do they depend on
requires some effort. It's also tedious to resolve conflicting
dependencies that could have been installed because older version of
``pip`` didn't have true dependency resolution [1]_. ``pipdeptree``
can help here by identifying conflicting dependencies installed in the
environment.
To some extent, ``pipdeptree`` is inspired by the ``lein deps :tree``
command of `Leiningen `_.
Installation
------------
.. code-block:: bash
$ pip install pipdeptree
pipdeptree has been tested with Python versions ``2.7``, ``3.5``,
``3.6``, ``3.7``, ``3.8``, ``3.9`` as well as ``pypy2`` and ``pypy3``.
Python ``2.6`` is way past it's end of life but if you ever find
yourself stuck on a legacy environment, version ``0.9.0`` *might*
work.
Running in virtualenvs
----------------------
`New in ver. 2.0.0`
If you want to run pipdeptree in the context of a particular
virtualenv, you can specify the ``--python`` option. Note that this
capability has been recently added in version ``2.0.0``.
Alternately, you may also install pipdeptree inside the virtualenv and
then run it from there.
Usage and examples
------------------
To give you a brief idea, here is the output of ``pipdeptree``
compared with ``pip freeze``:
.. code-block:: bash
$ pip freeze
Flask==0.10.1
itsdangerous==0.24
Jinja2==2.11.2
-e git+git@github.com:naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy
MarkupSafe==0.22
pipdeptree @ file:///private/tmp/pipdeptree-2.0.0b1-py3-none-any.whl
Werkzeug==0.11.2
And now see what ``pipdeptree`` outputs,
.. code-block:: bash
$ pipdeptree
Warning!!! Possibly conflicting dependencies found:
* Jinja2==2.11.2
- MarkupSafe [required: >=0.23, installed: 0.22]
------------------------------------------------------------------------
Flask==0.10.1
- itsdangerous [required: >=0.21, installed: 0.24]
- Jinja2 [required: >=2.4, installed: 2.11.2]
- MarkupSafe [required: >=0.23, installed: 0.22]
- Werkzeug [required: >=0.7, installed: 0.11.2]
Lookupy==0.1
pipdeptree==2.0.0b1
- pip [required: >=6.0.0, installed: 20.1.1]
setuptools==47.1.1
wheel==0.34.2
Is it possible to find out why a particular package is installed?
-----------------------------------------------------------------
`New in ver. 0.5.0`
Yes, there's a ``--reverse`` (or simply ``-r``) flag for this. To find
out which packages depend on a particular package(s), it can be
combined with ``--packages`` option as follows:
.. code-block:: bash
$ pipdeptree --reverse --packages itsdangerous,MarkupSafe
Warning!!! Possibly conflicting dependencies found:
* Jinja2==2.11.2
- MarkupSafe [required: >=0.23, installed: 0.22]
------------------------------------------------------------------------
itsdangerous==0.24
- Flask==0.10.1 [requires: itsdangerous>=0.21]
MarkupSafe==0.22
- Jinja2==2.11.2 [requires: MarkupSafe>=0.23]
- Flask==0.10.1 [requires: Jinja2>=2.4]
What's with the warning about conflicting dependencies?
-------------------------------------------------------
As seen in the above output, ``pipdeptree`` by default warns about
possible conflicting dependencies. Any package that's specified as a
dependency of multiple packages with different versions is considered
as a conflicting dependency. Conflicting dependencies are possible if
older version of pip<=20.2 (`without the new resolver
`_ [1]_) was ever used to
install dependencies at some point. The warning is printed to stderr
instead of stdout and it can be completely silenced by specifying the
``-w silence`` or ``--warn silence`` option. On the other hand, it can
be made mode strict with ``--warn fail``, in which case the command
will not only print the warnings to stderr but also exit with a
non-zero status code. This is useful if you want to fit this tool into
your CI pipeline.
**Note**: The ``--warn`` option is added in version ``0.6.0``. If you
are using an older version, use ``--nowarn`` flag to silence the
warnings.
Warnings about circular dependencies
------------------------------------
In case any of the packages have circular dependencies (eg. package A
depends on package B and package B depends on package A), then
``pipdeptree`` will print warnings about that as well.
.. code-block:: bash
$ pipdeptree --exclude pip,pipdeptree,setuptools,wheel
Warning!!! Cyclic dependencies found:
- CircularDependencyA => CircularDependencyB => CircularDependencyA
- CircularDependencyB => CircularDependencyA => CircularDependencyB
------------------------------------------------------------------------
wsgiref==0.1.2
argparse==1.2.1
Similar to the warnings about conflicting dependencies, these too are
printed to stderr and can be controlled using the ``--warn`` option.
In the above example, you can also see ``--exclude`` option which is
the opposite of ``--packages`` ie. these packages will be excluded
from the output.
Using pipdeptree to write requirements.txt file
-----------------------------------------------
If you wish to track only top level packages in your
``requirements.txt`` file, it's possible by grep-ing [2]_. only the
top-level lines from the output,
.. code-block:: bash
$ pipdeptree --warn silence | grep -E '^\w+'
Flask==0.10.1
gnureadline==8.0.0
Lookupy==0.1
pipdeptree==2.0.0b1
setuptools==47.1.1
wheel==0.34.2
There is a problem here though - The output doesn't mention anything
about ``Lookupy`` being installed as an *editable* package (refer to
the output of ``pip freeze`` above) and information about its source
is lost. To fix this, ``pipdeptree`` must be run with a ``-f`` or
``--freeze`` flag.
.. code-block:: bash
$ pipdeptree -f --warn silence | grep -E '^[a-zA-Z0-9\-]+'
Flask==0.10.1
gnureadline==8.0.0
-e git+git@github.com:naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy
pipdeptree @ file:///private/tmp/pipdeptree-2.0.0b1-py3-none-any.whl
setuptools==47.1.1
wheel==0.34.2
$ pipdeptree -f --warn silence | grep -E '^[a-zA-Z0-9\-]+' > requirements.txt
The freeze flag will not prefix child dependencies with hyphens, so
you could dump the entire output of ``pipdeptree -f`` to the
requirements.txt file thus making it human-friendly (due to
indentations) as well as pip-friendly.
.. code-block:: bash
$ pipdeptree -f | tee locked-requirements.txt
Flask==0.10.1
itsdangerous==0.24
Jinja2==2.11.2
MarkupSafe==0.23
Werkzeug==0.11.2
gnureadline==8.0.0
-e git+git@github.com:naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy
pipdeptree @ file:///private/tmp/pipdeptree-2.0.0b1-py3-none-any.whl
pip==20.1.1
setuptools==47.1.1
wheel==0.34.2
On confirming that there are no conflicting dependencies, you can even
treat this as a "lock file" where all packages, including the
transient dependencies will be pinned to their currently installed
versions. Note that the ``locked-requirements.txt`` file could end up
with duplicate entries. Although ``pip install`` wouldn't complain
about that, you can avoid duplicate lines (at the cost of losing
indentation) as follows,
.. code-block:: bash
$ pipdeptree -f | sed 's/ //g' | sort -u > locked-requirements.txt
Using pipdeptree with external tools
------------------------------------
`New in ver. 0.5.0`
It's also possible to have ``pipdeptree`` output json representation
of the dependency tree so that it may be used as input to other
external tools.
.. code-block:: bash
$ pipdeptree --json
Note that ``--json`` will output a flat list of all packages with
their immediate dependencies. This is not very useful in itself. To
obtain nested json, use ``--json-tree``
`New in ver. 0.11.0`
.. code-block:: bash
$ pipdeptree --json-tree
Visualizing the dependency graph
--------------------------------
.. image:: https://raw.githubusercontent.com/naiquevin/pipdeptree/master/docs/twine-pdt.png
The dependency graph can also be visualized using `GraphViz
`_:
.. code-block:: bash
$ pipdeptree --graph-output dot > dependencies.dot
$ pipdeptree --graph-output pdf > dependencies.pdf
$ pipdeptree --graph-output png > dependencies.png
$ pipdeptree --graph-output svg > dependencies.svg
Note that ``graphviz`` is an optional dependency ie. required only if
you want to use ``--graph-output``.
Since version ``2.0.0b1``, ``--package`` and ``--reverse`` flags are
supported for all output formats ie. text, json, json-tree and graph.
In earlier versions, ``--json``, ``--json-tree`` and
``--graph-output`` options override ``--package`` and ``--reverse``.
Usage
-----
.. code-block:: bash
usage: pipdeptree.py [-h] [-v] [-f] [--python PYTHON] [-a] [-l] [-u]
[-w [{silence,suppress,fail}]] [-r] [-p PACKAGES]
[-e PACKAGES] [-j] [--json-tree]
[--graph-output OUTPUT_FORMAT]
Dependency tree of the installed python packages
optional arguments:
-h, --help show this help message and exit
-v, --version show program's version number and exit
-f, --freeze Print names so as to write freeze files
--python PYTHON Python to use to look for packages in it (default:
where installed)
-a, --all list all deps at top level
-l, --local-only If in a virtualenv that has global access do not show
globally installed packages
-u, --user-only Only show installations in the user site dir
-w [{silence,suppress,fail}], --warn [{silence,suppress,fail}]
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. The default is "suppress".
-r, --reverse Shows the dependency tree in the reverse fashion ie.
the sub-dependencies are listed with the list of
packages that need them under them.
-p PACKAGES, --packages PACKAGES
Comma separated list of select packages to show in the
output. If set, --all will be ignored.
-e PACKAGES, --exclude PACKAGES
Comma separated list of select packages to exclude
from the output. If set, --all will be ignored.
-j, --json Display dependency tree as json. This will yield "raw"
output that may be used by external tools. This option
overrides all other options.
--json-tree Display dependency tree as json which is nested the
same way as the plain text output printed by default.
This option overrides all other options (except
--json).
--graph-output OUTPUT_FORMAT
Print a dependency graph in the specified output
format. Available are all formats supported by
GraphViz, e.g.: dot, jpeg, pdf, png, svg
Known issues
------------
1. ``pipdeptree`` relies on the internal API of ``pip``. I fully
understand that it's a bad idea but it mostly works! On rare
occasions, it breaks when a new version of ``pip`` is out with
backward incompatible changes in internal API. So beware if you are
using this tool in environments in which ``pip`` version is
unpinned, specially automation or CD/CI pipelines.
Limitations & Alternatives
--------------------------
``pipdeptree`` merely looks at the installed packages in the current
environment using pip, constructs the tree, then outputs it in the
specified format. If you want to generate the dependency tree without
installing the packages, then you need a dependency resolver. You
might want to check alternatives such as `pipgrip
`_ or `poetry
`_.
Runing Tests (for contributors)
-------------------------------
There are 2 test suites in this repo:
1. Unit tests that use mock objects. These are configured to run on
every push to the repo and on every PR thanks to Github Actions.
2. End-to-end tests that are run against actual packages installed in
virtualenvs
Unit tests can be run against all version of python using `tox
`_ as follows:
.. code-block:: bash
$ make test-tox-all
This assumes that you have python versions specified in the
``tox.ini`` file.
If you don't want to install all the versions of python but want to
run tests quickly against ``Python3.6`` only:
.. code-block:: bash
$ make test
Unit tests are written using ``pytest`` and you can also run the tests
with code coverage as follows,
.. code-block:: bash
$ make test-cov
On the other hand, end-to-end tests actually create virtualenvs,
install packages and then run tests against them. These tests are more
reliable in the sense that they also test ``pipdeptree`` with the
latest version of ``pip`` and ``setuptools``.
The downside is that when new versions of ``pip`` or ``setuptools``
are released, these need to be updated. At present the process is
manual but I have plans to setup nightly builds for these for faster
feedback.
The end-to-end tests can be run as follows,
.. code-block:: bash
$ make test-e2e # starts with a clean virtualenvs
$ # or
$ make test-e2e-quick # reuses existing virtualenvs
By default the e2e tests uses python executable ``python3.6``. To use
an alternate version set the environment var ``E2E_PYTHON_EXE``.
.. code-block:: bash
$ E2E_PYTHON_EXE=python2.7 make test-e2e
Release checklist
-----------------
#. Make sure that tests pass on Github Actions.
#. Create a commit with following changes and push it to github
#. Update the `__version__` in the `pipdeptree.py` file.
#. Add Changelog in `CHANGES.md` file.
#. Also update `README.md` if required.
#. Create an annotated tag on the above commit and push the tag to
github
#. Upload new version to PyPI.
License
-------
MIT (See `LICENSE <./LICENSE>`_)
Footnotes
---------
.. [1] pip version 20.3 has been released in Nov 2020 with the
dependency resolver
_
.. [2] If you are on windows (powershell) you can run
``pipdeptree --warn silence | Select-String -Pattern '^\w+'``
instead of grep
pipdeptree-2.2.0/pipdeptree.py 0000644 0001750 0001750 00000074172 14131341150 015721 0 ustar nilesh nilesh from __future__ import print_function
import os
import inspect
import sys
import subprocess
from itertools import chain
from collections import defaultdict, deque
import argparse
import json
from importlib import import_module
import tempfile
try:
from collections import OrderedDict
except ImportError:
from ordereddict import OrderedDict
try:
from collections.abc import Mapping
except ImportError:
from collections import Mapping
from pip._vendor import pkg_resources
# inline:
# from graphviz import backend, Digraph
__version__ = '2.2.0'
flatten = chain.from_iterable
def sorted_tree(tree):
"""Sorts the dict representation of the tree
The root packages as well as the intermediate packages are sorted
in the alphabetical order of the package names.
:param dict tree: the pkg dependency tree obtained by calling
`construct_tree` function
:returns: sorted tree
:rtype: collections.OrderedDict
"""
return OrderedDict([(k, sorted(v)) for k, v in sorted(tree.items())])
def guess_version(pkg_key, default='?'):
"""Guess the version of a pkg when pip doesn't provide it
:param str pkg_key: key of the package
:param str default: default version to return if unable to find
:returns: version
:rtype: string
"""
try:
m = import_module(pkg_key)
except ImportError:
return default
else:
v = getattr(m, '__version__', default)
if inspect.ismodule(v):
return getattr(v, '__version__', default)
else:
return v
def frozen_req_from_dist(dist):
# 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 import metadata
except ImportError:
pass
else:
dist = metadata.pkg_resources.Distribution(dist)
try:
return FrozenRequirement.from_dist(dist)
except TypeError:
return FrozenRequirement.from_dist(dist, [])
class Package(object):
"""Abstract class for wrappers around objects that pip returns.
This class needs to be subclassed with implementations for
`render_as_root` and `render_as_branch` methods.
"""
def __init__(self, obj):
self._obj = obj
self.project_name = obj.project_name
self.key = obj.key
def render_as_root(self, frozen):
return NotImplementedError
def render_as_branch(self, frozen):
return NotImplementedError
def render(self, parent=None, frozen=False):
if not parent:
return self.render_as_root(frozen)
else:
return self.render_as_branch(frozen)
@staticmethod
def frozen_repr(obj):
return str(obj.as_requirement())
def __getattr__(self, key):
return getattr(self._obj, key)
def __repr__(self):
return '<{0}("{1}")>'.format(self.__class__.__name__, self.key)
def __lt__(self, rhs):
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, req=None):
super(DistPackage, self).__init__(obj)
self.version_spec = None
self.req = req
def render_as_root(self, frozen):
if not frozen:
return '{0}=={1}'.format(self.project_name, self.version)
else:
return self.__class__.frozen_repr(self._obj)
def render_as_branch(self, frozen):
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 (
'{0}=={1} [requires: {2}]'
).format(self.project_name, self.version, parent_str)
else:
return self.render_as_root(frozen)
def as_requirement(self):
"""Return a ReqPackage representation of this DistPackage"""
return ReqPackage(self._obj.as_requirement(), dist=self)
def as_parent_of(self, req):
"""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):
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, dist=None):
super(ReqPackage, self).__init__(obj)
self.dist = dist
@property
def version_spec(self):
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):
if not self.dist:
return guess_version(self.key, self.UNKNOWN_VERSION)
return self.dist.version
@property
def is_missing(self):
return self.installed_version == self.UNKNOWN_VERSION
def is_conflicting(self):
"""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 = '{0}{1}'.format(self.project_name, ver_spec)
req_obj = pkg_resources.Requirement.parse(req_version_str)
return self.installed_version not in req_obj
def render_as_root(self, frozen):
if not frozen:
return '{0}=={1}'.format(self.project_name, self.installed_version)
elif self.dist:
return self.__class__.frozen_repr(self.dist._obj)
else:
return self.project_name
def render_as_branch(self, frozen):
if not frozen:
req_ver = self.version_spec if self.version_spec else 'Any'
return (
'{0} [required: {1}, installed: {2}]'
).format(self.project_name, req_ver, self.installed_version)
else:
return self.render_as_root(frozen)
def as_dict(self):
return {'key': self.key,
'package_name': self.project_name,
'installed_version': self.installed_version,
'required_version': self.version_spec}
class PackageDAG(Mapping):
"""Representation of Package dependencies as directed acyclic graph
using a dict (Mapping) as the underlying datastructure.
The nodes and their relationships (edges) are internally
stored using a map as follows,
{a: [b, c],
b: [d],
c: [d, e],
d: [e],
e: [],
f: [b],
g: [e, f]}
Here, node `a` has 2 children nodes `b` and `c`. Consider edge
direction from `a` -> `b` and `a` -> `c` respectively.
A node is expected to be an instance of a subclass of
`Package`. The keys are must be of class `DistPackage` and each
item in values must be of class `ReqPackage`. (See also
ReversedPackageDAG where the key and value types are
interchanged).
"""
@classmethod
def from_pkgs(cls, pkgs):
pkgs = [DistPackage(p) for p in pkgs]
idx = {p.key: p for p in pkgs}
m = {p: [ReqPackage(r, idx.get(r.key))
for r in p.requires()]
for p in pkgs}
return cls(m)
def __init__(self, m):
"""Initialize the PackageDAG object
:param dict m: dict of node objects (refer class docstring)
:returns: None
:rtype: NoneType
"""
self._obj = m
self._index = {p.key: p for p in list(self._obj)}
def get_node_as_parent(self, node_key):
"""Get the node from the keys of the dict representing the DAG.
This method is useful if the dict representing the DAG
contains different kind of objects in keys and values. Use
this method to lookup a node obj as a parent (from the keys of
the dict) given a node key.
:param node_key: identifier corresponding to key attr of node obj
:returns: node obj (as present in the keys of the dict)
:rtype: Object
"""
try:
return self._index[node_key]
except KeyError:
return None
def get_children(self, node_key):
"""Get child nodes for a node by it's key
:param str node_key: key of the node to get children of
:returns: list of child nodes
:rtype: ReqPackage[]
"""
node = self.get_node_as_parent(node_key)
return self._obj[node] if node else []
def filter(self, include, exclude):
"""Filters nodes in a graph by given parameters
If a node is included, then all it's children are also
included.
:param set include: set of node keys to include (or None)
:param set exclude: set of node keys to exclude (or None)
:returns: filtered version of the graph
:rtype: PackageDAG
"""
# If neither of the filters are specified, short circuit
if include is None and exclude is None:
return self
# Note: In following comparisons, we use lower cased values so
# that user may specify `key` or `project_name`. As per the
# documentation, `key` is simply
# `project_name.lower()`. Refer:
# https://setuptools.readthedocs.io/en/latest/pkg_resources.html#distribution-objects
if include:
include = set([s.lower() for s in include])
if exclude:
exclude = set([s.lower() for s in exclude])
else:
exclude = set([])
# Check for mutual exclusion of show_only and exclude sets
# after normalizing the values to lowercase
if include and exclude:
assert not (include & exclude)
# Traverse the graph in a depth first manner and filter the
# nodes according to `show_only` and `exclude` sets
stack = deque()
m = {}
seen = set([])
for node in self._obj.keys():
if node.key in exclude:
continue
if include is None or node.key in include:
stack.append(node)
while True:
if len(stack) > 0:
n = stack.pop()
cldn = [c for c in self._obj[n]
if c.key not in exclude]
m[n] = cldn
seen.add(n.key)
for c in cldn:
if c.key not in seen:
cld_node = self.get_node_as_parent(c.key)
if cld_node:
stack.append(cld_node)
else:
# It means there's no root node
# corresponding to the child node
# ie. a dependency is missing
continue
else:
break
return self.__class__(m)
def reverse(self):
"""Reverse the DAG, or turn it upside-down
In other words, the directions of edges of the nodes in the
DAG will be reversed.
Note that this function purely works on the nodes in the
graph. This implies that to perform a combination of filtering
and reversing, the order in which `filter` and `reverse`
methods should be applied is important. For eg. if reverse is
called on a filtered graph, then only the filtered nodes and
it's children will be considered when reversing. On the other
hand, if filter is called on reversed DAG, then the definition
of "child" nodes is as per the reversed DAG.
:returns: DAG in the reversed form
:rtype: ReversedPackageDAG
"""
m = defaultdict(list)
child_keys = set(r.key for r in flatten(self._obj.values()))
for k, vs in self._obj.items():
for v in vs:
# if v is already added to the dict, then ensure that
# we are using the same object. This check is required
# as we're using array mutation
try:
node = [p for p in m.keys() if p.key == v.key][0]
except IndexError:
node = v
m[node].append(k.as_parent_of(v))
if k.key not in child_keys:
m[k.as_requirement()] = []
return ReversedPackageDAG(dict(m))
def sort(self):
"""Return sorted tree in which the underlying _obj dict is an
OrderedDict, sorted alphabetically by the keys
:returns: Instance of same class with OrderedDict
"""
return self.__class__(sorted_tree(self._obj))
# Methods required by the abstract base class Mapping
def __getitem__(self, *args):
return self._obj.get(*args)
def __iter__(self):
return self._obj.__iter__()
def __len__(self):
return len(self._obj)
class ReversedPackageDAG(PackageDAG):
"""Representation of Package dependencies in the reverse
order.
Similar to it's super class `PackageDAG`, the underlying
datastructure is a dict, but here the keys are expected to be of
type `ReqPackage` and each item in the values of type
`DistPackage`.
Typically, this object will be obtained by calling
`PackageDAG.reverse`.
"""
def reverse(self):
"""Reverse the already reversed DAG to get the PackageDAG again
:returns: reverse of the reversed DAG
:rtype: PackageDAG
"""
m = defaultdict(list)
child_keys = set(r.key for r in flatten(self._obj.values()))
for k, vs in self._obj.items():
for v in vs:
try:
node = [p for p in m.keys() if p.key == v.key][0]
except IndexError:
node = v.as_parent_of(None)
m[node].append(k)
if k.key not in child_keys:
m[k.dist] = []
return PackageDAG(dict(m))
def render_text(tree, list_all=True, frozen=False):
"""Print tree as text on console
:param dict tree: the package tree
:param bool list_all: whether to list all the pgks at the root
level or only those that are the
sub-dependencies
:param bool frozen: whether or not show the names of the pkgs in
the output that's favourable to pip --freeze
:returns: None
"""
tree = tree.sort()
nodes = tree.keys()
branch_keys = set(r.key for r in flatten(tree.values()))
use_bullets = not frozen
if not list_all:
nodes = [p for p in nodes if p.key not in branch_keys]
def aux(node, parent=None, indent=0, chain=None):
chain = chain or []
node_str = node.render(parent, frozen)
if parent:
prefix = ' '*indent + ('- ' if use_bullets else '')
node_str = prefix + node_str
result = [node_str]
children = [aux(c, node, indent=indent+2,
chain=chain+[c.project_name])
for c in tree.get_children(node.key)
if c.project_name not in chain]
result += list(flatten(children))
return result
lines = flatten([aux(p) for p in nodes])
print('\n'.join(lines))
def render_json(tree, indent):
"""Converts the tree into a flat json representation.
The json repr will be a list of hashes, each hash having 2 fields:
- package
- dependencies: list of dependencies
:param dict tree: dependency tree
:param int indent: no. of spaces to indent json
:returns: json representation of the tree
:rtype: str
"""
tree = tree.sort()
return json.dumps([{'package': k.as_dict(),
'dependencies': [v.as_dict() for v in vs]}
for k, vs in tree.items()],
indent=indent)
def render_json_tree(tree, indent):
"""Converts the tree into a nested json representation.
The json repr will be a list of hashes, each hash having the following fields:
- package_name
- key
- required_version
- installed_version
- dependencies: list of dependencies
:param dict tree: dependency tree
:param int indent: no. of spaces to indent json
:returns: json representation of the tree
:rtype: str
"""
tree = tree.sort()
branch_keys = set(r.key for r in flatten(tree.values()))
nodes = [p for p in tree.keys() if p.key not in branch_keys]
def aux(node, parent=None, chain=None):
if chain is None:
chain = [node.project_name]
d = node.as_dict()
if parent:
d['required_version'] = node.version_spec if node.version_spec else 'Any'
else:
d['required_version'] = d['installed_version']
d['dependencies'] = [
aux(c, parent=node, chain=chain+[c.project_name])
for c in tree.get_children(node.key)
if c.project_name not in chain
]
return d
return json.dumps([aux(p) for p in nodes], indent=indent)
def dump_graphviz(tree, output_format='dot', is_reverse=False):
"""Output dependency graph as one of the supported GraphViz output formats.
:param dict tree: dependency graph
:param string output_format: output format
:returns: representation of tree in the specified output format
:rtype: str or binary representation depending on the output format
"""
try:
from graphviz import backend, Digraph
except ImportError:
print('graphviz is not available, but necessary for the output '
'option. Please install it.', file=sys.stderr)
sys.exit(1)
if output_format not in backend.FORMATS:
print('{0} is not a supported output format.'.format(output_format),
file=sys.stderr)
print('Supported formats are: {0}'.format(
', '.join(sorted(backend.FORMATS))), file=sys.stderr)
sys.exit(1)
graph = Digraph(format=output_format)
if not is_reverse:
for pkg, deps in tree.items():
pkg_label = '{0}\\n{1}'.format(pkg.project_name, pkg.version)
graph.node(pkg.key, label=pkg_label)
for dep in deps:
edge_label = dep.version_spec or 'any'
if dep.is_missing:
dep_label = '{0}\\n(missing)'.format(dep.project_name)
graph.node(dep.key, label=dep_label, style='dashed')
graph.edge(pkg.key, dep.key, style='dashed')
else:
graph.edge(pkg.key, dep.key, label=edge_label)
else:
for dep, parents in tree.items():
dep_label = '{0}\\n{1}'.format(dep.project_name,
dep.installed_version)
graph.node(dep.key, label=dep_label)
for parent in parents:
# req reference of the dep associated with this
# particular parent package
req_ref = parent.req
edge_label = req_ref.version_spec or 'any'
graph.edge(dep.key, parent.key, label=edge_label)
# Allow output of dot format, even if GraphViz isn't installed.
if output_format == 'dot':
return graph.source
# As it's unknown if the selected output format is binary or not, try to
# decode it as UTF8 and only print it out in binary if that's not possible.
try:
return graph.pipe().decode('utf-8')
except UnicodeDecodeError:
return graph.pipe()
def print_graphviz(dump_output):
"""Dump the data generated by GraphViz to stdout.
:param dump_output: The output from dump_graphviz
"""
if hasattr(dump_output, 'encode'):
print(dump_output)
else:
with os.fdopen(sys.stdout.fileno(), 'wb') as bytestream:
bytestream.write(dump_output)
def conflicting_deps(tree):
"""Returns dependencies which are not present or conflict with the
requirements of other packages.
e.g. will warn if pkg1 requires pkg2==2.0 and pkg2==1.0 is installed
:param tree: the requirements tree (dict)
:returns: dict of DistPackage -> list of unsatisfied/unknown ReqPackage
:rtype: dict
"""
conflicting = defaultdict(list)
for p, rs in tree.items():
for req in rs:
if req.is_conflicting():
conflicting[p].append(req)
return conflicting
def render_conflicts_text(conflicts):
if conflicts:
print('Warning!!! Possibly conflicting dependencies found:',
file=sys.stderr)
# Enforce alphabetical order when listing conflicts
pkgs = sorted(conflicts.keys())
for p in pkgs:
pkg = p.render_as_root(False)
print('* {}'.format(pkg), file=sys.stderr)
for req in conflicts[p]:
req_str = req.render_as_branch(False)
print(' - {}'.format(req_str), file=sys.stderr)
def cyclic_deps(tree):
"""Return cyclic dependencies as list of tuples
:param PackageDAG pkgs: package tree/dag
:returns: list of tuples representing cyclic dependencies
:rtype: list
"""
index = {p.key: set([r.key for r in rs]) for p, rs in tree.items()}
cyclic = []
for p, rs in tree.items():
for r in rs:
if p.key in index.get(r.key, []):
p_as_dep_of_r = [x for x
in tree.get(tree.get_node_as_parent(r.key))
if x.key == p.key][0]
cyclic.append((p, r, p_as_dep_of_r))
return cyclic
def render_cycles_text(cycles):
if cycles:
print('Warning!! Cyclic dependencies found:', file=sys.stderr)
# List in alphabetical order of the dependency that's cycling
# (2nd item in the tuple)
cycles = sorted(cycles, key=lambda xs: xs[1].key)
for a, b, c in cycles:
print('* {0} => {1} => {2}'.format(a.project_name,
b.project_name,
c.project_name),
file=sys.stderr)
def get_parser():
parser = argparse.ArgumentParser(description=(
'Dependency tree of the installed python packages'
))
parser.add_argument('-v', '--version', action='version',
version='{0}'.format(__version__))
parser.add_argument('-f', '--freeze', action='store_true',
help='Print names so as to write freeze files')
parser.add_argument('--python', default=sys.executable,
help='Python to use to look for packages in it (default: where'
' installed)')
parser.add_argument('-a', '--all', action='store_true',
help='list all deps at top level')
parser.add_argument('-l', '--local-only',
action='store_true', help=(
'If in a virtualenv that has global access '
'do not show globally installed packages'
))
parser.add_argument('-u', '--user-only', action='store_true',
help=(
'Only show installations in the user site dir'
))
parser.add_argument('-w', '--warn', action='store', dest='warn',
nargs='?', default='suppress',
choices=('silence', 'suppress', 'fail'),
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. The default is '
'"suppress".'
))
parser.add_argument('-r', '--reverse', action='store_true',
default=False, help=(
'Shows the dependency tree in the reverse fashion '
'ie. the sub-dependencies are listed with the '
'list of packages that need them under them.'
))
parser.add_argument('-p', '--packages',
help=(
'Comma separated list of select packages to show '
'in the output. If set, --all will be ignored.'
))
parser.add_argument('-e', '--exclude',
help=(
'Comma separated list of select packages to exclude '
'from the output. If set, --all will be ignored.'
), metavar='PACKAGES')
parser.add_argument('-j', '--json', action='store_true', default=False,
help=(
'Display dependency tree as json. This will yield '
'"raw" output that may be used by external tools. '
'This option overrides all other options.'
))
parser.add_argument('--json-tree', action='store_true', default=False,
help=(
'Display dependency tree as json which is nested '
'the same way as the plain text output printed by default. '
'This option overrides all other options (except --json).'
))
parser.add_argument('--graph-output', dest='output_format',
help=(
'Print a dependency graph in the specified output '
'format. Available are all formats supported by '
'GraphViz, e.g.: dot, jpeg, pdf, png, svg'
))
return parser
def _get_args():
parser = get_parser()
return parser.parse_args()
def handle_non_host_target(args):
of_python = os.path.abspath(args.python)
# if target is not current python re-invoke it under the actual host
if of_python != os.path.abspath(sys.executable):
# there's no way to guarantee that graphviz is available, so refuse
if args.output_format:
print("graphviz functionality is not supported when querying"
" non-host python", file=sys.stderr)
raise SystemExit(1)
argv = sys.argv[1:] # remove current python executable
for py_at, value in enumerate(argv):
if value == "--python":
del argv[py_at]
del argv[py_at]
elif value.startswith("--python"):
del argv[py_at]
# feed the file as argument, instead of file
# to avoid adding the file path to sys.path, that can affect result
file_path = inspect.getsourcefile(sys.modules[__name__])
with open(file_path, 'rt') as file_handler:
content = file_handler.read()
cmd = [of_python, "-c", content]
cmd.extend(argv)
# invoke from an empty folder to avoid cwd altering sys.path
cwd = tempfile.mkdtemp()
try:
return subprocess.call(cmd, cwd=cwd)
finally:
os.removedirs(cwd)
return None
def main():
args = _get_args()
result = handle_non_host_target(args)
if result is not None:
return result
pkgs = list(pkg_resources.working_set)
tree = PackageDAG.from_pkgs(pkgs)
is_text_output = not any([args.json, args.json_tree, args.output_format])
return_code = 0
# Before any reversing or filtering, show warnings to console
# about possibly conflicting or cyclic deps if found and warnings
# are enabled (ie. only if output is to be printed to console)
if is_text_output and args.warn != 'silence':
conflicts = conflicting_deps(tree)
if conflicts:
render_conflicts_text(conflicts)
print('-'*72, file=sys.stderr)
cycles = cyclic_deps(tree)
if cycles:
render_cycles_text(cycles)
print('-'*72, file=sys.stderr)
if args.warn == 'fail' and (conflicts or cycles):
return_code = 1
# Reverse the tree (if applicable) before filtering, thus ensuring
# that the filter will be applied on ReverseTree
if args.reverse:
tree = tree.reverse()
show_only = set(args.packages.split(',')) if args.packages else None
exclude = set(args.exclude.split(',')) if args.exclude else None
if show_only is not None or exclude is not None:
tree = tree.filter(show_only, exclude)
if args.json:
print(render_json(tree, indent=4))
elif args.json_tree:
print(render_json_tree(tree, indent=4))
elif args.output_format:
output = dump_graphviz(tree,
output_format=args.output_format,
is_reverse=args.reverse)
print_graphviz(output)
else:
render_text(tree, args.all, args.freeze)
return return_code
if __name__ == '__main__':
sys.exit(main())
pipdeptree-2.2.0/tox.ini 0000644 0001750 0001750 00000000613 14131341150 014506 0 ustar nilesh nilesh # http://tox.readthedocs.org/ - sets up and runs the test suite based on a declarative configuration
[tox]
envlist =
py39
py38
py37
py36
py35
py34
py27
pypy3
pypy2
[testenv]
description = run test suite under {basepython}
commands =
pytest {posargs:-vv}
deps =
graphviz
pip>=8.0.2
pytest
pytest-cov
virtualenv>=20,<21
mock;python_version<"3"
extras =
graphviz
pipdeptree-2.2.0/Makefile 0000644 0001750 0001750 00000001575 14131341150 014643 0 ustar nilesh nilesh .PHONY: clean test-env test test-cov test-tox-all test-e2e
TOX_ENV ?= py36
E2E_PYTHON_EXE ?= python3.6
clean:
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rmdir {} +
test-env:
pip install -r dev-requirements.txt
test:
tox -e $(TOX_ENV)
test-cov:
tox -e $(TOX_ENV) -- -x -vv --cov=pipdeptree --cov-report=xml --cov-report=html --cov-report=term-missing
# Requires all the versions of python executables to be present (See
# tox.ini for list of python versions)
test-tox-all:
tox
clean-e2e:
rm -rf tests/profiles/*/.env_$(E2E_PYTHON_EXE)*
test-e2e: clean-e2e
cd tests && ./e2e-tests webapp
cd tests && ./e2e-tests conflicting
cd tests && ./e2e-tests cyclic
test-e2e-quick:
cd tests && ./e2e-tests webapp
cd tests && ./e2e-tests conflicting
cd tests && ./e2e-tests cyclic
pipdeptree-2.2.0/setup.py 0000644 0001750 0001750 00000002726 14131341150 014714 0 ustar nilesh nilesh import re
import ast
from setuptools import setup
_version_re = re.compile(r'__version__\s+=\s+(.*)')
with open('pipdeptree.py', 'rb') as f:
version = str(ast.literal_eval(_version_re.search(
f.read().decode('utf-8')).group(1)))
with open('./README.rst') as f:
long_desc = f.read()
install_requires = ["pip >= 6.0.0"]
setup(
name='pipdeptree',
version=version,
author='Vineet Naik',
author_email='naikvin@gmail.com',
url='https://github.com/naiquevin/pipdeptree',
license='MIT License',
license_file='LICENSE',
description='Command line utility to show dependency tree of packages',
long_description=long_desc,
install_requires=install_requires,
extras_require={'graphviz': ['graphviz']},
python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*',
py_modules=['pipdeptree'],
entry_points={
'console_scripts': [
'pipdeptree = pipdeptree:main'
]
},
classifiers=[
'Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9'
]
)
pipdeptree-2.2.0/tests/ 0000755 0001750 0001750 00000000000 14131341150 014335 5 ustar nilesh nilesh pipdeptree-2.2.0/tests/profiles/ 0000755 0001750 0001750 00000000000 14131341150 016160 5 ustar nilesh nilesh pipdeptree-2.2.0/tests/profiles/webapp/ 0000755 0001750 0001750 00000000000 14131341150 017436 5 ustar nilesh nilesh pipdeptree-2.2.0/tests/profiles/webapp/test_spec.json 0000644 0001750 0001750 00000002202 14131341150 022316 0 ustar nilesh nilesh [
{
"id": "default_output",
"method": "cmp_with_file_contents",
"command": "{{pipdeptree}}",
"expected_output_file": "default.out",
"expected_err_file": null,
"expected_returncode": 0
},
{
"id": "reverse_output",
"method": "cmp_with_file_contents",
"command": "{{pipdeptree}} -r",
"expected_output_file": "reverse.out",
"expected_err_file": null,
"expected_returncode": 0
},
{
"id": "--all_output",
"method": "cmp_with_file_contents",
"command": "{{pipdeptree}} --all",
"expected_output_file": "all_flag.out",
"expected_err_file": null,
"expected_returncode": 0
},
{
"id": "--packages_output",
"method": "cmp_with_file_contents",
"command": "{{pipdeptree}} --packages pexpect,ipython",
"expected_output_file": "packages_opt.out",
"expected_err_file": null,
"expected_returncode": 0
},
{
"id": "--packages--reverse_output",
"method": "cmp_with_file_contents",
"command": "{{pipdeptree}} -r -p decorator,click",
"expected_output_file": "packages_reverse.out",
"expected_err_file": null,
"expected_returncode": 0
}
]
pipdeptree-2.2.0/tests/profiles/webapp/default.out 0000644 0001750 0001750 00000003006 14131341150 021612 0 ustar nilesh nilesh Flask-Script==2.0.6
- Flask [required: Any, installed: 1.1.2]
- click [required: >=5.1, installed: 8.0.3]
- importlib-metadata [required: Any, installed: 4.8.1]
- typing-extensions [required: >=3.6.4, installed: 3.10.0.2]
- zipp [required: >=0.5, installed: 3.6.0]
- itsdangerous [required: >=0.24, installed: 2.0.1]
- Jinja2 [required: >=2.10.1, installed: 3.0.2]
- MarkupSafe [required: >=2.0, installed: 2.0.1]
- Werkzeug [required: >=0.15, installed: 2.0.2]
- dataclasses [required: Any, installed: 0.8]
gnureadline==8.0.0
ipython==7.13.0
- appnope [required: Any, installed: 0.1.2]
- backcall [required: Any, installed: 0.2.0]
- decorator [required: Any, installed: 5.1.0]
- jedi [required: >=0.10, installed: 0.18.0]
- parso [required: >=0.8.0,<0.9.0, installed: 0.8.2]
- pexpect [required: Any, installed: 4.8.0]
- ptyprocess [required: >=0.5, installed: 0.7.0]
- pickleshare [required: Any, installed: 0.7.5]
- prompt-toolkit [required: >=2.0.0,<3.1.0,!=3.0.1,!=3.0.0, installed: 3.0.20]
- wcwidth [required: Any, installed: 0.2.5]
- pygments [required: Any, installed: 2.10.0]
- setuptools [required: >=18.5, installed: 58.2.0]
- traitlets [required: >=4.2, installed: 4.3.3]
- decorator [required: Any, installed: 5.1.0]
- ipython-genutils [required: Any, installed: 0.2.0]
- six [required: Any, installed: 1.16.0]
pipdeptree==2.1.0
- pip [required: >=6.0.0, installed: 21.3]
pymongo==3.10.1
redis==3.4.1
slugify==0.0.1
wheel==0.37.0
pipdeptree-2.2.0/tests/profiles/webapp/packages_reverse.out 0000644 0001750 0001750 00000000370 14131341150 023500 0 ustar nilesh nilesh click==8.0.3
- Flask==1.1.2 [requires: click>=5.1]
- Flask-Script==2.0.6 [requires: Flask]
decorator==5.1.0
- ipython==7.13.0 [requires: decorator]
- traitlets==4.3.3 [requires: decorator]
- ipython==7.13.0 [requires: traitlets>=4.2]
pipdeptree-2.2.0/tests/profiles/webapp/packages_opt.out 0000644 0001750 0001750 00000001512 14131341150 022626 0 ustar nilesh nilesh ipython==7.13.0
- appnope [required: Any, installed: 0.1.2]
- backcall [required: Any, installed: 0.2.0]
- decorator [required: Any, installed: 5.1.0]
- jedi [required: >=0.10, installed: 0.18.0]
- parso [required: >=0.8.0,<0.9.0, installed: 0.8.2]
- pexpect [required: Any, installed: 4.8.0]
- ptyprocess [required: >=0.5, installed: 0.7.0]
- pickleshare [required: Any, installed: 0.7.5]
- prompt-toolkit [required: >=2.0.0,<3.1.0,!=3.0.1,!=3.0.0, installed: 3.0.20]
- wcwidth [required: Any, installed: 0.2.5]
- pygments [required: Any, installed: 2.10.0]
- setuptools [required: >=18.5, installed: 58.2.0]
- traitlets [required: >=4.2, installed: 4.3.3]
- decorator [required: Any, installed: 5.1.0]
- ipython-genutils [required: Any, installed: 0.2.0]
- six [required: Any, installed: 1.16.0]
pipdeptree-2.2.0/tests/profiles/webapp/requirements.txt 0000644 0001750 0001750 00000000160 14131341150 022717 0 ustar nilesh nilesh Flask==1.1.2
Flask-Script==2.0.6
gnureadline==8.0.0
pymongo==3.10.1
redis==3.4.1
slugify==0.0.1
ipython==7.13.0
pipdeptree-2.2.0/tests/profiles/webapp/reverse.out 0000644 0001750 0001750 00000003750 14131341150 021647 0 ustar nilesh nilesh appnope==0.1.2
- ipython==7.13.0 [requires: appnope]
backcall==0.2.0
- ipython==7.13.0 [requires: backcall]
dataclasses==0.8
- Werkzeug==2.0.2 [requires: dataclasses]
- Flask==1.1.2 [requires: Werkzeug>=0.15]
- Flask-Script==2.0.6 [requires: Flask]
decorator==5.1.0
- ipython==7.13.0 [requires: decorator]
- traitlets==4.3.3 [requires: decorator]
- ipython==7.13.0 [requires: traitlets>=4.2]
gnureadline==8.0.0
ipython-genutils==0.2.0
- traitlets==4.3.3 [requires: ipython-genutils]
- ipython==7.13.0 [requires: traitlets>=4.2]
itsdangerous==2.0.1
- Flask==1.1.2 [requires: itsdangerous>=0.24]
- Flask-Script==2.0.6 [requires: Flask]
MarkupSafe==2.0.1
- Jinja2==3.0.2 [requires: MarkupSafe>=2.0]
- Flask==1.1.2 [requires: Jinja2>=2.10.1]
- Flask-Script==2.0.6 [requires: Flask]
parso==0.8.2
- jedi==0.18.0 [requires: parso>=0.8.0,<0.9.0]
- ipython==7.13.0 [requires: jedi>=0.10]
pickleshare==0.7.5
- ipython==7.13.0 [requires: pickleshare]
pip==21.3
- pipdeptree==2.1.0 [requires: pip>=6.0.0]
ptyprocess==0.7.0
- pexpect==4.8.0 [requires: ptyprocess>=0.5]
- ipython==7.13.0 [requires: pexpect]
pygments==2.10.0
- ipython==7.13.0 [requires: pygments]
pymongo==3.10.1
redis==3.4.1
setuptools==58.2.0
- ipython==7.13.0 [requires: setuptools>=18.5]
six==1.16.0
- traitlets==4.3.3 [requires: six]
- ipython==7.13.0 [requires: traitlets>=4.2]
slugify==0.0.1
typing-extensions==3.10.0.2
- importlib-metadata==4.8.1 [requires: typing-extensions>=3.6.4]
- click==8.0.3 [requires: importlib-metadata]
- Flask==1.1.2 [requires: click>=5.1]
- Flask-Script==2.0.6 [requires: Flask]
wcwidth==0.2.5
- prompt-toolkit==3.0.20 [requires: wcwidth]
- ipython==7.13.0 [requires: prompt-toolkit>=2.0.0,<3.1.0,!=3.0.1,!=3.0.0]
wheel==0.37.0
zipp==3.6.0
- importlib-metadata==4.8.1 [requires: zipp>=0.5]
- click==8.0.3 [requires: importlib-metadata]
- Flask==1.1.2 [requires: click>=5.1]
- Flask-Script==2.0.6 [requires: Flask]
pipdeptree-2.2.0/tests/profiles/webapp/all_flag.out 0000644 0001750 0001750 00000006076 14131341150 021741 0 ustar nilesh nilesh appnope==0.1.2
backcall==0.2.0
click==8.0.3
- importlib-metadata [required: Any, installed: 4.8.1]
- typing-extensions [required: >=3.6.4, installed: 3.10.0.2]
- zipp [required: >=0.5, installed: 3.6.0]
dataclasses==0.8
decorator==5.1.0
Flask==1.1.2
- click [required: >=5.1, installed: 8.0.3]
- importlib-metadata [required: Any, installed: 4.8.1]
- typing-extensions [required: >=3.6.4, installed: 3.10.0.2]
- zipp [required: >=0.5, installed: 3.6.0]
- itsdangerous [required: >=0.24, installed: 2.0.1]
- Jinja2 [required: >=2.10.1, installed: 3.0.2]
- MarkupSafe [required: >=2.0, installed: 2.0.1]
- Werkzeug [required: >=0.15, installed: 2.0.2]
- dataclasses [required: Any, installed: 0.8]
Flask-Script==2.0.6
- Flask [required: Any, installed: 1.1.2]
- click [required: >=5.1, installed: 8.0.3]
- importlib-metadata [required: Any, installed: 4.8.1]
- typing-extensions [required: >=3.6.4, installed: 3.10.0.2]
- zipp [required: >=0.5, installed: 3.6.0]
- itsdangerous [required: >=0.24, installed: 2.0.1]
- Jinja2 [required: >=2.10.1, installed: 3.0.2]
- MarkupSafe [required: >=2.0, installed: 2.0.1]
- Werkzeug [required: >=0.15, installed: 2.0.2]
- dataclasses [required: Any, installed: 0.8]
gnureadline==8.0.0
importlib-metadata==4.8.1
- typing-extensions [required: >=3.6.4, installed: 3.10.0.2]
- zipp [required: >=0.5, installed: 3.6.0]
ipython==7.13.0
- appnope [required: Any, installed: 0.1.2]
- backcall [required: Any, installed: 0.2.0]
- decorator [required: Any, installed: 5.1.0]
- jedi [required: >=0.10, installed: 0.18.0]
- parso [required: >=0.8.0,<0.9.0, installed: 0.8.2]
- pexpect [required: Any, installed: 4.8.0]
- ptyprocess [required: >=0.5, installed: 0.7.0]
- pickleshare [required: Any, installed: 0.7.5]
- prompt-toolkit [required: >=2.0.0,<3.1.0,!=3.0.1,!=3.0.0, installed: 3.0.20]
- wcwidth [required: Any, installed: 0.2.5]
- pygments [required: Any, installed: 2.10.0]
- setuptools [required: >=18.5, installed: 58.2.0]
- traitlets [required: >=4.2, installed: 4.3.3]
- decorator [required: Any, installed: 5.1.0]
- ipython-genutils [required: Any, installed: 0.2.0]
- six [required: Any, installed: 1.16.0]
ipython-genutils==0.2.0
itsdangerous==2.0.1
jedi==0.18.0
- parso [required: >=0.8.0,<0.9.0, installed: 0.8.2]
Jinja2==3.0.2
- MarkupSafe [required: >=2.0, installed: 2.0.1]
MarkupSafe==2.0.1
parso==0.8.2
pexpect==4.8.0
- ptyprocess [required: >=0.5, installed: 0.7.0]
pickleshare==0.7.5
pip==21.3
pipdeptree==2.1.0
- pip [required: >=6.0.0, installed: 21.3]
prompt-toolkit==3.0.20
- wcwidth [required: Any, installed: 0.2.5]
ptyprocess==0.7.0
Pygments==2.10.0
pymongo==3.10.1
redis==3.4.1
setuptools==58.2.0
six==1.16.0
slugify==0.0.1
traitlets==4.3.3
- decorator [required: Any, installed: 5.1.0]
- ipython-genutils [required: Any, installed: 0.2.0]
- six [required: Any, installed: 1.16.0]
typing-extensions==3.10.0.2
wcwidth==0.2.5
Werkzeug==2.0.2
- dataclasses [required: Any, installed: 0.8]
wheel==0.37.0
zipp==3.6.0
pipdeptree-2.2.0/tests/profiles/webapp/.gitignore 0000644 0001750 0001750 00000000005 14131341150 021421 0 ustar nilesh nilesh .env* pipdeptree-2.2.0/tests/profiles/cyclic/ 0000755 0001750 0001750 00000000000 14131341150 017426 5 ustar nilesh nilesh pipdeptree-2.2.0/tests/profiles/cyclic/test_spec.json 0000644 0001750 0001750 00000001251 14131341150 022311 0 ustar nilesh nilesh [
{
"id": "default_output",
"method": "cmp_with_file_contents",
"command": "{{pipdeptree}}",
"expected_output_file": "default.out",
"expected_err_file": "default.err",
"expected_returncode": 0
},
{
"id": "warning_silenced",
"method": "cmp_with_file_contents",
"command": "{{pipdeptree}} -w silence",
"expected_output_file": "default.out",
"expected_err_file": null,
"expected_returncode": 0
},
{
"id": "fail_if_cyclic",
"method": "cmp_with_file_contents",
"command": "{{pipdeptree}} -w fail",
"expected_output_file": "default.out",
"expected_err_file": "default.err",
"expected_returncode": 1
}
]
pipdeptree-2.2.0/tests/profiles/cyclic/default.err 0000644 0001750 0001750 00000000366 14131341150 021571 0 ustar nilesh nilesh Warning!! Cyclic dependencies found:
* CircularDependencyB => CircularDependencyA => CircularDependencyB
* CircularDependencyA => CircularDependencyB => CircularDependencyA
------------------------------------------------------------------------
pipdeptree-2.2.0/tests/profiles/cyclic/default.out 0000644 0001750 0001750 00000000140 14131341150 021576 0 ustar nilesh nilesh pipdeptree==2.1.0
- pip [required: >=6.0.0, installed: 21.3]
setuptools==58.2.0
wheel==0.37.0
pipdeptree-2.2.0/tests/profiles/cyclic/requirements.txt 0000644 0001750 0001750 00000000050 14131341150 022705 0 ustar nilesh nilesh CircularDependencyA
CircularDependencyB
pipdeptree-2.2.0/tests/profiles/cyclic/.gitignore 0000644 0001750 0001750 00000000005 14131341150 021411 0 ustar nilesh nilesh .env* pipdeptree-2.2.0/tests/profiles/conflicting/ 0000755 0001750 0001750 00000000000 14131341150 020457 5 ustar nilesh nilesh pipdeptree-2.2.0/tests/profiles/conflicting/test_spec.json 0000644 0001750 0001750 00000001617 14131341150 023350 0 ustar nilesh nilesh [
{
"id": "default_output",
"method": "cmp_with_file_contents",
"command": "{{pipdeptree}}",
"expected_output_file": "default.out",
"expected_err_file": "default.err",
"expected_returncode": 0
},
{
"id": "reverse_output",
"method": "cmp_with_file_contents",
"command": "{{pipdeptree}} -r",
"expected_output_file": "reverse.out",
"expected_err_file": "default.err",
"expected_returncode": 0
},
{
"id": "warning_silenced",
"method": "cmp_with_file_contents",
"command": "{{pipdeptree}} -w silence",
"expected_output_file": "default.out",
"expected_err_file": null,
"expected_returncode": 0
},
{
"id": "fail_if_conflicting",
"method": "cmp_with_file_contents",
"command": "{{pipdeptree}} -w fail",
"expected_output_file": "default.out",
"expected_err_file": "default.err",
"expected_returncode": 1
}
]
pipdeptree-2.2.0/tests/profiles/conflicting/default.err 0000644 0001750 0001750 00000000276 14131341150 022622 0 ustar nilesh nilesh Warning!!! Possibly conflicting dependencies found:
* Jinja2==3.0.2
- MarkupSafe [required: >=2.0, installed: 0.22]
------------------------------------------------------------------------
pipdeptree-2.2.0/tests/profiles/conflicting/default.out 0000644 0001750 0001750 00000000512 14131341150 022632 0 ustar nilesh nilesh argparse==1.4.0
Flask==0.10.1
- itsdangerous [required: >=0.21, installed: 0.24]
- Jinja2 [required: >=2.4, installed: 3.0.2]
- MarkupSafe [required: >=2.0, installed: 0.22]
- Werkzeug [required: >=0.7, installed: 0.11.2]
pipdeptree==2.1.0
- pip [required: >=6.0.0, installed: 20.2.3]
setuptools==58.2.0
wheel==0.37.0
pipdeptree-2.2.0/tests/profiles/conflicting/requirements.txt 0000644 0001750 0001750 00000000123 14131341150 023737 0 ustar nilesh nilesh Flask==0.10.1
itsdangerous==0.24
Jinja2
MarkupSafe==0.22
Werkzeug==0.11.2
argparse
pipdeptree-2.2.0/tests/profiles/conflicting/reverse.out 0000644 0001750 0001750 00000000526 14131341150 022666 0 ustar nilesh nilesh argparse==1.4.0
itsdangerous==0.24
- Flask==0.10.1 [requires: itsdangerous>=0.21]
MarkupSafe==0.22
- Jinja2==3.0.2 [requires: MarkupSafe>=2.0]
- Flask==0.10.1 [requires: Jinja2>=2.4]
pip==20.2.3
- pipdeptree==2.1.0 [requires: pip>=6.0.0]
setuptools==58.2.0
Werkzeug==0.11.2
- Flask==0.10.1 [requires: Werkzeug>=0.7]
wheel==0.37.0
pipdeptree-2.2.0/tests/profiles/conflicting/.gitignore 0000644 0001750 0001750 00000000005 14131341150 022442 0 ustar nilesh nilesh .env* pipdeptree-2.2.0/tests/e2e_tests.py 0000644 0001750 0001750 00000003543 14131341150 016611 0 ustar nilesh nilesh import json
import os
import shlex
import subprocess
from jinja2 import Environment, BaseLoader
import pytest
## Uncomment following lines for running in shell
# os.environ['TEST_PROFILE_DIR'] = 'profiles/webapp'
# os.environ['PIPDEPTREE_EXE'] = 'profiles/webapp/.env_python3.6_pip-latest/bin/pipdeptree'
test_profile_dir = os.environ['TEST_PROFILE_DIR']
pipdeptree_path = os.environ['PIPDEPTREE_EXE']
def load_test_spec():
test_spec_path = os.path.join(test_profile_dir, 'test_spec.json')
with open(test_spec_path) as f:
return json.load(f)
test_spec = load_test_spec()
def final_command(s):
tmpl = Environment(loader=BaseLoader).from_string(s)
return tmpl.render(pipdeptree=pipdeptree_path)
def _test_cmp_with_file_contents(spec):
p = subprocess.Popen(shlex.split(spec['command']),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, err = p.communicate()
assert spec['expected_returncode'] == p.returncode
if spec['expected_output_file'] is not None:
exp_output_file = os.path.join(test_profile_dir,
spec['expected_output_file'])
with open(exp_output_file, 'rb') as f:
expected_output = f.read()
assert expected_output == out
else:
assert out == b''
if spec['expected_err_file'] is not None:
exp_err_file = os.path.join(test_profile_dir,
spec['expected_err_file'])
with open(exp_err_file, 'rb') as f:
expected_err = f.read()
assert expected_err == err
else:
assert err == b''
@pytest.mark.parametrize('spec', test_spec)
def test_all_tests_in_profile(spec):
spec['command'] = final_command(spec['command'])
if spec['method'] == 'cmp_with_file_contents':
_test_cmp_with_file_contents(spec)
pipdeptree-2.2.0/tests/e2e-tests 0000755 0001750 0001750 00000001426 14131341150 016101 0 ustar nilesh nilesh #!/usr/bin/env bash
set -e
PROFILE=$1
PYTHON_EXE=${PYTHON_EXE:-python3.6}
PIP_VERSION=${PIP_VERSION:-latest}
if [ "$PROFILE" == "conflicting" ]; then
PIP_VERSION=20.2.3
fi
cd profiles/$PROFILE
echo "Profile dir: $(pwd)"
env_dir=".env_$(basename $PYTHON_EXE)_pip-${PIP_VERSION}"
echo "Profile env: $env_dir"
if [ ! -d $env_dir ]; then
virtualenv -p $PYTHON_EXE $env_dir
fi
pip=$env_dir/bin/pip
if [ "$PIP_VERSION" == "latest" ]; then
$pip install -U pip
else
$pip install pip==$PIP_VERSION
fi
# Install requirements
$pip install -r requirements.txt
# Install pipdeptree
$pip install -e ../../../
pip_deptree=$env_dir/bin/pipdeptree
export TEST_PROFILE_DIR="profiles/$PROFILE"
export PIPDEPTREE_EXE=$TEST_PROFILE_DIR/$pip_deptree
cd -
pytest -v e2e_tests.py
pipdeptree-2.2.0/tests/test_pipdeptree.py 0000644 0001750 0001750 00000044317 14131341150 020120 0 ustar nilesh nilesh from contextlib import contextmanager
import platform
import sys
from tempfile import NamedTemporaryFile
try:
from unittest import mock
except ImportError:
import mock
import pytest
import virtualenv
import pipdeptree as p
# Tests for DAG classes
def mock_pkgs(simple_graph):
for node, children in simple_graph.items():
nk, nv = node
p = mock.Mock(key=nk, project_name=nk, version=nv)
as_req = mock.Mock(key=nk, project_name=nk, specs=[('==', nv)])
p.as_requirement = mock.Mock(return_value=as_req)
reqs = []
for child in children:
ck, cv = child
r = mock.Mock(key=ck, project_name=ck, specs=cv)
reqs.append(r)
p.requires = mock.Mock(return_value=reqs)
yield p
def mock_PackageDAG(simple_graph):
pkgs = list(mock_pkgs(simple_graph))
return p.PackageDAG.from_pkgs(pkgs)
# util for comparing tree contents with a simple graph
def dag_to_dict(g):
return {k.key: [v.key for v in vs] for k, vs in g._obj.items()}
def sort_map_values(m):
return {k: sorted(v) for k, v in m.items()}
t = mock_PackageDAG({
('a', '3.4.0'): [('b', [('>=', '2.0.0')]),
('c', [('>=', '5.7.1')])],
('b', '2.3.1'): [('d', [('>=', '2.30'), ('<', '2.42')])],
('c', '5.10.0'): [('d', [('>=', '2.30')]),
('e', [('>=', '0.12.1')])],
('d', '2.35'): [('e', [('>=', '0.9.0')])],
('e', '0.12.1'): [],
('f', '3.1'): [('b', [('>=', '2.1.0')])],
('g', '6.8.3rc1'): [('e', [('>=', '0.9.0')]),
('f', [('>=', '3.0.0')])]
})
def test_PackageDAG__get_node_as_parent():
assert 'b' == t.get_node_as_parent('b').key
assert 'c' == t.get_node_as_parent('c').key
def test_PackageDAG_filter():
# When both show_only and exclude are not specified, same tree
# object is returned
assert t.filter(None, None) is t
# when show_only is specified
g1 = dag_to_dict(t.filter(set(['a', 'd']), None))
expected = {'a': ['b', 'c'],
'b': ['d'],
'c': ['d', 'e'],
'd': ['e'],
'e': []}
assert expected == g1
# when exclude is specified
g2 = dag_to_dict(t.filter(None, ['d']))
expected = {'a': ['b', 'c'],
'b': [],
'c': ['e'],
'e': [],
'f': ['b'],
'g': ['e', 'f']}
assert expected == g2
# when both show_only and exclude are specified
g3 = dag_to_dict(t.filter(set(['a', 'g']), set(['d', 'e'])))
expected = {'a': ['b', 'c'],
'b': [],
'c': [],
'f': ['b'],
'g': ['f']}
assert expected == g3
# when conflicting values in show_only and exclude, AssertionError
# is raised
with pytest.raises(AssertionError):
dag_to_dict(t.filter(set(['d']), set(['D', 'e'])))
def test_PackageDAG_reverse():
t1 = t.reverse()
expected = {'a': [],
'b': ['a', 'f'],
'c': ['a'],
'd': ['b', 'c'],
'e': ['c', 'd', 'g'],
'f': ['g'],
'g': []}
assert isinstance(t1, p.ReversedPackageDAG)
assert sort_map_values(expected) == sort_map_values(dag_to_dict(t1))
assert all([isinstance(k, p.ReqPackage) for k in t1.keys()])
assert all([isinstance(v, p.DistPackage) for v in p.flatten(t1.values())])
# testing reversal of ReversedPackageDAG instance
expected = {'a': ['b', 'c'],
'b': ['d'],
'c': ['d', 'e'],
'd': ['e'],
'e': [],
'f': ['b'],
'g': ['e', 'f']}
t2 = t1.reverse()
assert isinstance(t2, p.PackageDAG)
assert sort_map_values(expected) == sort_map_values(dag_to_dict(t2))
assert all([isinstance(k, p.DistPackage) for k in t2.keys()])
assert all([isinstance(v, p.ReqPackage) for v in p.flatten(t2.values())])
# Tests for Package classes
#
# Note: For all render methods, we are only testing for frozen=False
# as mocks with frozen=True are a lot more complicated
def test_DistPackage__render_as_root():
foo = mock.Mock(key='foo', project_name='foo', version='20.4.1')
dp = p.DistPackage(foo)
is_frozen = False
assert 'foo==20.4.1' == dp.render_as_root(is_frozen)
def test_DistPackage__render_as_branch():
foo = mock.Mock(key='foo', project_name='foo', version='20.4.1')
bar = mock.Mock(key='bar', project_name='bar', version='4.1.0')
bar_req = mock.Mock(key='bar',
project_name='bar',
version='4.1.0',
specs=[('>=', '4.0')])
rp = p.ReqPackage(bar_req, dist=bar)
dp = p.DistPackage(foo).as_parent_of(rp)
is_frozen = False
assert 'foo==20.4.1 [requires: bar>=4.0]' == dp.render_as_branch(is_frozen)
def test_DistPackage__as_parent_of():
foo = mock.Mock(key='foo', project_name='foo', version='20.4.1')
dp = p.DistPackage(foo)
assert dp.req is None
bar = mock.Mock(key='bar', project_name='bar', version='4.1.0')
bar_req = mock.Mock(key='bar',
project_name='bar',
version='4.1.0',
specs=[('>=', '4.0')])
rp = p.ReqPackage(bar_req, dist=bar)
dp1 = dp.as_parent_of(rp)
assert dp1._obj == dp._obj
assert dp1.req is rp
dp2 = dp.as_parent_of(None)
assert dp2 is dp
def test_DistPackage__as_dict():
foo = mock.Mock(key='foo', project_name='foo', version='1.3.2b1')
dp = p.DistPackage(foo)
result = dp.as_dict()
expected = {'key': 'foo',
'package_name': 'foo',
'installed_version': '1.3.2b1'}
assert expected == result
def test_ReqPackage__render_as_root():
bar = mock.Mock(key='bar', project_name='bar', version='4.1.0')
bar_req = mock.Mock(key='bar',
project_name='bar',
version='4.1.0',
specs=[('>=', '4.0')])
rp = p.ReqPackage(bar_req, dist=bar)
is_frozen = False
assert 'bar==4.1.0' == rp.render_as_root(is_frozen)
def test_ReqPackage__render_as_branch():
bar = mock.Mock(key='bar', project_name='bar', version='4.1.0')
bar_req = mock.Mock(key='bar',
project_name='bar',
version='4.1.0',
specs=[('>=', '4.0')])
rp = p.ReqPackage(bar_req, dist=bar)
is_frozen = False
assert 'bar [required: >=4.0, installed: 4.1.0]' == rp.render_as_branch(is_frozen)
def test_ReqPackage__as_dict():
bar = mock.Mock(key='bar', project_name='bar', version='4.1.0')
bar_req = mock.Mock(key='bar',
project_name='bar',
version='4.1.0',
specs=[('>=', '4.0')])
rp = p.ReqPackage(bar_req, dist=bar)
result = rp.as_dict()
expected = {'key': 'bar',
'package_name': 'bar',
'installed_version': '4.1.0',
'required_version': '>=4.0'}
assert expected == result
# Tests for render_text
#
# @NOTE: These tests use mocked tree and it's not easy to test for
# frozen=True with mocks. Hence those tests are covered only in
# end-to-end tests. Check the ./e2e-tests script.
@pytest.mark.parametrize(
"list_all,reverse,expected_output",
[
(
True,
False,
[
'a==3.4.0',
' - b [required: >=2.0.0, installed: 2.3.1]',
' - d [required: >=2.30,<2.42, installed: 2.35]',
' - e [required: >=0.9.0, installed: 0.12.1]',
' - c [required: >=5.7.1, installed: 5.10.0]',
' - d [required: >=2.30, installed: 2.35]',
' - e [required: >=0.9.0, installed: 0.12.1]',
' - e [required: >=0.12.1, installed: 0.12.1]',
'b==2.3.1',
' - d [required: >=2.30,<2.42, installed: 2.35]',
' - e [required: >=0.9.0, installed: 0.12.1]',
'c==5.10.0',
' - d [required: >=2.30, installed: 2.35]',
' - e [required: >=0.9.0, installed: 0.12.1]',
' - e [required: >=0.12.1, installed: 0.12.1]',
'd==2.35',
' - e [required: >=0.9.0, installed: 0.12.1]',
'e==0.12.1',
'f==3.1',
' - b [required: >=2.1.0, installed: 2.3.1]',
' - d [required: >=2.30,<2.42, installed: 2.35]',
' - e [required: >=0.9.0, installed: 0.12.1]',
'g==6.8.3rc1',
' - e [required: >=0.9.0, installed: 0.12.1]',
' - f [required: >=3.0.0, installed: 3.1]',
' - b [required: >=2.1.0, installed: 2.3.1]',
' - d [required: >=2.30,<2.42, installed: 2.35]',
' - e [required: >=0.9.0, installed: 0.12.1]'
]
),
(
True,
True,
[
'a==3.4.0',
'b==2.3.1',
' - a==3.4.0 [requires: b>=2.0.0]',
' - f==3.1 [requires: b>=2.1.0]',
' - g==6.8.3rc1 [requires: f>=3.0.0]',
'c==5.10.0',
' - a==3.4.0 [requires: c>=5.7.1]',
'd==2.35',
' - b==2.3.1 [requires: d>=2.30,<2.42]',
' - a==3.4.0 [requires: b>=2.0.0]',
' - f==3.1 [requires: b>=2.1.0]',
' - g==6.8.3rc1 [requires: f>=3.0.0]',
' - c==5.10.0 [requires: d>=2.30]',
' - a==3.4.0 [requires: c>=5.7.1]',
'e==0.12.1',
' - c==5.10.0 [requires: e>=0.12.1]',
' - a==3.4.0 [requires: c>=5.7.1]',
' - d==2.35 [requires: e>=0.9.0]',
' - b==2.3.1 [requires: d>=2.30,<2.42]',
' - a==3.4.0 [requires: b>=2.0.0]',
' - f==3.1 [requires: b>=2.1.0]',
' - g==6.8.3rc1 [requires: f>=3.0.0]',
' - c==5.10.0 [requires: d>=2.30]',
' - a==3.4.0 [requires: c>=5.7.1]',
' - g==6.8.3rc1 [requires: e>=0.9.0]',
'f==3.1',
' - g==6.8.3rc1 [requires: f>=3.0.0]',
'g==6.8.3rc1'
]
),
(
False,
False,
[
'a==3.4.0',
' - b [required: >=2.0.0, installed: 2.3.1]',
' - d [required: >=2.30,<2.42, installed: 2.35]',
' - e [required: >=0.9.0, installed: 0.12.1]',
' - c [required: >=5.7.1, installed: 5.10.0]',
' - d [required: >=2.30, installed: 2.35]',
' - e [required: >=0.9.0, installed: 0.12.1]',
' - e [required: >=0.12.1, installed: 0.12.1]',
'g==6.8.3rc1',
' - e [required: >=0.9.0, installed: 0.12.1]',
' - f [required: >=3.0.0, installed: 3.1]',
' - b [required: >=2.1.0, installed: 2.3.1]',
' - d [required: >=2.30,<2.42, installed: 2.35]',
' - e [required: >=0.9.0, installed: 0.12.1]',
]
),
(
False,
True,
[
'e==0.12.1',
' - c==5.10.0 [requires: e>=0.12.1]',
' - a==3.4.0 [requires: c>=5.7.1]',
' - d==2.35 [requires: e>=0.9.0]',
' - b==2.3.1 [requires: d>=2.30,<2.42]',
' - a==3.4.0 [requires: b>=2.0.0]',
' - f==3.1 [requires: b>=2.1.0]',
' - g==6.8.3rc1 [requires: f>=3.0.0]',
' - c==5.10.0 [requires: d>=2.30]',
' - a==3.4.0 [requires: c>=5.7.1]',
' - g==6.8.3rc1 [requires: e>=0.9.0]',
]
)
]
)
def test_render_text(capsys, list_all, reverse, expected_output):
tree = t.reverse() if reverse else t
p.render_text(tree, list_all=list_all, frozen=False)
captured = capsys.readouterr()
assert '\n'.join(expected_output).strip() == captured.out.strip()
# Tests for graph outputs
def test_render_pdf():
output = p.dump_graphviz(t, output_format='pdf')
@contextmanager
def redirect_stdout(new_target):
old_target, sys.stdout = sys.stdout, new_target
try:
yield new_target
finally:
sys.stdout = old_target
with NamedTemporaryFile(delete=True) as f:
with redirect_stdout(f):
p.print_graphviz(output)
rf = open(f.name, 'rb')
assert b'%PDF' == rf.read()[:4]
# @NOTE: rf is not closed to avoid "bad filedescriptor" error
def test_render_svg(capsys):
output = p.dump_graphviz(t, output_format='svg')
p.print_graphviz(output)
out, _ = capsys.readouterr()
assert out.startswith('')
# Test for conflicting deps
@pytest.mark.parametrize(
"mpkgs,expected_keys,expected_output",
[
(
{
('a', '1.0.1'): [('b', [('>=', '2.3.0')])],
('b', '1.9.1'): []
},
{'a': ['b']},
[
'Warning!!! Possibly conflicting dependencies found:',
'* a==1.0.1',
' - b [required: >=2.3.0, installed: 1.9.1]'
]
),
(
{
('a', '1.0.1'): [('c', [('>=', '9.4.1')])],
('b', '2.3.0'): [('c', [('>=', '7.0')])],
('c', '8.0.1'): []
},
{'a': ['c']},
[
'Warning!!! Possibly conflicting dependencies found:',
'* a==1.0.1',
' - c [required: >=9.4.1, installed: 8.0.1]'
]
),
(
{
('a', '1.0.1'): [('c', [('>=', '9.4.1')])],
('b', '2.3.0'): [('c', [('>=', '9.4.0')])]
},
{'a': ['c'], 'b': ['c']},
[
'Warning!!! Possibly conflicting dependencies found:',
'* a==1.0.1',
' - c [required: >=9.4.1, installed: ?]',
'* b==2.3.0',
' - c [required: >=9.4.0, installed: ?]'
]
),
(
{
('a', '1.0.1'): [('c', [('>=', '9.4.1')])],
('b', '2.3.0'): [('c', [('>=', '7.0')])],
('c', '9.4.1'): []
},
{},
[]
)
]
)
def test_conflicting_deps(capsys, mpkgs, expected_keys, expected_output):
tree = mock_PackageDAG(mpkgs)
result = p.conflicting_deps(tree)
result_keys = {k.key: [v.key for v in vs]
for k, vs in result.items()}
assert expected_keys == result_keys
p.render_conflicts_text(result)
captured = capsys.readouterr()
assert '\n'.join(expected_output).strip() == captured.err.strip()
# Tests for cyclic deps
@pytest.mark.parametrize(
"mpkgs,expected_keys,expected_output",
[
(
{
('a', '1.0.1'): [('b', [('>=', '2.0.0')])],
('b', '2.3.0'): [('a', [('>=', '1.0.1')])],
('c', '4.5.0'): [('d', [('==', '2.0')])],
('d', '2.0'): []
},
[('a', 'b', 'a'), ('b', 'a', 'b')],
[
'Warning!! Cyclic dependencies found:',
'* b => a => b',
'* a => b => a'
]
),
( # if a dependency isn't installed, cannot verify cycles
{
('a', '1.0.1'): [('b', [('>=', '2.0.0')])],
},
[],
[] # no output expected
)
]
)
def test_cyclic_deps(capsys, mpkgs, expected_keys, expected_output):
tree = mock_PackageDAG(mpkgs)
result = p.cyclic_deps(tree)
result_keys = [(a.key, b.key, c.key) for (a, b, c) in result]
assert sorted(expected_keys) == sorted(result_keys)
p.render_cycles_text(result)
captured = capsys.readouterr()
assert '\n'.join(expected_output).strip() == captured.err.strip()
# Tests for the argparse parser
def test_parser_default():
parser = p.get_parser()
args = parser.parse_args([])
assert not args.json
assert args.output_format is None
def test_parser_j():
parser = p.get_parser()
args = parser.parse_args(['-j'])
assert args.json
assert args.output_format is None
def test_parser_json():
parser = p.get_parser()
args = parser.parse_args(['--json'])
assert args.json
assert args.output_format is None
def test_parser_json_tree():
parser = p.get_parser()
args = parser.parse_args(['--json-tree'])
assert args.json_tree
assert not args.json
assert args.output_format is None
def test_parser_pdf():
parser = p.get_parser()
args = parser.parse_args(['--graph-output', 'pdf'])
assert args.output_format == 'pdf'
assert not args.json
def test_parser_svg():
parser = p.get_parser()
args = parser.parse_args(['--graph-output', 'svg'])
assert args.output_format == 'svg'
assert not args.json
@pytest.mark.parametrize("args_joined", [True, False])
def test_custom_interpreter(tmp_path, monkeypatch, capfd, args_joined):
result = virtualenv.cli_run([str(tmp_path), "--activators", ""])
cmd = [sys.executable]
cmd += ["--python={}".format(result.creator.exe)] if args_joined else ["--python", str(result.creator.exe)]
monkeypatch.setattr(sys, "argv", cmd)
p.main()
out, _ = capfd.readouterr()
found = {i.split("==")[0] for i in out.splitlines()}
implementation = platform.python_implementation()
if implementation == "CPython":
expected = {"pip", "setuptools", "wheel"}
elif implementation == "PyPy":
expected = {"cffi", "greenlet", "pip", "readline", "setuptools", "wheel"}
else:
raise ValueError(implementation)
assert found == expected, out
monkeypatch.setattr(sys, "argv", cmd + ["--graph-output", "something"])
with pytest.raises(SystemExit) as context:
p.main()
out, err = capfd.readouterr()
assert context.value.code == 1
assert not out
assert err == "graphviz functionality is not supported when querying" " non-host python\n"
pipdeptree-2.2.0/CHANGES.md 0000644 0001750 0001750 00000013766 14131341150 014602 0 ustar nilesh nilesh Changelog
=========
2.2.0
-----
* Fix pipdeptree to work with pip version 21.3. The _internal pip api
that was being used earlier is now replaced with new functions. (PR
#154)
2.1.0
-----
* JSON output is sorted alphabetically to make it deterministic
* Fix --freeze option due to breaking changes in pip's internal api in
version > 21.1.1
* Include license file in dist package
2.0.0
-----
* Support for running in the context of a virtualenv (without
installing pipdeptree inside the virtualenv)
* Avoid crash when rendering cyclic dependencies
* Fix graphviz (dot file) output
* Handle a (rare) case while guessing version of a package
* Migrate from travisCI to Github workflows
* Improve integration tests
2.0.0b1 (beta version)
----------------------
* In this first beta release targeting `2.0.0`, the underlying code is
heavily refactored to make different CLI options work well with each
other. This was a serious limitation in older version `<=1.0.0`
which made it difficult to extend the tool.
For more information about the plans for 2.0.0 release, please check
`docs/v2beta-opts.org` file.
* The `--reverse`, `--packages` and `--exclude` flags now work
with `--json-tree` and `--graph-output`
* Dropped support for python `3.3` and added support for python
`3.7` and `3.8`
* Another problem with older version was that tests setup was
convoluted and involved loading packages pickled from one env into
the current env (in which tests are run). Moreover there was no
separation between unit tests and integration tests (flaky)
* Tests have been separated into 2 suites (1) unit tests that
totally rely on mock objects and run on every commit (
travis.ci) and (2) end-to-end tests that need to be run
manually.
* The test setup for end-to-end tests has been greatly simplified
although the "flakyness"" still remains because these tests are
run against unpinned versions of `pip`. However this is by
design because we want to know when `pipdeptree` fails with a
new version of `pip`.
* Move continuous integration from Travis to Github Actions.
1.0.0
-----
* Use `pkg_resources` vendored with `pip`.
* Besides this, there's no other change in this major version release.
0.13.2
------
* Fixed call to `FrozenRequirement.to_dist` to handle changes to the
internal api of pip version 19.0. The api change is because
dependency links support has been removed in pip 19.0
See more:
- https://github.com/pypa/pip/pull/6060
- https://github.com/pypa/pip/pull/5881/commits/46ffb13f13f69c509fd253329da49889008f8e23
0.13.1
------
* Fixed import after changes in pip._internal introduced in pip
version 18.1
0.13.0
------
* Added `--exclude` option to exclude packages specified as CSV
* In case of multiple version specs eg. =y, fix the order to
ensure consistent output. The sorting is naive - puts the '>' prior
to '<', and '!'.
* [Developer affecting] Updated dependencies in test environments, thereby
fixing the long standing issue of inconsistent test behaviour.
0.12.1
------
* Fix import of 'FrozenRequirement' for pip 10.0.0
0.12.0
------
* Changes to make pipdeptree work with pip 10.0.0. This change is
backward compatible.
0.11.0
------
* Added support for nested json output (`--json-tree` flag). Behaviour
of `--json` stays the same.
* Test environments have been updated to fix the builds.
0.10.1
------
* Fixed change of behaviour due to support for ``--json`` and
``--packages`` together. PR #65 was reverted for this.
0.10.0
------
* Dropped support for Python 2.6.
* ``--json`` and ``--packages`` options can now be used together.
* Fixed binary graphviz output on Python 3
0.9.0
-----
* Support for visualizing dependency tree of packages using Graphviz
in various formats.
* Support to consider only packages installed in the user directory.
* Fix the output to use a better term, "Any" instead of "None" if a
dependency doesn't need to be of a specific version.
* CLI option to print version.
0.8.0
-----
* Use pip's list of excluded default packages. This means that the
``pipdeptree`` package itself is no longer excluded and will appear
in the output tree.
* Fix the bug that caused a package to appear in conflicting deps
although it's installed version could be guessed.
0.7.0
-----
* Fix for a bug in reverse mode.
* Alphabetical sorting of packages in the output.
* Fallback to guess installed version of packages "skipped" by pip.
0.6.0
-----
* Better checking for possibly "confusing" dependencies, hence the
word "confusing" in the warning message is now replaced with
"coflicting" [PR#37]
* Fix a bug when rendering dependencies of packages [PR#38]
* The ``--nowarn`` flag is now replaced with ``--warn`` with
'silence', 'suppress' and 'fail' as possible values, thus giving
more control over what should happen when there are warnings. The
default behaviour (ie. when the flag is not specified) remains the
same. [PR#39]
* Fixes for Python 3.5 support [PR#40]
0.5.0
-----
* Add `--reverse` flag to show the dependency tree upside down.
* Add `--packages` flag to show only select packages in output.
* Add `--json` flag to output dependency tree as json that may be used
by external tools.
0.4.3
-----
* Add python support classifiers to setup.py
* Include license and changelog in distribution tar ball
* Removed bullets from output of pipdeptree if the `freeze` (-f) flag
is set.
* Changes related to test setup and travis-ci integration.
0.4.2
-----
* Fix Python 3.x incompatibility (`next()` instead of `.next()`)
* Suppress error if a dep is in skipped packages
0.4.1
-----
* Fix: Show warning about cyclic deps only if found
0.4
---
* Python 2.6 compatibility
* Fix infinite recursion in case of cyclic dependencies
* Show warnings about cyclic dependencies
* Travis integration and other improvements
0.3
---
* Add `--freeze` flag
* Warn about possible confusing dependencies
* Some minor help text and README fixes
0.2
---
* Minor fixes
0.1
---
First version
pipdeptree-2.2.0/pytest.ini 0000644 0001750 0001750 00000001065 14131341150 015226 0 ustar nilesh nilesh [pytest]
norecursedirs = build docs/_build *.egg .tox *.venv profiles
addopts =
# --verbose
--tb=short
# Turn on --capture to have brief, less noisy output
# You will only see output if the test fails
# Use --capture no if you want to see it all or have problems debugging
# --capture=fd
# --capture=no
# show extra test summary info as specified by chars (f)ailed, (E)error, (s)skipped, (x)failed, (X)passed.
-rfEsxX
# --junitxml=junit.xml
# --cov=pipdeptree --cov-report=xml --cov-report=html --cov-report=term-missing
pipdeptree-2.2.0/.gitignore 0000644 0001750 0001750 00000000210 14131341150 015154 0 ustar nilesh nilesh *.pyc
__pycache__
*~
*.egg-info/
build/
dist/
.tox/
tests/virtualenvs/equimapper/
.coverage
coverage.xml
htmlcov/
.cache/
.pytest_cache/ pipdeptree-2.2.0/LICENSE 0000644 0001750 0001750 00000002063 14131341150 014201 0 ustar nilesh nilesh Copyright (c) 2015 Vineet Naik (naikvin@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
pipdeptree-2.2.0/.github/ 0000755 0001750 0001750 00000000000 14131341150 014533 5 ustar nilesh nilesh pipdeptree-2.2.0/.github/workflows/ 0000755 0001750 0001750 00000000000 14131341150 016570 5 ustar nilesh nilesh pipdeptree-2.2.0/.github/workflows/check.yml 0000644 0001750 0001750 00000002646 14131341150 020400 0 ustar nilesh nilesh name: check
on:
push:
pull_request:
schedule:
- cron: "0 8 * * *"
jobs:
test:
name: Test on ${{ matrix.py }} under ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- Ubuntu-latest
py:
- 3.9
- 3.8
- 3.7
- 3.6
- 3.5
- 2.7
- pypy3
- pypy2
steps:
- name: Setup graphviz
uses: ts-graphviz/setup-graphviz@v1
- name: Setup python for tox
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install tox
run: python -m pip install tox
- name: Setup python for test ${{ matrix.py }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.py }}
- uses: actions/checkout@v2
- name: Pick tox environment to run
run: |
import os
import platform
import sys
major, minor, impl = sys.version_info[0], sys.version_info[1], platform.python_implementation()
toxenv = ("py" if impl == "CPython" else "pypy") + ("{}{}".format(major, minor) if impl == "CPython" else ("3" if major == 3 else ""))
env_file = open(os.environ['GITHUB_ENV'], "a")
env_file.write("TOXENV={}\n".format(toxenv))
env_file.close()
shell: python
- name: Setup test suite
run: tox -vv --notest
- name: Run test suite
run: tox --skip-pkg-install
pipdeptree-2.2.0/MANIFEST.in 0000644 0001750 0001750 00000000032 14131341150 014724 0 ustar nilesh nilesh include LICENSE CHANGES.md pipdeptree-2.2.0/docs/ 0000755 0001750 0001750 00000000000 14131341150 014123 5 ustar nilesh nilesh pipdeptree-2.2.0/docs/v2beta-opts.org 0000644 0001750 0001750 00000007614 14131341150 017012 0 ustar nilesh nilesh * Options in version 0.x v/s 2.x (upcoming release)
Until version 0.13.2, the some of the options that pipdeptree supports
didn't work in combination with other options. In fact this was the
primary reason behind refactoring the code.
The upcoming version 2.x plans to fix this as shown in the tables below.
Note: The changes for upcoming 2.x release can be found in the
~v2beta~ branch.
** Until version 0.13.2
| *Features* | all | local-only | user-only | freeze | warn | reverse | packages | json | json-tree | graph-output |
|--------------+-----+------------+-----------+--------+------+---------+----------+------+-----------+--------------|
| | | | | | | | | | | |
| all | | | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| local-only | | | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| user-only | | | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| freeze | ✓ | ✓ | ✓ | ✓ | | ✓ | ✓ | | | |
| warn | ✓ | ✓ | ✓ | | | | | ✗ | ✗ | |
| reverse | ✓ | ✓ | ✓ | ✓ | | | ✓ | | ✗ | ✗ |
| packages | ✓ | ✓ | ✓ | ✓ | | ✓ | | ✗ | ✗ | ✗ |
| json | ✓ | ✓ | ✓ | | ✗ | | ✗ | | | |
| json-tree | ✓ | ✓ | ✓ | | ✗ | ✗ | ✗ | | | |
| graph-output | ✓ | ✓ | ✓ | | | ✗ | ✗ | | | |
** Plan for version 2.0.0 (work in progress)
| *Features* | all | local-only | user-only | freeze | warn | reverse | packages | json | json-tree | graph-output | |
|--------------+-----+------------+-----------+--------+----------+----------+----------+----------+-----------+--------------+---|
| | | | | | | | | | | | |
| all | | | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| local-only | | | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| user-only | | | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| freeze | ✓ | ✓ | ✓ | ✓ | | ✓ | ✓ | | | | |
| warn | ✓ | ✓ | ✓ | | | | | ✓ (todo) | ✓ (todo) | | |
| reverse | ✓ | ✓ | ✓ | ✓ | | | ✓ | | ✓ (done) | ✓ (done) | |
| packages | ✓ | ✓ | ✓ | ✓ | | ✓ | | ✓ (done) | ✓ (done) | ✓ (done) | |
| json | ✓ | ✓ | ✓ | | ✓ (todo) | | ✓ (done) | | | | |
| json-tree | ✓ | ✓ | ✓ | | ✓ (todo) | ✓ (done) | ✓ (done) | | | | |
| graph-output | ✓ | ✓ | ✓ | | | ✓ (done) | ✓ (done) | | | | |
pipdeptree-2.2.0/docs/twine-pdt.png 0000644 0001750 0001750 00000276440 14131341150 016561 0 ustar nilesh nilesh PNG
IHDR c sRGB @ IDATx USI%Rf
2,)C^3![̳D""C!C(ʜ!CB2}={99wa(S tA]' K@U0R#" " "L>sox?իWׯo/O55\ӧ
&oRE@D@D@D@D@D +$ve*$O?oQ2[6|/*iRK-V,lխ[DBCJ,Z]v>oݺ-2RƘVژ4oƦNj