diff --git a/pickle_env.py b/pickle_env.py new file mode 100755 index 0000000..ad8ee69 --- /dev/null +++ b/pickle_env.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +# This is a small tool to create a pickle file for a set of packages for the +# purposes of writing tests + +import pickle +import sys + +import pip + + +def main(): + default_skip = ['setuptools', 'pip', 'python', 'distribute'] + skip = default_skip + ['pipdeptree'] + pkgs = pip.get_installed_distributions(local_only=True, skip=skip) + pickle.dump(pkgs, sys.stdout) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/pipdeptree.py b/pipdeptree.py index 9cca598..f9372b0 100644 --- a/pipdeptree.py +++ b/pipdeptree.py @@ -130,7 +130,10 @@ def render_tree(pkgs, pkg_index, req_map, list_all, non_top = set(r.key for r in flatten(req_map.values())) top = [p for p in pkgs if p.key not in non_top] - def aux(pkg, indent=0): + def aux(pkg, indent=0, chain=None): + if chain is None: + chain = [pkg.project_name] + # In this function, pkg can either be a Distribution or # Requirement instance if indent > 0: @@ -150,14 +153,31 @@ def render_tree(pkgs, pkg_index, req_map, list_all, # packages, eg. `testresources`, this will fail if pkg.key in pkg_index: pkg_deps = pkg_index[pkg.key].requires() - result += list(flatten([aux(d, indent=indent+2) - for d in pkg_deps])) + filtered_deps = [ + aux(d, indent=indent+2, chain=chain+[d.project_name]) + for d in pkg_deps + if d.project_name not in chain] + result += list(flatten(filtered_deps)) return result lines = flatten([aux(p) for p in (pkgs if list_all else top)]) return '\n'.join(lines) +def cyclic_deps(pkgs, pkg_index): + def aux(pkg, chain): + if pkg.key in pkg_index: + for d in pkg_index[pkg.key].requires(): + if d.project_name in chain: + yield ' => '.join([str(p) for p in chain] + [str(d)]) + else: + for cycle in aux(d, chain=chain+[d.project_name]): + yield cycle + + for cycle in flatten([aux(p, chain=[]) for p in pkgs]): + yield cycle + + def main(): parser = argparse.ArgumentParser(description=( 'Dependency tree of the installed python packages' @@ -200,6 +220,12 @@ def main(): print(tmpl.format(pkg, req), file=sys.stderr) print('-'*72, file=sys.stderr) + cyclic = cyclic_deps(pkgs, pkg_index) + if cyclic: + print('Warning!! Cyclic dependencies found:', file=sys.stderr) + for xs in cyclic: + print('- {0}'.format(xs)) + if args.freeze: top_pkg_str, non_top_pkg_str = top_pkg_src, non_top_pkg_src else: diff --git a/tests/cyclic_deps.pickle b/tests/cyclic_deps.pickle new file mode 100644 index 0000000..98e068d --- /dev/null +++ b/tests/cyclic_deps.pickle @@ -0,0 +1,85 @@ +(lp0 +ccopy_reg +_reconstructor +p1 +(cpip._vendor.pkg_resources +Distribution +p2 +c__builtin__ +object +p3 +Ntp4 +Rp5 +(dp6 +S'project_name' +p7 +S'CircularDependencyA' +p8 +sS'precedence' +p9 +I-1 +sS'_key' +p10 +S'circulardependencya' +p11 +sS'_version' +p12 +S'0.0.0' +p13 +sS'platform' +p14 +NsS'location' +p15 +S'.tox/cyclic_deps/lib/python2.7/site-packages' +p16 +sS'py_version' +p17 +S'2.7' +p18 +sS'_provider' +p19 +(ipip._vendor.pkg_resources +PathMetadata +p20 +(dp21 +S'module_path' +p22 +g16 +sS'egg_info' +p23 +S'.tox/cyclic_deps/lib/python2.7/site-packages/CircularDependencyA-0.0.0-py2.7.egg-info' +p24 +sbsbag1 +(g2 +g3 +Ntp25 +Rp26 +(dp27 +g7 +S'CircularDependencyB' +p28 +sg9 +I-1 +sg10 +S'circulardependencyb' +p29 +sg12 +S'0.0.0' +p30 +sg14 +Nsg15 +g16 +sg17 +S'2.7' +p31 +sg19 +(ipip._vendor.pkg_resources +PathMetadata +p32 +(dp33 +g22 +g16 +sg23 +S'.tox/cyclic_deps/lib/python2.7/site-packages/CircularDependencyB-0.0.0-py2.7.egg-info' +p34 +sbsba. diff --git a/tests/test_pipdeptree.py b/tests/test_pipdeptree.py index 13deab4..681647a 100644 --- a/tests/test_pipdeptree.py +++ b/tests/test_pipdeptree.py @@ -79,3 +79,33 @@ def test_render_tree_freeze(): assert ' - SQLAlchemy==0.9.1' in lines assert '-e git+https://github.com/naiquevin/lookupy.git@cdbe30c160e1c29802df75e145ea4ad903c05386#egg=Lookupy-master' in lines assert 'itsdangerous==0.23' not in lines + + +def test_render_tree_cyclic_dependency(): + with open('tests/cyclic_deps.pickle', 'rb') as f: + cyclic_pkgs = pickle.load(f) + + list_all = True + + tree_str = render_tree(cyclic_pkgs, pkg_index, req_map, list_all, + top_pkg_name, non_top_pkg_name) + lines = set(tree_str.split('\n')) + assert 'CircularDependencyA==0.0.0' in lines + assert ' - CircularDependencyB [installed: 0.0.0]' in lines + assert 'CircularDependencyB==0.0.0' in lines + assert ' - CircularDependencyA [installed: 0.0.0]' in lines + + +def test_render_tree_freeze_cyclic_dependency(): + with open('tests/cyclic_deps.pickle', 'rb') as f: + cyclic_pkgs = pickle.load(f) + + list_all = True + + tree_str = render_tree(cyclic_pkgs, pkg_index, req_map, list_all, + top_pkg_src, non_top_pkg_src) + 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 ' - CircularDependencyA==0.0.0' in lines diff --git a/tests/virtualenvs/cyclic_deps_requirements.txt b/tests/virtualenvs/cyclic_deps_requirements.txt new file mode 100644 index 0000000..64644a8 --- /dev/null +++ b/tests/virtualenvs/cyclic_deps_requirements.txt @@ -0,0 +1,2 @@ +CircularDependencyA +CircularDependencyB diff --git a/tox.ini b/tox.ini index 2bbc4db..3555847 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ envlist = py26, py27, py32, py33, py34 [testenv] commands = - tox -e dummy + tox -e dummy,cyclic_deps py.test {posargs:--cov pipdeptree --cov-report xml --cov-report html --cov-report term-missing tests/} deps = pytest @@ -24,3 +24,7 @@ whitelist_externals = tox [testenv:dummy] deps = -r{toxinidir}/tests/virtualenvs/dummy_requirements.txt commands = + +[testenv:cyclic_deps] +deps = -r{toxinidir}/tests/virtualenvs/cyclic_deps_requirements.txt +commands =