Skip to content

Instantly share code, notes, and snippets.

@jlinoff
Created August 26, 2017 14:52
Show Gist options
  • Save jlinoff/4c4dab3d8a998c9a20c86bfaea6204e4 to your computer and use it in GitHub Desktop.
Save jlinoff/4c4dab3d8a998c9a20c86bfaea6204e4 to your computer and use it in GitHub Desktop.
Report information about python wheel files. Works for both python 2 and 3.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Report information about Python wheel files.
It is useful for analyzing wheels that have not been installed.
By default it provides a one-line ls -l like report of each wheel that
reports the file name, the size, the wheel name, the version and the
summary.
You can see more of the wheel contents by increasing the verbosity of
the report. When you specify -vv, it will report all of the wheel
files and their contents.
You can report all of the wheel files in a directory in two ways:
explicitly specify each file (i.e., whls/*.whl) or just provide
the directory name (i.e., whls).
'''
import argparse
import inspect
import json
import os
import sys
import zipfile
VERSION = '1.0.0'
def info(msg, d=1):
'''
Print an info message.
'''
print('INFO:{}: {}'.format(inspect.stack()[d][2], msg))
def warn(msg, d=1):
'''
Print warning message.
'''
w = sys.stderr.write
w('\033[31;1mWARNING:{}:\033[0;31m {}\033[0m\n'.format(inspect.stack()[d][2], msg))
def err(msg, d=1, ec=1):
'''
Print an error message.
'''
w = sys.stderr.write
w('\033[31;1mERROR:{}:\033[0;31m {}\033[0m\n'.format(inspect.stack()[d][2], msg))
sys.exit(ec)
def getopts():
'''
Get the command line options.
'''
def gettext(s):
'''
Convert to upper case to make things consistent.
'''
lookup = {
'usage: ': 'USAGE:',
'positional arguments': 'POSITIONAL ARGUMENTS',
'optional arguments': 'OPTIONAL ARGUMENTS',
'show this help message and exit': 'Show this help message and exit.\n ',
}
return lookup.get(s, s)
argparse._ = gettext # to capitalize help headers
base = os.path.basename(sys.argv[0])
name = os.path.splitext(base)[0]
usage = '\n {0} [OPTIONS] [FILES]'.format(base)
desc = 'DESCRIPTION:{0}'.format('\n '.join(__doc__.split('\n')))
epilog = r'''
EXAMPLES:
# Example 1: help
$ {0} -h
# Example 2: list whl info
$ {0} whl/*.whl
whl/example-1.0.2.post1-py27-none-any.whl 27,335 example 1.0.2.post1 Example package.
# ^ ^ ^ ^
# | | | +------ summary
# | | +------------------- version
# | +---------------------------- name
# +------------------------------------ size
# Example 3: repo whl details
$ {0} -vv whl/*.whl
<output snipped>
'''.format(base)
afc = argparse.RawTextHelpFormatter
parser = argparse.ArgumentParser(formatter_class=afc,
description=desc[:-2],
usage=usage,
epilog=epilog.rstrip() + '\n ')
parser.add_argument('-v', '--verbose',
action='count',
default=0,
help='''\
Increase the level of verbosity.
Use -v to see the wheel files.
Use -vv to see the wheel file contents.
''')
parser.add_argument('-V', '--version',
action='version',
version='%(prog)s version {0}'.format(VERSION),
help='''\
Show program's version number and exit.
''')
parser.add_argument('FILES',
nargs='*',
help='''\
Wheel files.
''')
opts = parser.parse_args()
return opts
def ls(opts, whls):
'''
Ls type listing of the wheels that includes
the file name, the size, the version, the name
and the short description from the metadata.
'''
# Preprocess - get max sizes and data.
m = {'path': 0, 'version': 0, 'summary': 0, 'name': 0}
d = {'version': '', 'summary': '', 'name': ''}
for path in whls:
m['path'] = max(m['path'], len(path))
zf = zipfile.ZipFile(path)
metadata = None
for name in sorted(zf.namelist(), key=str.lower):
if name.endswith('.json'):
metadata = name
break
content = zf.read(metadata)
jd = json.loads(content)
for n in ['version', 'summary', 'name']:
m[n] = max(m[n], len(jd[n]))
d[n] = jd[n]
# Get data.
for path in whls:
sz = os.stat(path).st_size
print('{:<{}} {:>10,} {:<{}} {:<{}} {}'.format(path, m['path'],
sz,
d['name'], m['name'],
d['version'], m['version'],
d['summary'].strip()))
def report(opts, path, maxp):
'''
Dump a wheel file.
This takes advantage of the fact that Python wheel
files are basically zip files with structure.
@param opts The command line options.
@param path The full path to the whl file.
'''
zf = zipfile.ZipFile(path)
sz = os.stat(path).st_size
p1 = ' '*3
p2 = p1 + ' '*3
print('{:<{}} {:>10,}'.format(path, maxp, sz))
maxw = max(len(n) for n in zf.namelist())
for name in sorted(zf.namelist(), key=str.lower):
content = zf.read(name)
if (opts.verbose > 0):
print('{}{:<{}} {:>10,}'.format(p1, name, maxw, len(content)))
if opts.verbose > 1:
if name.endswith('.jar'):
# Jars are binary files, there is no useful information.
print('{}<binary>'.format(p2))
elif name.endswith('.json'):
# Output JSON in JSON format.
jd = json.loads(content)
lines = json.dumps(jd, indent=3)
for line in lines.splitlines():
print('{}{}'.format(p2, line))
else:
# Treat everything else as text.
# Make it work for python 2 and 3.
for line in content.rstrip().splitlines():
print('{}{}'.format(p2, line.rstrip().decode('utf-8')))
def iszip(path):
'''
Is this a zip file?
'''
try:
ok = True
zf = zipfile.ZipFile(path)
except (zipfile.BadZipfile):
ok = False
return ok
def load_whls(opts):
'''
Load the wheel files.
'''
whls = []
maxp = 0
paths = opts.FILES if opts.FILES else ['.']
for path in paths:
if os.path.isfile(path):
if iszip(path):
whls.append(path)
maxp = max(maxp, len(path))
else:
warn('Not a zipfile: {}'.format(path))
elif os.path.isdir(path):
for whl in os.listdir(path):
if whl.endswith('.whl'):
path = os.path.join(path, whl)
if iszip(path):
whls.append(path)
maxp = max(maxp, len(path))
else:
warn('Not a zipfile: {}'.format(path))
else:
warn('No such file or directory: {}'.format(path))
return whls, maxp
def main():
'''
Main.
'''
opts = getopts()
whls, maxp = load_whls(opts)
if opts.verbose == 0:
ls(opts, whls)
else:
for path in whls:
report(opts, path, maxp)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment