Last active
January 18, 2018 10:23
-
-
Save simon-engledew/5a89313aa78692e8e6a8adc2f1e0d832 to your computer and use it in GitHub Desktop.
Requirements solver
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
# -*- coding: utf-8 -*- | |
""" | |
Generates a requirements.txt from any packages | |
in the current working directory. | |
""" | |
import os | |
import glob | |
import contextlib | |
import functools | |
import argparse | |
import setuptools | |
import pkg_resources | |
class UnspecifiedRequirementError(AssertionError): | |
pass | |
def _assert_requirement_pinned(requirement): | |
if not requirement.specifier: | |
raise UnspecifiedRequirementError( | |
'Missing specifier for {!r}'.format(requirement.name) | |
) | |
return True | |
def _filter_requirements( | |
predicate, output, install_requires=None, | |
extras_require=None, dependency_links=None, | |
*_args, **_kwargs | |
): | |
if install_requires: | |
output.update( | |
str(requirement) for requirement in | |
pkg_resources.parse_requirements(install_requires) | |
if predicate(requirement, extra=None) | |
) | |
if dependency_links: | |
output.update(dependency_links) | |
if extras_require: | |
for extra, requirements in extras_require.items(): | |
output.update( | |
str(requirement) for requirement in | |
pkg_resources.parse_requirements(requirements) | |
if predicate(requirement, extra=extra) | |
) | |
@contextlib.contextmanager | |
def patch(target, attr, fn): | |
""" | |
Replace a function for the duration of the context manager | |
""" | |
original = getattr(target, attr) | |
try: | |
setattr(target, attr, fn) | |
yield | |
finally: | |
setattr(target, attr, original) | |
def find_requirements(predicate, *paths): | |
""" | |
Return the list of requirements for a setup.py | |
""" | |
requirements = set() | |
setup = functools.partial( | |
_filter_requirements, predicate, requirements | |
) | |
try: | |
with patch(setuptools, 'setup', setup): | |
for path in paths: | |
with open(path, 'r') as handle: | |
exec( | |
handle.read(), | |
{'__file__': os.path.realpath(path)}, | |
None | |
) | |
except UnspecifiedRequirementError as err: | |
raise UnspecifiedRequirementError( | |
'{} in {}'.format( | |
err.message, path | |
) | |
) | |
for requirement in requirements: | |
yield requirement | |
def main(): | |
""" | |
Print a requirements.txt based on the | |
install_requires and dependency_links of our setup.py files | |
""" | |
parser = argparse.ArgumentParser( | |
description='Generate a requirements.txt from setup.pys' | |
) | |
parser.add_argument( | |
'--extras', | |
help='Generate an requirements.txt that only includes extra', | |
action='append', | |
default=[None] | |
) | |
parser.add_argument( | |
'setups', | |
nargs='+', | |
help='Target setup.py files to scan for requirements' | |
) | |
args = parser.parse_args() | |
print("""# | |
# autogenerated by requirements.py for build/osirium-venv | |
# | |
""") | |
requirements = sorted( | |
find_requirements( | |
lambda requirement, extra: _assert_requirement_pinned(requirement) and extra in args.extras, | |
*args.setups | |
), | |
key=str.lower | |
) | |
for requirement in requirements: | |
print(requirement) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment