Created
August 26, 2017 14:52
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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