pipdeptree/pipdeptree.py

326 lines
10 KiB
Python
Raw Normal View History

from __future__ import print_function
2014-02-02 16:27:58 +00:00
import sys
from itertools import chain, tee
from collections import defaultdict
2014-02-02 16:27:58 +00:00
import argparse
import pip
2014-02-05 16:13:05 +00:00
__version__ = '0.4.3'
2014-02-02 16:27:58 +00:00
flatten = chain.from_iterable
def build_dist_index(pkgs):
"""Build an index pkgs by their key as a dict.
2014-02-02 16:27:58 +00:00
:param list pkgs: list of pkg_resources.Distribution instances
:returns: index of the pkgs by the pkg key
:rtype: dict
2014-02-02 16:27:58 +00:00
"""
return {p.key: DistPackage(p) for p in pkgs}
2014-02-02 16:27:58 +00:00
def construct_tree(index):
"""Construct tree representation of the pkgs from the index.
2014-02-02 16:27:58 +00:00
The keys of the dict representing the tree will be objects of type
DistPackage and the values will be list of ReqPackage objects.
2014-02-02 16:27:58 +00:00
:param dict index: dist index ie. index of pkgs by their keys
:returns: tree of pkgs and their dependencies
:rtype: dict
2014-02-02 16:27:58 +00:00
"""
return {p: [ReqPackage(r, index.get(r.key))
for r in p.requires()]
for p in index.values()}
2014-02-02 16:27:58 +00:00
class Package(object):
"""Abstract class for wrappers around objects that pip returns.
2014-02-02 16:27:58 +00:00
This class needs to be subclassed with implementations for
`render_as_root` and `render_as_branch` methods.
2014-02-02 16:27:58 +00:00
"""
def __init__(self, obj):
self._obj = obj
self.project_name = obj.project_name
self.key = obj.key
# an instance of every subclass of Package will have a
# DistPackage object associated with it. In case of
# DistPackage class, it will be the object itself.
self.dist = None
2014-02-02 16:27:58 +00:00
def render_as_root(self, frozen):
return NotImplementedError
2014-05-10 18:59:06 +00:00
def render_as_branch(self, parent, frozen):
return NotImplementedError
2014-05-10 18:59:06 +00:00
def render(self, parent=None, frozen=False):
if not parent:
return self.render_as_root(frozen)
else:
return self.render_as_branch(parent, frozen)
2014-05-10 18:59:06 +00:00
def frozen_repr(self):
if self.dist:
fr = pip.FrozenRequirement.from_dist(self.dist._obj, [])
return str(fr).strip()
else:
return self.project_name
def __getattr__(self, key):
return getattr(self._obj, key)
def __repr__(self):
return '<{0}("{1}")>'.format(self.__class__.__name__, self.key)
2014-05-10 18:59:06 +00:00
class DistPackage(Package):
"""Wrapper class for pkg_resources.Distribution instances"""
def __init__(self, obj):
super(DistPackage, self).__init__(obj)
# this itself is the associated dist package obj
self.dist = self
self.version_spec = None
def render_as_root(self, frozen):
if not frozen:
return '{0}=={1}'.format(self.project_name, self.version)
else:
return self.frozen_repr()
def render_as_branch(self, parent, _frozen):
parent_ver_spec = parent.version_spec
parent_str = parent.project_name
if parent_ver_spec:
parent_str += parent_ver_spec
return (
'{0}=={1} [requires: {2}]'
).format(self.project_name, self.version, parent_str)
def as_requirement(self):
return ReqPackage(self._obj.as_requirement(), dist=self)
class ReqPackage(Package):
"""Wrapper class for Requirements instance"""
def __init__(self, obj, dist=None):
super(ReqPackage, self).__init__(obj)
self.dist = dist
@property
def version_spec(self):
specs = self._obj.specs
return ''.join(specs[0]) if specs else None
@property
def installed_version(self):
# if the dist is None as in some cases, we don't know the
# installed version
return self.dist.version if self.dist else '?'
def render_as_root(self, frozen):
if not frozen:
return '{0}=={1}'.format(self.project_name, self.installed_version)
else:
return self.frozen_repr()
def render_as_branch(self, _parent, frozen):
if not frozen:
vers = []
if self.version_spec:
vers.append(('required', self.version_spec))
if self.dist:
vers.append(('installed', self.installed_version))
if not vers:
return self.key
ver_str = ', '.join(['{0}: {1}'.format(k, v) for k, v in vers])
return '{0} [{1}]'.format(self.project_name, ver_str)
else:
return self.render_as_root(frozen)
def render_tree(tree, list_all=True, frozen=False):
"""Convert to tree to string representation
: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: string representation of the tree
:rtype: str
2014-05-10 18:59:06 +00:00
"""
branch_keys = set(r.key for r in flatten(tree.values()))
nodes = tree.keys()
use_bullets = not frozen
key_tree = {k.key: v for k, v in tree.iteritems()}
get_children = lambda n: key_tree[n.key]
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):
if chain is None:
chain = [node.project_name]
node_str = node.render(parent, frozen)
if parent:
prefix = ' '*indent + ('-' if use_bullets else ' ') + ' '
node_str = prefix + node_str
result = [node_str]
# the dist attr for some ReqPackage could be None
# eg. testresources, setuptools which is a dependencies of
# some pkg but doesn't get listed in the result of
# pip.get_installed_distributions.
if node.dist:
children = [aux(c, node, indent=indent+2,
chain=chain+[c.project_name])
for c in get_children(node)
if c.project_name not in chain]
result += list(flatten(children))
return result
lines = flatten([aux(p) for p in nodes])
return '\n'.join(lines)
2014-02-02 16:27:58 +00:00
def confusing_deps(tree):
"""Returns group of dependencies that are possibly confusing
eg. if pkg1 requires pkg3>=1.0 and pkg2 requires pkg3>=1.0,<=2.0
:param dict tree: the requirements tree
:returns: groups of dependencies paired with their top level pkgs
:rtype: list of list of pairs
"""
def has_multi_versions(req_pkgs):
return len(set(r.version_spec for r in req_pkgs)) > 1
deps = defaultdict(list)
for p, rs in tree.items():
for r in rs:
deps[r.key].append((p, r))
return [ps for r, ps in deps.items()
if len(ps) > 1
and has_multi_versions(d for p, d in ps)]
def cyclic_deps(tree):
"""Generator that produces cyclic dependencies
:param list pkgs: pkg_resources.Distribution instances
:param dict pkg_index: mapping of pkgs with their respective keys
:returns: generator that yields str representation of cyclic
dependencies
:rtype: generator
"""
nodes = tree.keys()
key_tree = {k.key: v for k, v in tree.iteritems()}
get_children = lambda n: key_tree[n.key]
def aux(node, chain):
if node.dist:
for c in get_children(node):
if c.project_name in chain:
yield ' => '.join([str(p) for p in chain] + [str(c)])
else:
for cycle in aux(c, chain=chain+[c.project_name]):
yield cycle
for cycle in flatten([aux(n, chain=[]) for n in nodes]):
yield cycle
def peek_into(iterator):
"""Peeks into an iterator to check if it's empty
:param iterator: an iterator
:returns: tuple of boolean representing whether the iterator is
empty or not and the iterator itself.
:rtype: tuple
"""
a, b = tee(iterator)
is_empty = False
try:
2014-10-07 10:32:33 +00:00
next(a)
except StopIteration:
is_empty = True
return is_empty, b
2014-02-02 16:27:58 +00:00
def main():
2014-04-05 14:26:11 +00:00
parser = argparse.ArgumentParser(description=(
'Dependency tree of the installed python packages'
))
parser.add_argument('-f', '--freeze', action='store_true',
help='Print names so as to write freeze files')
2014-04-05 14:26:11 +00:00
parser.add_argument('-a', '--all', action='store_true',
help='list all deps at top level')
2014-02-02 16:27:58 +00:00
parser.add_argument('-l', '--local-only',
action='store_true', help=(
2014-02-06 17:07:42 +00:00
'If in a virtualenv that has global access '
'donot show globally installed packages'
2014-02-02 16:27:58 +00:00
))
parser.add_argument('-w', '--nowarn', action='store_true',
help=(
'Inhibit warnings about possibly '
'confusing packages'
))
2014-02-02 16:27:58 +00:00
args = parser.parse_args()
default_skip = ['setuptools', 'pip', 'python', 'distribute']
2014-04-05 14:26:11 +00:00
skip = default_skip + ['pipdeptree']
pkgs = pip.get_installed_distributions(local_only=args.local_only,
skip=skip)
dist_index = build_dist_index(pkgs)
tree = construct_tree(dist_index)
# show warnings about possibly confusing deps if found and
# warnings are enabled
if not args.nowarn:
confusing = confusing_deps(tree)
if confusing:
print('Warning!!! Possible confusing dependencies found:', file=sys.stderr)
for xs in confusing:
for i, (p, d) in enumerate(xs):
if d.key in skip:
continue
pkg = p.render_as_root(False)
req = d.render_as_branch(p, False)
2014-06-12 19:57:36 +00:00
tmpl = ' {0} -> {1}' if i > 0 else '* {0} -> {1}'
print(tmpl.format(pkg, req), file=sys.stderr)
print('-'*72, file=sys.stderr)
is_empty, cyclic = peek_into(cyclic_deps(tree))
if not is_empty:
print('Warning!!! Cyclic dependencies found:', file=sys.stderr)
for xs in cyclic:
print('- {0}'.format(xs), file=sys.stderr)
print('-'*72, file=sys.stderr)
tree = render_tree(tree, list_all=args.all, frozen=args.freeze)
print(tree)
2014-02-02 16:27:58 +00:00
return 0
if __name__ == '__main__':
sys.exit(main())