From a44baabaa5c2db3abfe6afbbc81e05ac914dfa0b Mon Sep 17 00:00:00 2001 From: Vineet Naik Date: Sat, 6 Aug 2016 18:39:32 +0530 Subject: [PATCH] Try guessing versions of pkgs not listed by pip * 'pip.get_installed_distributions' doesn't include some packages such as 'pip', 'setuptools' etc. so the installed versions for these is not available. This change tries to guess the installed version by importing the module and checking if the version is defined in '__version__' variable. * Another related change is that the 'required' and 'installed' versions will be shown for all intermediate packages. When 'required' is not specified, it will show 'None' and when 'installed' is not available, it will show '?'. This is to keep the output consistent with the confusing deps output. * Fix indentation in output. Kind of fixes #46. --- pipdeptree.py | 47 +++++++++++++++++++++++----------------- tests/test_pipdeptree.py | 12 +++++----- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/pipdeptree.py b/pipdeptree.py index 2971cdc..f7238f7 100644 --- a/pipdeptree.py +++ b/pipdeptree.py @@ -5,6 +5,7 @@ from collections import defaultdict, OrderedDict import argparse from operator import attrgetter import json +from importlib import import_module import pip import pkg_resources @@ -98,6 +99,23 @@ def reverse_tree(tree): return rtree +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: + return getattr(m, '__version__', default) + + class Package(object): """Abstract class for wrappers around objects that pip returns. @@ -209,9 +227,9 @@ class ReqPackage(Package): @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 '?' + if not self.dist: + return guess_version(self.key) + return self.dist.version def render_as_root(self, frozen): if not frozen: @@ -223,15 +241,10 @@ class ReqPackage(Package): def render_as_branch(self, 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) + return ( + '{0} [required: {1}, installed: {2}]' + ).format(self.project_name, self.version_spec, + self.installed_version) else: return self.render_as_root(frozen) @@ -276,7 +289,7 @@ def render_tree(tree, list_all=True, show_only=None, frozen=False): chain = [node.project_name] node_str = node.render(parent, frozen) if parent: - prefix = ' '*indent + ('-' if use_bullets else ' ') + ' ' + prefix = ' '*indent + ('- ' if use_bullets else '') node_str = prefix + node_str result = [node_str] children = [aux(c, node, indent=indent+2, @@ -422,13 +435,7 @@ def main(): pkg = p.render_as_root(False) print('* %s' % pkg, file=sys.stderr) for req in reqs: - if not req.dist: - req_str = ( - '{0} [required: {1}, ' - 'installed: ]' - ).format(req.project_name, req.version_spec) - else: - req_str = req.render_as_branch(p, False) + req_str = req.render_as_branch(False) print(' - %s' % req_str, file=sys.stderr) print('-'*72, file=sys.stderr) diff --git a/tests/test_pipdeptree.py b/tests/test_pipdeptree.py index 841e8ec..f6b8c7d 100644 --- a/tests/test_pipdeptree.py +++ b/tests/test_pipdeptree.py @@ -87,7 +87,7 @@ def test_ReqPackage_render_as_branch(): assert mks1.project_name == 'markupsafe' assert mks1.installed_version == '0.18' assert mks1.version_spec is None - assert mks1.render_as_branch(False) == 'markupsafe [installed: 0.18]' + assert mks1.render_as_branch(False) == 'markupsafe [required: None, installed: 0.18]' assert mks1.render_as_branch(True) == 'MarkupSafe==0.18' mks2 = find_req('markupsafe', 'mako') assert mks2.project_name == 'MarkupSafe' @@ -126,7 +126,7 @@ def test_render_tree_freeze(): line = line.replace('origin/HEAD', 'master') lines.add(line) assert 'Flask-Script==0.6.6' in lines - assert ' SQLAlchemy==0.9.1' in lines + assert ' SQLAlchemy==0.9.1' in lines # TODO! Fix the following failing test # assert '-e git+https://github.com/naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy-master' in lines assert 'itsdangerous==0.23' not in lines @@ -147,9 +147,9 @@ def test_render_tree_cyclic_dependency(): tree_str = render_tree(tree, list_all=True) lines = set(tree_str.split('\n')) assert 'CircularDependencyA==0.0.0' in lines - assert ' - CircularDependencyB [installed: 0.0.0]' in lines + assert ' - CircularDependencyB [required: None, installed: 0.0.0]' in lines assert 'CircularDependencyB==0.0.0' in lines - assert ' - CircularDependencyA [installed: 0.0.0]' in lines + assert ' - CircularDependencyA [required: None, installed: 0.0.0]' in lines def test_render_tree_freeze_cyclic_dependency(): @@ -157,9 +157,9 @@ def test_render_tree_freeze_cyclic_dependency(): tree_str = render_tree(tree, list_all=True, frozen=True) lines = set(tree_str.split('\n')) assert 'CircularDependencyA==0.0.0' in lines - assert ' CircularDependencyB==0.0.0' in lines + assert ' CircularDependencyB==0.0.0' in lines assert 'CircularDependencyB==0.0.0' in lines - assert ' CircularDependencyA==0.0.0' in lines + assert ' CircularDependencyA==0.0.0' in lines def test_conflicting_deps():