Created
December 26, 2017 20:55
-
-
Save bryanhelmig/895d130749ee0eb2e43a98fba32ac180 to your computer and use it in GitHub Desktop.
Compare pip freeze to requirements.txt for use in a bash script where pip may not be installed.
This file contains 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 | |
import argparse | |
import subprocess | |
import sys | |
from packaging.version import parse as parse_version | |
def parse_dependencies(text): | |
""" | |
Generate tuples from a raw 'Django==1.11.3\nrequests==2.5.0' text. | |
""" | |
for line in text.splitlines(): | |
if line.startswith('#'): | |
continue | |
if line.startswith('-'): | |
continue | |
if '#' in line: | |
line, _ = line.split('#', 1) | |
line = line.strip() | |
if not line: | |
continue | |
if '==' in line: | |
package, version = line.split('==', 1) | |
version = version.strip('=').strip().lower() # sometimes we see ===? | |
else: | |
package = line | |
version = None | |
yield package.lower(), version | |
def read_dependencies(*paths): | |
""" | |
Read the requirements.txt and parse, return large {package: version} dict. | |
""" | |
all_deps = [] | |
for path in paths: | |
with open(path, 'r') as f: | |
all_deps.extend(parse_dependencies(f.read())) | |
return dict(all_deps) | |
def get_installed_dependencies(): | |
""" | |
Run pip freeze and parse, return large {package: version} dict. | |
""" | |
return dict(parse_dependencies(subprocess.check_output(['pip', 'freeze']))) | |
def compare_dependencies(expected_dependencies, current_dependencies): | |
""" | |
Given two lists of tuples for expected and current dependencies | |
this returns a list of packages that are incorrect as a three | |
part tuple: | |
expected_dependencies = [('Django', '1.3.4')] | |
current_dependencies = [('Django', '1.2.3')] | |
out = compare_dependencies(expected_dependencies, current_dependencies) | |
list(out) == [('Django', '1.3.4', '1.2.3')] | |
""" | |
for expected_package, expected_version in expected_dependencies.items(): | |
# not installed at all | |
if expected_package not in current_dependencies: | |
yield expected_package, expected_version or '*any*', '*missing*' | |
continue | |
# any version will do | |
if expected_version is None: | |
continue | |
# wrong version | |
current_version = current_dependencies[expected_package] | |
if parse_version(expected_version) > parse_version(current_version): | |
yield expected_package, expected_version or '*any*', current_version | |
def main(files, ignore=None): | |
""" | |
Compare existing and installed dependencies, and quit with different | |
exit codes depending on the results of that. | |
""" | |
ignore = ignore or [] | |
expected_dependencies = read_dependencies(*files) | |
current_dependencies = get_installed_dependencies() | |
package_diff = [ | |
(package, expected_version, current_version) | |
for package, expected_version, current_version in | |
compare_dependencies(expected_dependencies, current_dependencies) | |
if package not in ignore | |
] | |
if package_diff: | |
# pylint: disable=E1601 | |
print('packagename\texpect\tcurrent'.format(package, expected_version, current_version)) | |
print('-------------------------------') | |
for package, expected_version, current_version in package_diff: | |
print('{}\t{}\t{}'.format(package, expected_version, current_version)) | |
sys.exit(1) | |
else: | |
sys.exit(0) | |
def test(): | |
dep_text = ''' | |
# some comment | |
Django==1.2.3 | |
-some-arg | |
'''.strip() | |
dep_parsed = dict(parse_dependencies(dep_text)) | |
assert dep_parsed == {'django': '1.2.3'} | |
diff = list(compare_dependencies({'django': '1.2.3'}, {'django': '1.2.3'})) | |
assert diff == [] | |
diff = list(compare_dependencies({'django': '1.2.3'}, {'django': '1.2.3.0'})) | |
assert diff == [] | |
diff = list(compare_dependencies({'django': '1.2.3.0'}, {'django': '1.2.3'})) | |
assert diff == [] | |
diff = list(compare_dependencies({'django': None}, {'django': '1.2.3'})) | |
assert diff == [] | |
diff = list(compare_dependencies({'django': '1.3.4'}, {'django': '1.2.3'})) | |
assert diff == [('django', '1.3.4', '1.2.3')] | |
diff = list(compare_dependencies({'django': '1.2.3'}, {})) | |
assert diff == [('django', '1.2.3', '*missing*')] | |
diff = list(compare_dependencies({'django': None}, {})) | |
assert diff == [('django', '*any*', '*missing*')] | |
diff = list(compare_dependencies({'django': '1.2.3.1'}, {'django': '1.2.3'})) | |
assert diff == [('django', '1.2.3.1', '1.2.3')] | |
# diff = list(compare_dependencies({'django': '1.2.3.zapier'}, {'django': '1.2.3'})) | |
# assert diff == [('django', '1.2.3.zapier', '1.2.3')] | |
if __name__ == '__main__': | |
""" | |
Use like this: | |
./compare-pips.py | |
./compare-pips.py other_requirements.txt | |
./compare-pips.py deps/base.txt --ignore imapclient pyopenssl | |
./compare-pips.py deps/base.txt deps/dev.txt --ignore imapclient pyopenssl | |
Run tests like this: | |
./compare-pips.py -t | |
""" | |
parser = argparse.ArgumentParser( | |
description='Compare currently installed packages with requirements files.', | |
) | |
parser.add_argument( | |
'files', | |
metavar='requirements.txt', | |
type=str, | |
nargs='*', | |
default=['requirements.txt'], | |
help='Which requirements files should we parse. Provide one or many.', | |
) | |
parser.add_argument( | |
'--ignore', | |
metavar='Django', | |
nargs='*', | |
dest='ignore', | |
help='Which should be ignored packages. Provide none, one or many.', | |
) | |
parser.add_argument( | |
'-t', | |
action='store_true', | |
dest='test', | |
default=False, | |
help='Run tests.', | |
) | |
args = parser.parse_args() | |
if args.test: | |
test() | |
else: | |
main(args.files, args.ignore) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment