├── .gitignore ├── .travis.yml ├── CHANGES.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── README.txt ├── TODO.txt ├── distribute_setup.py ├── gen_rst.py ├── gluttony ├── __init__.py ├── commands.py ├── dependency.py ├── tests │ ├── __init__.py │ └── test_gluttony.py └── version.py ├── requirements.txt ├── setup.py └── test-requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | cover 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mrs. Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | *.sublime-project 38 | *.sublime-workspace 39 | 40 | # virtualenv 41 | env 42 | 43 | # SQLite database 44 | *.db 45 | *.sqlite 46 | 47 | # Vagrant 48 | .vagrant 49 | docs/_build/ 50 | 51 | # intellij 52 | *.iml 53 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | # command to install dependencies 5 | install: 6 | - "pip install flake8 --use-mirrors" 7 | - "pip install -r requirements.txt --use-mirrors" 8 | - "pip install -r test-requirements.txt --use-mirrors" 9 | - "python setup.py develop" 10 | before_script: "flake8 gluttony --ignore=E501,W291,W293" 11 | # command to run tests 12 | script: "python setup.py nosetests" 13 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v0.8, 2014/01/12 2 | ---------------- 3 | * Fix version number is None caused by build folder removed too early 4 | * Remove pickle output format, add JSON instead 5 | 6 | v0.7, 2014/01/12 7 | ---------------- 8 | * Fix build folder is not removed issue 9 | 10 | v0.6, 2013/01/31 11 | ---------------- 12 | * Fix missing distribute_setup.py issue. 13 | * Fix problem caused by change of pip API 14 | 15 | v0.5, 2011/02/19 16 | ---------------- 17 | * Fix problem caused by change of new pip 18 | 19 | v0.4, 2010/02/25 20 | ---------------- 21 | * Fix broken easy_install problem of 0.3 version 22 | 23 | v0.3, 2010/02/20 24 | ---------------- 25 | * Add --ignore-installed and --upgrade back, now you have choice to run 26 | gluttony on installed packages or on pypi 27 | * Fix bug of drawing no dependency diagram. 28 | 29 | v0.2, 2010/02/19 30 | ---------------- 31 | * Makes the name of nodes look better 32 | * Support output dot file with PyGraphviz or Pydot 33 | 34 | v0.1, 2010/02/19 35 | ---------------- 36 | * Initial release. 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Victor Lin (bornstub@gmail.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include distribute_setup.py 2 | include requirements.txt 3 | include test-requirements.txt 4 | include README.txt 5 | recursive-include gluttony 6 | prune env 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Gluttony 2 | 3 | [![Build Status](https://travis-ci.org/victorlin/gluttony.png?branch=master)](https://travis-ci.org/victorlin/gluttony) 4 | 5 | Gluttony is a tool for finding dependencies relationship of a Python package. 6 | 7 | ![Gluttony](http://static.ez2learn.com/gluttony/gluttony.jpg) 8 | 9 | ## Installation 10 | 11 | To install Gluttony 12 | 13 | ``` 14 | pip install Gluttony 15 | ``` 16 | 17 | ## Usage 18 | 19 | To understand the available optons of Gluttony, you can type: 20 | 21 | ``` 22 | gluttony --help 23 | ``` 24 | 25 | ## Drawing Graph 26 | 27 | To figure out the dependencies relationship of a Python package, here you can 28 | type (the diagram will be displayed by [matplotlib](http://matplotlib.org/), 29 | you need to install it before you can use --display-graph option) 30 | 31 | ``` 32 | gluttony --display-graph 33 | ``` 34 | 35 | For example, you would like to know the dependency relationships of 36 | [Sprox](http://sprox.org/), then you can type: 37 | 38 | ``` 39 | gluttony sprox --display-graph 40 | ``` 41 | 42 | The result might looks like this: 43 | 44 | ![Sproxy dependencies diagram](http://static.ez2learn.com/gluttony/sprox.png) 45 | 46 | Another example example: 47 | you want to understand the dependencies relationship of 48 | [TurboGears2](http://turbogears.org/), here we type 49 | 50 | ``` 51 | gluttony -i http://www.turbogears.org/2.0/downloads/current/index tg.devtools --display-graph 52 | ``` 53 | 54 | The result: 55 | 56 | ![Turbogears2 dependencies diagram](http://static.ez2learn.com/gluttony/tg2.png) 57 | 58 | Oops, the graph is a little bit messy. Currently, the layout of graph is not 59 | handled properly. However, it's not a big deal, you can output the graph as 60 | dot or JSON format file for further processing. 61 | 62 | ## Output Graphviz File 63 | 64 | To draw the diagram with Graphviz, you can output that dot format like this 65 | 66 | ``` 67 | gluttony sprox --pydot sprox.dot 68 | ``` 69 | 70 | Then you can use [Graphviz](http://www.graphviz.org/) for drawing beautiful 71 | graph. Like this one: 72 | 73 | ![Sproxy dependencies diagram](http://static.ez2learn.com/gluttony/sprox_dot.png) 74 | 75 | Another huge example: 76 | 77 | ![Dependency relationship digram of TurboGears2](http://static.ez2learn.com/gluttony/tg2_dot.png) 78 | 79 | ## Output JSON File 80 | 81 | If you want to get the raw relationship data in Python, this tool also 82 | provides a JSON output format. For example: 83 | 84 | ``` 85 | gluttony pyramid --json=pyramid.json 86 | ``` 87 | 88 | Then you can use `json.load` to load it into Python for further processing. 89 | 90 | ## Gallery 91 | 92 | See some beautiful Python package dependencies relationship diagram :) 93 | 94 | [Gallery](http://code.google.com/p/python-gluttony/wiki/Gallery) 95 | 96 | ## Author 97 | 98 | - Victor Lin (bornstub at gmail.com) 99 | - Twitter: [victorlin](http://twitter.com/victorlin) 100 | - Blog: [Victor's Blog](http://victorlin.me) 101 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Gluttony 2 | -------- 3 | 4 | |Build Status| 5 | 6 | Gluttony is a tool for finding dependencies relationship of a Python 7 | package. 8 | 9 | .. figure:: http://static.ez2learn.com/gluttony/gluttony.jpg 10 | :alt: Gluttony 11 | 12 | Installation 13 | ------------ 14 | 15 | To install Gluttony 16 | 17 | :: 18 | 19 | pip install Gluttony 20 | 21 | Usage 22 | ----- 23 | 24 | To understand the available optons of Gluttony, you can type: 25 | 26 | :: 27 | 28 | gluttony --help 29 | 30 | Drawing Graph 31 | ------------- 32 | 33 | To figure out the dependencies relationship of a Python package, here 34 | you can type (the diagram will be displayed by 35 | `matplotlib `__, you need to install it before 36 | you can use --display-graph option) 37 | 38 | :: 39 | 40 | gluttony --display-graph 41 | 42 | For example, you would like to know the dependency relationships of 43 | `Sprox `__, then you can type: 44 | 45 | :: 46 | 47 | gluttony sprox --display-graph 48 | 49 | The result might looks like this: 50 | 51 | .. figure:: http://static.ez2learn.com/gluttony/sprox.png 52 | :alt: Sproxy dependencies diagram 53 | 54 | Another example example: you want to understand the dependencies 55 | relationship of `TurboGears2 `__, here we type 56 | 57 | :: 58 | 59 | gluttony -i http://www.turbogears.org/2.0/downloads/current/index tg.devtools --display-graph 60 | 61 | The result: 62 | 63 | .. figure:: http://static.ez2learn.com/gluttony/tg2.png 64 | :alt: Turbogears2 dependencies diagram 65 | 66 | Oops, the graph is a little bit messy. Currently, the layout of graph is 67 | not handled properly. However, it's not a big deal, you can output the 68 | graph as dot or JSON format file for further processing. 69 | 70 | Output Graphviz File 71 | -------------------- 72 | 73 | To draw the diagram with Graphviz, you can output that dot format like 74 | this 75 | 76 | :: 77 | 78 | gluttony sprox --pydot sprox.dot 79 | 80 | Then you can use `Graphviz `__ for drawing 81 | beautiful graph. Like this one: 82 | 83 | .. figure:: http://static.ez2learn.com/gluttony/sprox_dot.png 84 | :alt: Sproxy dependencies diagram 85 | 86 | Another huge example: 87 | 88 | .. figure:: http://static.ez2learn.com/gluttony/tg2_dot.png 89 | :alt: Dependency relationship digram of TurboGears2 90 | 91 | Output JSON File 92 | ---------------- 93 | 94 | If you want to get the raw relationship data in Python, this tool also 95 | provides a JSON output format. For example: 96 | 97 | :: 98 | 99 | gluttony pyramid --json=pyramid.json 100 | 101 | Then you can use ``json.load`` to load it into Python for further 102 | processing. 103 | 104 | Gallery 105 | ------- 106 | 107 | See some beautiful Python package dependencies relationship diagram :) 108 | 109 | `Gallery `__ 110 | 111 | Author 112 | ------ 113 | 114 | - Victor Lin (bornstub at gmail.com) 115 | - Twitter: `victorlin `__ 116 | - Blog: `Victor's Blog `__ 117 | 118 | .. |Build Status| image:: https://travis-ci.org/victorlin/gluttony.png?branch=master 119 | :target: https://travis-ci.org/victorlin/gluttony 120 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fangpenlin/gluttony/86c24c7555dbc8de073aee66edb07a030f77275e/TODO.txt -------------------------------------------------------------------------------- /distribute_setup.py: -------------------------------------------------------------------------------- 1 | #!python 2 | """Bootstrap distribute installation 3 | 4 | If you want to use setuptools in your package's setup.py, just include this 5 | file in the same directory with it, and add this to the top of your setup.py:: 6 | 7 | from distribute_setup import use_setuptools 8 | use_setuptools() 9 | 10 | If you want to require a specific version of setuptools, set a download 11 | mirror, or use an alternate download directory, you can do so by supplying 12 | the appropriate options to ``use_setuptools()``. 13 | 14 | This file can also be run as a script to install or upgrade setuptools. 15 | """ 16 | import os 17 | import sys 18 | import time 19 | import fnmatch 20 | import tempfile 21 | import tarfile 22 | from distutils import log 23 | 24 | try: 25 | from site import USER_SITE 26 | except ImportError: 27 | USER_SITE = None 28 | 29 | try: 30 | import subprocess 31 | 32 | def _python_cmd(*args): 33 | args = (sys.executable,) + args 34 | return subprocess.call(args) == 0 35 | 36 | except ImportError: 37 | # will be used for python 2.3 38 | def _python_cmd(*args): 39 | args = (sys.executable,) + args 40 | # quoting arguments if windows 41 | if sys.platform == 'win32': 42 | def quote(arg): 43 | if ' ' in arg: 44 | return '"%s"' % arg 45 | return arg 46 | args = [quote(arg) for arg in args] 47 | return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 48 | 49 | DEFAULT_VERSION = "0.6.14" 50 | DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" 51 | SETUPTOOLS_FAKED_VERSION = "0.6c11" 52 | 53 | SETUPTOOLS_PKG_INFO = """\ 54 | Metadata-Version: 1.0 55 | Name: setuptools 56 | Version: %s 57 | Summary: xxxx 58 | Home-page: xxx 59 | Author: xxx 60 | Author-email: xxx 61 | License: xxx 62 | Description: xxx 63 | """ % SETUPTOOLS_FAKED_VERSION 64 | 65 | 66 | def _install(tarball): 67 | # extracting the tarball 68 | tmpdir = tempfile.mkdtemp() 69 | log.warn('Extracting in %s', tmpdir) 70 | old_wd = os.getcwd() 71 | try: 72 | os.chdir(tmpdir) 73 | tar = tarfile.open(tarball) 74 | _extractall(tar) 75 | tar.close() 76 | 77 | # going in the directory 78 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 79 | os.chdir(subdir) 80 | log.warn('Now working in %s', subdir) 81 | 82 | # installing 83 | log.warn('Installing Distribute') 84 | if not _python_cmd('setup.py', 'install'): 85 | log.warn('Something went wrong during the installation.') 86 | log.warn('See the error message above.') 87 | finally: 88 | os.chdir(old_wd) 89 | 90 | 91 | def _build_egg(egg, tarball, to_dir): 92 | # extracting the tarball 93 | tmpdir = tempfile.mkdtemp() 94 | log.warn('Extracting in %s', tmpdir) 95 | old_wd = os.getcwd() 96 | try: 97 | os.chdir(tmpdir) 98 | tar = tarfile.open(tarball) 99 | _extractall(tar) 100 | tar.close() 101 | 102 | # going in the directory 103 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 104 | os.chdir(subdir) 105 | log.warn('Now working in %s', subdir) 106 | 107 | # building an egg 108 | log.warn('Building a Distribute egg in %s', to_dir) 109 | _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) 110 | 111 | finally: 112 | os.chdir(old_wd) 113 | # returning the result 114 | log.warn(egg) 115 | if not os.path.exists(egg): 116 | raise IOError('Could not build the egg.') 117 | 118 | 119 | def _do_download(version, download_base, to_dir, download_delay): 120 | egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' 121 | % (version, sys.version_info[0], sys.version_info[1])) 122 | if not os.path.exists(egg): 123 | tarball = download_setuptools(version, download_base, 124 | to_dir, download_delay) 125 | _build_egg(egg, tarball, to_dir) 126 | sys.path.insert(0, egg) 127 | import setuptools 128 | setuptools.bootstrap_install_from = egg 129 | 130 | 131 | def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 132 | to_dir=os.curdir, download_delay=15, no_fake=True): 133 | # making sure we use the absolute path 134 | to_dir = os.path.abspath(to_dir) 135 | was_imported = 'pkg_resources' in sys.modules or \ 136 | 'setuptools' in sys.modules 137 | try: 138 | try: 139 | import pkg_resources 140 | if not hasattr(pkg_resources, '_distribute'): 141 | if not no_fake: 142 | _fake_setuptools() 143 | raise ImportError 144 | except ImportError: 145 | return _do_download(version, download_base, to_dir, download_delay) 146 | try: 147 | pkg_resources.require("distribute>="+version) 148 | return 149 | except pkg_resources.VersionConflict: 150 | e = sys.exc_info()[1] 151 | if was_imported: 152 | sys.stderr.write( 153 | "The required version of distribute (>=%s) is not available,\n" 154 | "and can't be installed while this script is running. Please\n" 155 | "install a more recent version first, using\n" 156 | "'easy_install -U distribute'." 157 | "\n\n(Currently using %r)\n" % (version, e.args[0])) 158 | sys.exit(2) 159 | else: 160 | del pkg_resources, sys.modules['pkg_resources'] # reload ok 161 | return _do_download(version, download_base, to_dir, 162 | download_delay) 163 | except pkg_resources.DistributionNotFound: 164 | return _do_download(version, download_base, to_dir, 165 | download_delay) 166 | finally: 167 | if not no_fake: 168 | _create_fake_setuptools_pkg_info(to_dir) 169 | 170 | def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 171 | to_dir=os.curdir, delay=15): 172 | """Download distribute from a specified location and return its filename 173 | 174 | `version` should be a valid distribute version number that is available 175 | as an egg for download under the `download_base` URL (which should end 176 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 177 | `delay` is the number of seconds to pause before an actual download 178 | attempt. 179 | """ 180 | # making sure we use the absolute path 181 | to_dir = os.path.abspath(to_dir) 182 | try: 183 | from urllib.request import urlopen 184 | except ImportError: 185 | from urllib2 import urlopen 186 | tgz_name = "distribute-%s.tar.gz" % version 187 | url = download_base + tgz_name 188 | saveto = os.path.join(to_dir, tgz_name) 189 | src = dst = None 190 | if not os.path.exists(saveto): # Avoid repeated downloads 191 | try: 192 | log.warn("Downloading %s", url) 193 | src = urlopen(url) 194 | # Read/write all in one block, so we don't create a corrupt file 195 | # if the download is interrupted. 196 | data = src.read() 197 | dst = open(saveto, "wb") 198 | dst.write(data) 199 | finally: 200 | if src: 201 | src.close() 202 | if dst: 203 | dst.close() 204 | return os.path.realpath(saveto) 205 | 206 | def _no_sandbox(function): 207 | def __no_sandbox(*args, **kw): 208 | try: 209 | from setuptools.sandbox import DirectorySandbox 210 | if not hasattr(DirectorySandbox, '_old'): 211 | def violation(*args): 212 | pass 213 | DirectorySandbox._old = DirectorySandbox._violation 214 | DirectorySandbox._violation = violation 215 | patched = True 216 | else: 217 | patched = False 218 | except ImportError: 219 | patched = False 220 | 221 | try: 222 | return function(*args, **kw) 223 | finally: 224 | if patched: 225 | DirectorySandbox._violation = DirectorySandbox._old 226 | del DirectorySandbox._old 227 | 228 | return __no_sandbox 229 | 230 | def _patch_file(path, content): 231 | """Will backup the file then patch it""" 232 | existing_content = open(path).read() 233 | if existing_content == content: 234 | # already patched 235 | log.warn('Already patched.') 236 | return False 237 | log.warn('Patching...') 238 | _rename_path(path) 239 | f = open(path, 'w') 240 | try: 241 | f.write(content) 242 | finally: 243 | f.close() 244 | return True 245 | 246 | _patch_file = _no_sandbox(_patch_file) 247 | 248 | def _same_content(path, content): 249 | return open(path).read() == content 250 | 251 | def _rename_path(path): 252 | new_name = path + '.OLD.%s' % time.time() 253 | log.warn('Renaming %s into %s', path, new_name) 254 | os.rename(path, new_name) 255 | return new_name 256 | 257 | def _remove_flat_installation(placeholder): 258 | if not os.path.isdir(placeholder): 259 | log.warn('Unkown installation at %s', placeholder) 260 | return False 261 | found = False 262 | for file in os.listdir(placeholder): 263 | if fnmatch.fnmatch(file, 'setuptools*.egg-info'): 264 | found = True 265 | break 266 | if not found: 267 | log.warn('Could not locate setuptools*.egg-info') 268 | return 269 | 270 | log.warn('Removing elements out of the way...') 271 | pkg_info = os.path.join(placeholder, file) 272 | if os.path.isdir(pkg_info): 273 | patched = _patch_egg_dir(pkg_info) 274 | else: 275 | patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) 276 | 277 | if not patched: 278 | log.warn('%s already patched.', pkg_info) 279 | return False 280 | # now let's move the files out of the way 281 | for element in ('setuptools', 'pkg_resources.py', 'site.py'): 282 | element = os.path.join(placeholder, element) 283 | if os.path.exists(element): 284 | _rename_path(element) 285 | else: 286 | log.warn('Could not find the %s element of the ' 287 | 'Setuptools distribution', element) 288 | return True 289 | 290 | _remove_flat_installation = _no_sandbox(_remove_flat_installation) 291 | 292 | def _after_install(dist): 293 | log.warn('After install bootstrap.') 294 | placeholder = dist.get_command_obj('install').install_purelib 295 | _create_fake_setuptools_pkg_info(placeholder) 296 | 297 | def _create_fake_setuptools_pkg_info(placeholder): 298 | if not placeholder or not os.path.exists(placeholder): 299 | log.warn('Could not find the install location') 300 | return 301 | pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) 302 | setuptools_file = 'setuptools-%s-py%s.egg-info' % \ 303 | (SETUPTOOLS_FAKED_VERSION, pyver) 304 | pkg_info = os.path.join(placeholder, setuptools_file) 305 | if os.path.exists(pkg_info): 306 | log.warn('%s already exists', pkg_info) 307 | return 308 | 309 | log.warn('Creating %s', pkg_info) 310 | f = open(pkg_info, 'w') 311 | try: 312 | f.write(SETUPTOOLS_PKG_INFO) 313 | finally: 314 | f.close() 315 | 316 | pth_file = os.path.join(placeholder, 'setuptools.pth') 317 | log.warn('Creating %s', pth_file) 318 | f = open(pth_file, 'w') 319 | try: 320 | f.write(os.path.join(os.curdir, setuptools_file)) 321 | finally: 322 | f.close() 323 | 324 | _create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info) 325 | 326 | def _patch_egg_dir(path): 327 | # let's check if it's already patched 328 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 329 | if os.path.exists(pkg_info): 330 | if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): 331 | log.warn('%s already patched.', pkg_info) 332 | return False 333 | _rename_path(path) 334 | os.mkdir(path) 335 | os.mkdir(os.path.join(path, 'EGG-INFO')) 336 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 337 | f = open(pkg_info, 'w') 338 | try: 339 | f.write(SETUPTOOLS_PKG_INFO) 340 | finally: 341 | f.close() 342 | return True 343 | 344 | _patch_egg_dir = _no_sandbox(_patch_egg_dir) 345 | 346 | def _before_install(): 347 | log.warn('Before install bootstrap.') 348 | _fake_setuptools() 349 | 350 | 351 | def _under_prefix(location): 352 | if 'install' not in sys.argv: 353 | return True 354 | args = sys.argv[sys.argv.index('install')+1:] 355 | for index, arg in enumerate(args): 356 | for option in ('--root', '--prefix'): 357 | if arg.startswith('%s=' % option): 358 | top_dir = arg.split('root=')[-1] 359 | return location.startswith(top_dir) 360 | elif arg == option: 361 | if len(args) > index: 362 | top_dir = args[index+1] 363 | return location.startswith(top_dir) 364 | if arg == '--user' and USER_SITE is not None: 365 | return location.startswith(USER_SITE) 366 | return True 367 | 368 | 369 | def _fake_setuptools(): 370 | log.warn('Scanning installed packages') 371 | try: 372 | import pkg_resources 373 | except ImportError: 374 | # we're cool 375 | log.warn('Setuptools or Distribute does not seem to be installed.') 376 | return 377 | ws = pkg_resources.working_set 378 | try: 379 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', 380 | replacement=False)) 381 | except TypeError: 382 | # old distribute API 383 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) 384 | 385 | if setuptools_dist is None: 386 | log.warn('No setuptools distribution found') 387 | return 388 | # detecting if it was already faked 389 | setuptools_location = setuptools_dist.location 390 | log.warn('Setuptools installation detected at %s', setuptools_location) 391 | 392 | # if --root or --preix was provided, and if 393 | # setuptools is not located in them, we don't patch it 394 | if not _under_prefix(setuptools_location): 395 | log.warn('Not patching, --root or --prefix is installing Distribute' 396 | ' in another location') 397 | return 398 | 399 | # let's see if its an egg 400 | if not setuptools_location.endswith('.egg'): 401 | log.warn('Non-egg installation') 402 | res = _remove_flat_installation(setuptools_location) 403 | if not res: 404 | return 405 | else: 406 | log.warn('Egg installation') 407 | pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') 408 | if (os.path.exists(pkg_info) and 409 | _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): 410 | log.warn('Already patched.') 411 | return 412 | log.warn('Patching...') 413 | # let's create a fake egg replacing setuptools one 414 | res = _patch_egg_dir(setuptools_location) 415 | if not res: 416 | return 417 | log.warn('Patched done.') 418 | _relaunch() 419 | 420 | 421 | def _relaunch(): 422 | log.warn('Relaunching...') 423 | # we have to relaunch the process 424 | # pip marker to avoid a relaunch bug 425 | if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']: 426 | sys.argv[0] = 'setup.py' 427 | args = [sys.executable] + sys.argv 428 | sys.exit(subprocess.call(args)) 429 | 430 | 431 | def _extractall(self, path=".", members=None): 432 | """Extract all members from the archive to the current working 433 | directory and set owner, modification time and permissions on 434 | directories afterwards. `path' specifies a different directory 435 | to extract to. `members' is optional and must be a subset of the 436 | list returned by getmembers(). 437 | """ 438 | import copy 439 | import operator 440 | from tarfile import ExtractError 441 | directories = [] 442 | 443 | if members is None: 444 | members = self 445 | 446 | for tarinfo in members: 447 | if tarinfo.isdir(): 448 | # Extract directories with a safe mode. 449 | directories.append(tarinfo) 450 | tarinfo = copy.copy(tarinfo) 451 | tarinfo.mode = 448 # decimal for oct 0700 452 | self.extract(tarinfo, path) 453 | 454 | # Reverse sort directories. 455 | if sys.version_info < (2, 4): 456 | def sorter(dir1, dir2): 457 | return cmp(dir1.name, dir2.name) 458 | directories.sort(sorter) 459 | directories.reverse() 460 | else: 461 | directories.sort(key=operator.attrgetter('name'), reverse=True) 462 | 463 | # Set correct owner, mtime and filemode on directories. 464 | for tarinfo in directories: 465 | dirpath = os.path.join(path, tarinfo.name) 466 | try: 467 | self.chown(tarinfo, dirpath) 468 | self.utime(tarinfo, dirpath) 469 | self.chmod(tarinfo, dirpath) 470 | except ExtractError: 471 | e = sys.exc_info()[1] 472 | if self.errorlevel > 1: 473 | raise 474 | else: 475 | self._dbg(1, "tarfile: %s" % e) 476 | 477 | 478 | def main(argv, version=DEFAULT_VERSION): 479 | """Install or upgrade setuptools and EasyInstall""" 480 | tarball = download_setuptools() 481 | _install(tarball) 482 | 483 | 484 | if __name__ == '__main__': 485 | main(sys.argv[1:]) 486 | -------------------------------------------------------------------------------- /gen_rst.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | import pandoc 5 | 6 | pandoc.core.PANDOC_PATH = os.environ['PANDOC_PATH'] 7 | 8 | doc = pandoc.Document() 9 | with open('README.md', 'rt') as md: 10 | doc.markdown = md.read() 11 | with open('README.txt', 'wt') as rst: 12 | rst_txt = doc.rst 13 | rst_txt = rst_txt.replace('\r\n', '\n') 14 | rst_txt = re.sub(r':alt: (.*)\n(\s+)(.*)', r':alt: \1\n', rst_txt) 15 | rst.write(rst_txt) 16 | 17 | print 'done' 18 | -------------------------------------------------------------------------------- /gluttony/__init__.py: -------------------------------------------------------------------------------- 1 | from version import __version__ # noqa 2 | -------------------------------------------------------------------------------- /gluttony/commands.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import sys 3 | import os 4 | import optparse 5 | import json 6 | 7 | from pip.log import logger 8 | from pip.index import PackageFinder 9 | from pip.req import RequirementSet, InstallRequirement, parse_requirements 10 | from pip.locations import build_prefix, src_prefix 11 | 12 | from dependency import trace_dependencies 13 | from version import __version__ 14 | 15 | 16 | def pretty_project_name(req): 17 | """Get project name in a pretty form: 18 | 19 | name-version 20 | 21 | """ 22 | return '%s-%s' % (req.name, req.installed_version) 23 | 24 | 25 | class Command(object): 26 | bundle = False 27 | 28 | def __init__(self): 29 | self.parser = optparse.OptionParser(version="%prog " + __version__) 30 | self.parser.add_option( 31 | '-e', '--editable', 32 | dest='editables', 33 | action='append', 34 | default=[], 35 | metavar='VCS+REPOS_URL[@REV]#egg=PACKAGE', 36 | help='Install a package directly from a checkout. Source will be checked ' 37 | 'out into src/PACKAGE (lower-case) and installed in-place (using ' 38 | 'setup.py develop). You can run this on an existing directory/checkout (like ' 39 | 'pip install -e src/mycheckout). This option may be provided multiple times. ' 40 | 'Possible values for VCS are: svn, git, hg and bzr.') 41 | self.parser.add_option( 42 | '-r', '--requirement', 43 | dest='requirements', 44 | action='append', 45 | default=[], 46 | metavar='FILENAME', 47 | help='Install all the packages listed in the given requirements file. ' 48 | 'This option can be used multiple times.') 49 | self.parser.add_option( 50 | '-f', '--find-links', 51 | dest='find_links', 52 | action='append', 53 | default=[], 54 | metavar='URL', 55 | help='URL to look for packages at') 56 | self.parser.add_option( 57 | '-i', '--index-url', '--pypi-url', 58 | dest='index_url', 59 | metavar='URL', 60 | default='http://pypi.python.org/simple', 61 | help='Base URL of Python Package Index (default %default)') 62 | self.parser.add_option( 63 | '--extra-index-url', 64 | dest='extra_index_urls', 65 | metavar='URL', 66 | action='append', 67 | default=[], 68 | help='Extra URLs of package indexes to use in addition to --index-url') 69 | self.parser.add_option( 70 | '--no-index', 71 | dest='no_index', 72 | action='store_true', 73 | default=False, 74 | help='Ignore package index (only looking at --find-links URLs instead)') 75 | self.parser.add_option( 76 | '-b', '--build', '--build-dir', '--build-directory', 77 | dest='build_dir', 78 | metavar='DIR', 79 | default=None, 80 | help='Unpack packages into DIR (default %s) and build from there' % build_prefix) 81 | self.parser.add_option( 82 | '-d', '--download', '--download-dir', '--download-directory', 83 | dest='download_dir', 84 | metavar='DIR', 85 | default=None, 86 | help='Download packages into DIR instead of installing them') 87 | self.parser.add_option( 88 | '--download-cache', 89 | dest='download_cache', 90 | metavar='DIR', 91 | default=None, 92 | help='Cache downloaded packages in DIR') 93 | self.parser.add_option( 94 | '--src', '--source', '--source-dir', '--source-directory', 95 | dest='src_dir', 96 | metavar='DIR', 97 | default=None, 98 | help='Check out --editable packages into DIR (default %s)' % src_prefix) 99 | self.parser.add_option( 100 | '-U', '--upgrade', 101 | dest='upgrade', 102 | action='store_true', 103 | help='Upgrade all packages to the newest available version') 104 | self.parser.add_option( 105 | '-I', '--ignore-installed', 106 | dest='ignore_installed', 107 | action='store_true', 108 | help='Ignore the installed packages (reinstalling instead)') 109 | 110 | # options for output 111 | self.parser.add_option( 112 | '-j', '--json', 113 | dest='json_file', 114 | metavar='FILE', 115 | help='JSON filename for result output') 116 | self.parser.add_option( 117 | '--pydot', 118 | dest='py_dot', 119 | metavar='FILE', 120 | help='Output dot file with pydot') 121 | self.parser.add_option( 122 | '--pygraphviz', 123 | dest='py_graphviz', 124 | metavar='FILE', 125 | help='Output dot file with PyGraphviz') 126 | self.parser.add_option( 127 | '--display', '--display-graph', 128 | dest='display_graph', 129 | action='store_true', 130 | help='Display graph with Networkx and matplotlib') 131 | self.parser.add_option( 132 | '-R', '--reverse', 133 | dest='reverse', 134 | action='store_true', 135 | help='Reverse the direction of edge') 136 | 137 | def run(self, options, args): 138 | if not options.build_dir: 139 | options.build_dir = build_prefix 140 | if not options.src_dir: 141 | options.src_dir = src_prefix 142 | if options.download_dir: 143 | options.no_install = True 144 | options.ignore_installed = True 145 | else: 146 | options.build_dir = os.path.abspath(options.build_dir) 147 | options.src_dir = os.path.abspath(options.src_dir) 148 | index_urls = [options.index_url] + options.extra_index_urls 149 | if options.no_index: 150 | logger.notify('Ignoring indexes: %s' % ','.join(index_urls)) 151 | index_urls = [] 152 | finder = PackageFinder( 153 | find_links=options.find_links, 154 | index_urls=index_urls) 155 | requirement_set = RequirementSet( 156 | build_dir=options.build_dir, 157 | src_dir=options.src_dir, 158 | download_dir=options.download_dir, 159 | download_cache=options.download_cache, 160 | upgrade=options.upgrade, 161 | ignore_installed=options.ignore_installed, 162 | ignore_dependencies=False, 163 | ) 164 | 165 | for name in args: 166 | requirement_set.add_requirement( 167 | InstallRequirement.from_line(name, None)) 168 | for name in options.editables: 169 | requirement_set.add_requirement( 170 | InstallRequirement.from_editable(name, default_vcs=options.default_vcs)) 171 | for filename in options.requirements: 172 | for req in parse_requirements(filename, finder=finder, options=options): 173 | requirement_set.add_requirement(req) 174 | 175 | requirement_set.prepare_files( 176 | finder, 177 | force_root_egg_info=self.bundle, 178 | bundle=self.bundle, 179 | ) 180 | 181 | return requirement_set 182 | 183 | def _output_json(self, json_file, dependencies): 184 | packages = set() 185 | json_deps = [] 186 | for src, dest in dependencies: 187 | packages.add(src) 188 | packages.add(dest) 189 | json_deps.append([ 190 | pretty_project_name(src), 191 | pretty_project_name(dest), 192 | ]) 193 | 194 | json_packages = [] 195 | for package in packages: 196 | json_packages.append(dict( 197 | name=package.name, 198 | installed_version=package.installed_version, 199 | )) 200 | 201 | with open(json_file, 'wt') as jfile: 202 | json.dump(dict( 203 | packages=json_packages, 204 | dependencies=json_deps, 205 | ), jfile, sort_keys=True, indent=4, separators=(',', ': ')) 206 | 207 | def output(self, options, args, dependencies): 208 | """Output result 209 | 210 | """ 211 | if options.reverse: 212 | dependencies = map(lambda x: x[::-1], dependencies) 213 | 214 | if options.json_file: 215 | self._output_json(options.json_file, dependencies) 216 | logger.notify("Dependencies relationships result is in %s now", 217 | options.json_file) 218 | 219 | if options.display_graph or options.py_dot or options.py_graphviz: 220 | import networkx as nx 221 | 222 | # extract name and version 223 | def convert(pair): 224 | return ( 225 | pretty_project_name(pair[0]), 226 | pretty_project_name(pair[1]), 227 | ) 228 | plain_dependencies = map(convert, dependencies) 229 | dg = nx.DiGraph() 230 | dg.add_edges_from(plain_dependencies) 231 | if options.py_dot: 232 | logger.notify("Writing dot to %s with Pydot ...", 233 | options.py_dot) 234 | from networkx.drawing.nx_pydot import write_dot 235 | write_dot(dg, options.py_dot) 236 | if options.py_graphviz: 237 | logger.notify("Writing dot to %s with PyGraphviz ...", 238 | options.py_graphviz) 239 | from networkx.drawing.nx_agraph import write_dot 240 | write_dot(dg, options.py_graphviz) 241 | if options.display_graph: 242 | import matplotlib.pyplot as plt 243 | logger.notify("Drawing graph ...") 244 | if not plain_dependencies: 245 | logger.notify("There is no dependency to draw.") 246 | else: 247 | nx.draw(dg) 248 | plt.show() 249 | 250 | def main(self, args): 251 | options, args = self.parser.parse_args(args) 252 | if not args: 253 | self.parser.print_help() 254 | return 255 | 256 | level = 1 # Notify 257 | logger.level_for_integer(level) 258 | logger.consumers.extend([(level, sys.stdout)]) 259 | # get all files 260 | requirement_set = self.run(options, args) 261 | # trace dependencies 262 | logger.notify("Tracing dependencies ...") 263 | dependencies = [] 264 | values = None 265 | if hasattr(requirement_set.requirements, 'itervalues'): 266 | values = list(requirement_set.requirements.itervalues()) 267 | elif hasattr(requirement_set.requirements, 'values'): 268 | values = list(requirement_set.requirements.values()) 269 | for req in values: 270 | trace_dependencies(req, requirement_set, dependencies) 271 | # output the result 272 | logger.notify("Output result ...") 273 | self.output(options, args, dependencies) 274 | requirement_set.cleanup_files() 275 | 276 | 277 | def main(): 278 | command = Command() 279 | command.main(sys.argv[1:]) 280 | 281 | if __name__ == '__main__': 282 | main() 283 | -------------------------------------------------------------------------------- /gluttony/dependency.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import pkg_resources 4 | from pip.log import logger 5 | 6 | 7 | def trace_dependencies(req, requirement_set, dependencies, _visited=None): 8 | """Trace all dependency relationship 9 | 10 | @param req: requirements to trace 11 | @param requirement_set: RequirementSet 12 | @param dependencies: list for storing dependencies relationships 13 | @param _visited: visited requirement set 14 | """ 15 | _visited = _visited or set() 16 | if req in _visited: 17 | return 18 | _visited.add(req) 19 | for reqName in req.requirements(): 20 | try: 21 | name = pkg_resources.Requirement.parse(reqName).project_name 22 | except ValueError, e: 23 | logger.error('Invalid requirement: %r (%s) in requirement %s' % ( 24 | reqName, e, req)) 25 | continue 26 | subreq = requirement_set.get_requirement(name) 27 | dependencies.append((req, subreq)) 28 | trace_dependencies(subreq, requirement_set, dependencies, _visited) 29 | -------------------------------------------------------------------------------- /gluttony/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fangpenlin/gluttony/86c24c7555dbc8de073aee66edb07a030f77275e/gluttony/tests/__init__.py -------------------------------------------------------------------------------- /gluttony/tests/test_gluttony.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import os 3 | import unittest 4 | import tempfile 5 | import shutil 6 | import json 7 | 8 | from gluttony import commands 9 | 10 | 11 | class TestGluttony(unittest.TestCase): 12 | def setUp(self): 13 | self.temp_dir = tempfile.mkdtemp() 14 | 15 | def tearDown(self): 16 | shutil.rmtree(self.temp_dir) 17 | 18 | def test_command(self): 19 | json_path = os.path.join(self.temp_dir, 'flask.json') 20 | 21 | command = commands.Command() 22 | command.main(['flask==0.10.1', '--json={}'.format(json_path)]) 23 | 24 | with open(json_path, 'rb') as jfile: 25 | result = json.load(jfile) 26 | 27 | pkg_map = {} 28 | packages = set([p['name'] for p in result['packages']]) 29 | expected_packages = set([ 30 | 'flask', 31 | 'Werkzeug', 32 | 'itsdangerous', 33 | 'Jinja2', 34 | 'markupsafe', 35 | ]) 36 | self.assertEqual(packages, expected_packages) 37 | 38 | for p in result['packages']: 39 | pkg_map[p['name']] = p 40 | self.assertEqual(pkg_map['flask']['installed_version'], '0.10.1') 41 | 42 | dependencies = set([ 43 | (src.split('-')[0], dest.split('-')[0]) 44 | for src, dest in result['dependencies'] 45 | ]) 46 | expected_dependencies = set([ 47 | ('flask', 'Werkzeug'), 48 | ('flask', 'itsdangerous'), 49 | ('flask', 'Jinja2'), 50 | ('Jinja2', 'markupsafe'), 51 | ]) 52 | self.assertEqual(dependencies, expected_dependencies) 53 | 54 | def test_build_cleanup(self): 55 | # run the command twice, to ensure the build folder is removed correctly 56 | self.test_command() 57 | self.test_command() 58 | -------------------------------------------------------------------------------- /gluttony/version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | from __future__ import unicode_literals 3 | 4 | __version__ = '0.8' 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pip>=0.8 2 | networkx>=1.0.1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from distribute_setup import use_setuptools 4 | use_setuptools() 5 | 6 | from setuptools import setup 7 | 8 | here = os.path.abspath(os.path.dirname(__file__)) 9 | readme = open(os.path.join(here, 'README.txt')).read() 10 | requires = open(os.path.join(here, 'requirements.txt')).read() 11 | requires = map(lambda r: r.strip(), requires.splitlines()) 12 | test_requires = open(os.path.join(here, 'test-requirements.txt')).read() 13 | test_requires = map(lambda r: r.strip(), test_requires.splitlines()) 14 | 15 | extra = {} 16 | try: 17 | import gluttony 18 | extra['version'] = gluttony.__version__ 19 | except ImportError: 20 | pass 21 | 22 | setup( 23 | name='Gluttony', 24 | description="A tool for find dependencies relationships between Python " 25 | "packages", 26 | author='Victor Lin', 27 | author_email='bornstub@gmail.com', 28 | keywords='package dependency relationship', 29 | long_description=readme, 30 | url='http://github.com/victorlin/gluttony', 31 | install_requires=requires, 32 | tests_require=test_requires, 33 | include_package_data=True, 34 | packages=['gluttony'], 35 | license='MIT', 36 | entry_points={ 37 | 'console_scripts': ['gluttony = gluttony.commands:main'] 38 | }, 39 | **extra 40 | ) 41 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | nose 2 | --------------------------------------------------------------------------------