Skip to content

Instantly share code, notes, and snippets.

@bryanhelmig
Created December 26, 2017 20:55
Show Gist options
  • Save bryanhelmig/895d130749ee0eb2e43a98fba32ac180 to your computer and use it in GitHub Desktop.
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.
#!/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