summaryrefslogtreecommitdiff
path: root/exts/pip_find_deps.py
diff options
context:
space:
mode:
Diffstat (limited to 'exts/pip_find_deps.py')
-rwxr-xr-xexts/pip_find_deps.py245
1 files changed, 245 insertions, 0 deletions
diff --git a/exts/pip_find_deps.py b/exts/pip_find_deps.py
new file mode 100755
index 0000000..3666afe
--- /dev/null
+++ b/exts/pip_find_deps.py
@@ -0,0 +1,245 @@
+#!/usr/bin/env python
+#
+# Find the build and runtime dependencies for a given Python package
+#
+# Copyright (C) 2014 Codethink Limited
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from __future__ import print_function
+
+import sys
+import subprocess
+import os
+
+import pkg_resources
+import xmlrpclib
+
+DEBUG = False
+
+# TODO: I'm guessing these things are probably standard somewhere
+def warn(*args, **kwargs):
+ print('%s:' % sys.argv[0], *args, file=sys.stderr, **kwargs)
+
+def error(*args, **kwargs):
+ warn(*args, **kwargs)
+ sys.exit(1)
+
+def debug(s):
+ if DEBUG:
+ print(s)
+
+# TODO: this is copied from pip_lorry, it should be shared.
+def specs_satisfied(version, specs):
+ def mapping_error(op):
+ # We parse ops with requirements-parser, so any invalid user input
+ # should be detected there. This really guards against
+ # the pip developers adding some new operation to a requirement.
+ error("Invalid op in spec: %s" % op)
+
+ opmap = {'==' : lambda x, y: x == y, '!=' : lambda x, y: x != y,
+ '<=' : lambda x, y: x <= y, '>=' : lambda x, y: x >= y,
+ '<': lambda x, y: x < y, '>' : lambda x, y: x > y}
+
+ def get_op_func(op):
+ return opmap[op] if op in opmap else lambda x, y: mapping_error(op)
+
+ return all([get_op_func(op)(version, sv) for (op, sv) in specs])
+
+def conflict(specs, spec):
+ # Return whether spec_x conflicts with spec_y
+ # examples: ('==', '0.1') conflicts with ('==', '0.2')
+
+ # ('==', '0.1') conflicts with ('<', 0.1)
+ # ('==', '0.1') conflicts with ('>', 0.1)
+ # ('==', '0.1') conflicts with ('!=', 0.1)
+
+ # ('<', '0.1') conflicts with ('>', '0.1')
+ # ('<=', 0.1) conflicts with ('>=', 0.2)
+ # ('<', '0.1') conflicts with ('>', '0.2')
+ pass
+
+class Dependency(object):
+ def __init__(self):
+ # non-inclusive boundaries
+ self.less_than = None
+ self.greater_than = None
+
+ self.absolute = None
+ self.excludes = []
+
+ # A list of conclicting specs for this dependency
+ self.conflicts = []
+
+ def is_unbounded(self):
+ return self.less_than == None and self.greater_than == None
+
+ def _in_less_than(self, version):
+ return True if self.less_than == None else version < self.less_than
+
+ def _in_greater_than(self, version):
+ return True if self.greater_than == None else version > self.greater_than
+
+ def in_bounds(self, version):
+ return self.is_unbounded() or (self._in_less_than(version)
+ and self._in_greater_than(version))
+
+ def get_bounds_conflict(self, version):
+ if not self._in_less_than(version):
+ return ('<', str(self.less_than))
+ elif not self._in_greater_than(version):
+ return ('>', str(self.greater_than))
+ else:
+ return None
+
+ def is_unconstrained(self):
+ return (self.is_unbounded() and self.excludes == []
+ and self.absolute == None)
+
+ def set_absolute_version(self, version):
+ self.absolute = version
+
+
+def check_eqs(dep, version):
+ if dep.is_unconstrained():
+ dep.set_absolute_version(version)
+ elif version not in dep.excludes:
+ if dep.absolute in [version, None]:
+ if dep.in_bounds(version):
+ dep.set_absolute_version(version)
+ else:
+ warn('conflict! == %s conflicts with %s'
+ % (version,
+ dep.get_bounds_conflict(version)))
+ dep.conflicts.append((('==', version),
+ dep.get_bounds_conflict(version)))
+
+ else:
+ warn('conflict! == %s conflicts with == %s'
+ % (version, dep.absolute))
+ dep.conflicts.append(('==', version), ('!=', dep.absolute))
+ elif version in dep.excludes:
+ warn('conflict! == %s conflicts with != %s'
+ % (version, version))
+ dep.conflicts.append((('==', version), ('!=', version)))
+ elif version > dep.less_than:
+ # conflict
+ warn('conflict! == %s conflicts with < %s'
+ % (version, dep.less_than))
+ dep.conflicts.append((('==', version), ('<', dep.less_than)))
+ elif version < dep.greater_than:
+ # conflict
+ warn('conflict! == %s conflicts with > %s'
+ % (version, dep.greater_than))
+ dep.conflicts.append(('==', version), ('>', dep.greater_than))
+
+def check_lt(dep, version):
+ if dep.is_unconstrained():
+ dep.less_than = version
+ elif dep.greater_than >= version:
+ # conflict #(our minimum version is greater
+ # than this greater_than version)
+ warn('conflict! > %s conflicts with < %s'
+ % (dep.greater_than, version))
+ dep.conflicts.append((('>', dep.greater_than), ('<', version)))
+ else:
+ dep.less_than = version
+
+def check_gt(dep, version):
+ if dep.is_unconstrained():
+ dep.greater_than = version
+ elif dep.less_than <= version:
+ # conflict (our maximum version is less than this
+ # less_than version)
+ warn('conflict! < %s conflicts with > %s'
+ % (dep.less_than, version))
+ dep.conflicts.append((('<', dep.less_than), ('>', version)))
+
+def resolve_version_constraints(requirements):
+ build_deps = {}
+
+ for r in requirements:
+ print('%s %s' % (r.project_name, r.specs))
+
+ if r.project_name not in build_deps:
+ build_deps[r.project_name] = Dependency()
+
+ for (op, version) in r.specs:
+ version = pkg_resources.parse_version(version)
+
+ #if no_conflict(requirements_versions_map[r.project_name], s):
+ # requirements_versions_map[r.project_name] += s
+ dep = build_deps[r.project_name]
+
+ print('dep.excludes: %s' % str(dep.excludes))
+
+ # TODO: replace with function table
+ if op == '==':
+ check_eqs(dep, version)
+ elif op == '!=':
+ dep.excludes.append(version)
+ # TODO: chk_eqs needed here
+ elif op == '<':
+ check_lt(dep, version)
+ elif op == '>':
+ check_gt(dep, version)
+
+ # Resolve versions
+ #client = xmlrpclib.ServerProxy(PYPI_URL)
+ #releases = client.package_releases(requirement.name)
+ return build_deps
+
+def find_build_deps(source, name, version=None):
+ debug('source: %s' % source)
+ debug('name: %s' % name)
+ debug('version: %s' % version)
+
+ # This amounts to running python setup.py egg_info and checking
+ # the resulting egg_info dir for a file called setup_requires.txt
+
+ # So it's $name.egg_info
+ p = subprocess.Popen(['python', 'setup.py', 'egg_info'], cwd=source,
+ stdout=subprocess.PIPE)
+
+ if p.wait() != 0:
+ error('egg_info command failed')
+
+ egg_dir = '%s.egg-info' % name
+ build_deps_file = os.path.join(source, egg_dir, 'setup_requires.txt')
+
+ build_deps = {}
+
+ # Check whether there's a setup_requires.txt
+ if not os.path.isfile(build_deps_file):
+ print('%s has no build dependencies' % name)
+ else:
+ with open(build_deps_file) as f:
+ build_deps = resolve_version_constraints(
+ pkg_resources.parse_requirements(f))
+
+ return build_deps
+
+if len(sys.argv) not in [3, 4]:
+ print('usage: %s PACKAGE_SOURCE_DIR NAME [VERSION]' % sys.argv[0])
+ sys.exit(1)
+
+# Ignore the issue of dependency conflicts for now
+
+# First, given a source return build dependencies in json from
+
+for name, dep in find_build_deps(*sys.argv[1:]).iteritems():
+ print('%s less_than: %s greater_than: %s absolute: %s excludes: %s'
+ % (name, str(dep.less_than), str(dep.greater_than),
+ str(dep.absolute), dep.excludes))
+ print('conflicts: %s' % str(dep.conflicts)) \ No newline at end of file