2014-05-11 06:42:48 +00:00
|
|
|
from __future__ import print_function
|
2014-02-02 16:27:58 +00:00
|
|
|
import sys
|
2014-07-02 17:49:48 +00:00
|
|
|
from itertools import chain, tee
|
2014-05-11 06:42:48 +00:00
|
|
|
from collections import defaultdict
|
2014-02-02 16:27:58 +00:00
|
|
|
import argparse
|
|
|
|
|
|
|
|
import pip
|
|
|
|
|
2014-02-05 16:13:05 +00:00
|
|
|
|
2015-08-03 15:56:16 +00:00
|
|
|
__version__ = '0.4.3'
|
2015-08-03 15:55:24 +00:00
|
|
|
|
|
|
|
|
2014-02-02 16:27:58 +00:00
|
|
|
flatten = chain.from_iterable
|
|
|
|
|
|
|
|
|
2015-11-07 17:05:24 +00:00
|
|
|
def build_dist_index(pkgs):
|
|
|
|
"""Build an index pkgs by their key as a dict.
|
2014-02-02 16:27:58 +00:00
|
|
|
|
2015-11-07 17:05:24 +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
|
|
|
|
|
|
|
"""
|
2015-11-07 17:05:24 +00:00
|
|
|
return {p.key: DistPackage(p) for p in pkgs}
|
2014-02-02 16:27:58 +00:00
|
|
|
|
|
|
|
|
2015-11-07 17:05:24 +00:00
|
|
|
def construct_tree(index):
|
|
|
|
"""Construct tree representation of the pkgs from the index.
|
2014-02-02 16:27:58 +00:00
|
|
|
|
2015-11-07 17:05:24 +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
|
|
|
|
2015-11-07 17:05:24 +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
|
|
|
|
|
|
|
"""
|
2015-11-07 17:05:24 +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
|
|
|
|
|
|
|
|
2015-11-07 17:05:24 +00:00
|
|
|
class Package(object):
|
|
|
|
"""Abstract class for wrappers around objects that pip returns.
|
2014-02-02 16:27:58 +00:00
|
|
|
|
2015-11-07 17:05:24 +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
|
|
|
|
|
|
|
"""
|
|
|
|
|
2015-11-07 17:05:24 +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
|
|
|
|
2015-11-07 17:05:24 +00:00
|
|
|
def render_as_root(self, frozen):
|
|
|
|
return NotImplementedError
|
2014-05-10 18:59:06 +00:00
|
|
|
|
2015-11-07 17:05:24 +00:00
|
|
|
def render_as_branch(self, parent, frozen):
|
|
|
|
return NotImplementedError
|
2014-05-10 18:59:06 +00:00
|
|
|
|
2015-11-07 17:05:24 +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
|
|
|
|
2015-11-07 17:05:24 +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)
|
2014-05-10 18:50:15 +00:00
|
|
|
|
2015-11-07 17:05:24 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return '<{0}("{1}")>'.format(self.__class__.__name__, self.key)
|
2014-05-10 18:50:15 +00:00
|
|
|
|
2014-05-10 18:59:06 +00:00
|
|
|
|
2015-11-07 17:05:24 +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
|
|
|
|
|
|
|
"""
|
2015-11-07 17:05:24 +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]
|
2014-05-10 18:50:15 +00:00
|
|
|
|
2015-11-07 17:05:24 +00:00
|
|
|
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
|
2014-05-10 18:50:15 +00:00
|
|
|
|
2015-11-07 17:05:24 +00:00
|
|
|
lines = flatten([aux(p) for p in nodes])
|
|
|
|
return '\n'.join(lines)
|
2014-02-02 16:27:58 +00:00
|
|
|
|
2014-05-11 06:42:48 +00:00
|
|
|
|
2015-11-07 17:05:24 +00:00
|
|
|
def confusing_deps(tree):
|
2014-05-11 06:42:48 +00:00
|
|
|
"""Returns group of dependencies that are possibly confusing
|
|
|
|
|
|
|
|
eg. if pkg1 requires pkg3>=1.0 and pkg2 requires pkg3>=1.0,<=2.0
|
|
|
|
|
2015-11-07 17:05:24 +00:00
|
|
|
:param dict tree: the requirements tree
|
2014-05-11 06:42:48 +00:00
|
|
|
:returns: groups of dependencies paired with their top level pkgs
|
|
|
|
:rtype: list of list of pairs
|
|
|
|
|
|
|
|
"""
|
2015-11-07 17:05:24 +00:00
|
|
|
def has_multi_versions(req_pkgs):
|
|
|
|
return len(set(r.version_spec for r in req_pkgs)) > 1
|
|
|
|
|
2014-06-22 17:20:26 +00:00
|
|
|
deps = defaultdict(list)
|
2015-11-07 17:05:24 +00:00
|
|
|
for p, rs in tree.items():
|
2014-05-11 06:42:48 +00:00
|
|
|
for r in rs:
|
|
|
|
deps[r.key].append((p, r))
|
2014-06-26 11:26:44 +00:00
|
|
|
return [ps for r, ps in deps.items()
|
2014-05-11 06:42:48 +00:00
|
|
|
if len(ps) > 1
|
|
|
|
and has_multi_versions(d for p, d in ps)]
|
|
|
|
|
|
|
|
|
2015-11-07 17:05:24 +00:00
|
|
|
def cyclic_deps(tree):
|
2014-06-22 17:20:26 +00:00
|
|
|
"""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
|
|
|
|
|
|
|
|
"""
|
2015-11-07 17:05:24 +00:00
|
|
|
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)])
|
2014-06-14 01:57:10 +00:00
|
|
|
else:
|
2015-11-07 17:05:24 +00:00
|
|
|
for cycle in aux(c, chain=chain+[c.project_name]):
|
2014-06-14 01:57:10 +00:00
|
|
|
yield cycle
|
|
|
|
|
2015-11-07 17:05:24 +00:00
|
|
|
for cycle in flatten([aux(n, chain=[]) for n in nodes]):
|
2014-06-14 01:57:10 +00:00
|
|
|
yield cycle
|
|
|
|
|
|
|
|
|
2014-07-02 17:49:48 +00:00
|
|
|
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)
|
2014-07-02 17:49:48 +00:00
|
|
|
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'
|
|
|
|
))
|
2014-05-10 18:50:15 +00:00
|
|
|
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
|
|
|
))
|
2014-05-11 06:42:48 +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()
|
2014-05-11 06:42:48 +00:00
|
|
|
|
2014-02-05 16:12:43 +00:00
|
|
|
default_skip = ['setuptools', 'pip', 'python', 'distribute']
|
2014-04-05 14:26:11 +00:00
|
|
|
skip = default_skip + ['pipdeptree']
|
2014-05-11 06:42:48 +00:00
|
|
|
pkgs = pip.get_installed_distributions(local_only=args.local_only,
|
|
|
|
skip=skip)
|
|
|
|
|
2015-11-07 17:05:24 +00:00
|
|
|
dist_index = build_dist_index(pkgs)
|
|
|
|
tree = construct_tree(dist_index)
|
2014-05-11 06:42:48 +00:00
|
|
|
|
|
|
|
# show warnings about possibly confusing deps if found and
|
|
|
|
# warnings are enabled
|
|
|
|
if not args.nowarn:
|
2015-11-07 17:05:24 +00:00
|
|
|
confusing = confusing_deps(tree)
|
2014-05-11 06:42:48 +00:00
|
|
|
if confusing:
|
|
|
|
print('Warning!!! Possible confusing dependencies found:', file=sys.stderr)
|
|
|
|
for xs in confusing:
|
|
|
|
for i, (p, d) in enumerate(xs):
|
2014-11-12 08:35:02 +00:00
|
|
|
if d.key in skip:
|
|
|
|
continue
|
2015-11-07 17:05:24 +00:00
|
|
|
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}'
|
2014-05-11 06:42:48 +00:00
|
|
|
print(tmpl.format(pkg, req), file=sys.stderr)
|
|
|
|
print('-'*72, file=sys.stderr)
|
|
|
|
|
2015-11-07 17:05:24 +00:00
|
|
|
is_empty, cyclic = peek_into(cyclic_deps(tree))
|
2014-07-02 17:49:48 +00:00
|
|
|
if not is_empty:
|
2014-06-22 17:21:10 +00:00
|
|
|
print('Warning!!! Cyclic dependencies found:', file=sys.stderr)
|
2014-06-14 01:57:10 +00:00
|
|
|
for xs in cyclic:
|
2014-06-22 17:21:10 +00:00
|
|
|
print('- {0}'.format(xs), file=sys.stderr)
|
|
|
|
print('-'*72, file=sys.stderr)
|
2014-06-14 01:57:10 +00:00
|
|
|
|
2015-11-07 17:05:24 +00:00
|
|
|
tree = render_tree(tree, list_all=args.all, frozen=args.freeze)
|
2014-05-11 06:42:48 +00:00
|
|
|
print(tree)
|
2014-02-02 16:27:58 +00:00
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
sys.exit(main())
|