Use traditional Unix style for multipip

multipip output can be parsed by pip tool. All errors
go to stderr. Anvil reports where incompatible requirements
were found.

Change-Id: Iba110e10ed3e2580d60ef475ac877190d1d03fcc
Fixes: bug #1188080
This commit is contained in:
Alessio Ababilov
2013-06-06 09:16:43 +04:00
committed by Joshua Harlow
parent 06fc741026
commit f016749dd0
3 changed files with 71 additions and 83 deletions

View File

@@ -18,11 +18,9 @@
# R0921: Abstract class not referenced # R0921: Abstract class not referenced
#pylint: disable=R0902,R0921 #pylint: disable=R0902,R0921
import json
import pkg_resources import pkg_resources
from anvil import colorizer from anvil import colorizer
from anvil import exceptions as excp
from anvil import log as logging from anvil import log as logging
from anvil import shell as sh from anvil import shell as sh
from anvil import utils from anvil import utils
@@ -151,7 +149,7 @@ class DependencyHandler(object):
req = pkg_resources.Requirement.parse(line) req = pkg_resources.Requirement.parse(line)
new_lines.append(str(forced_by_key[req.key])) new_lines.append(str(forced_by_key[req.key]))
except: except:
# We don't force the package or it has a bad format # we don't force the package or it has a bad format
new_lines.append(line) new_lines.append(line)
contents = "# Cleaned on %s\n\n%s\n" % ( contents = "# Cleaned on %s\n\n%s\n" % (
utils.iso8601(), "\n".join(new_lines)) utils.iso8601(), "\n".join(new_lines))
@@ -173,46 +171,34 @@ class DependencyHandler(object):
] ]
cmdline = cmdline + extra_pips + ["-r"] + requires_files cmdline = cmdline + extra_pips + ["-r"] + requires_files
output = sh.execute(cmdline) output = sh.execute(cmdline, check_exit_code=False)
contents = json.loads(output[0]) conflict_descr = output[1].strip()
# Figure out which ones were easily matched.
self.pips_to_install = []
for pkg in contents.get('compatibles', []):
if pkg.lower() not in OPENSTACK_PACKAGES:
self.pips_to_install.append(pkg)
# Figure out which ones we are forced to install.
forced_keys = set() forced_keys = set()
for (k, req_list) in contents.get('incompatibles', {}): if conflict_descr:
forced_keys.add(k.lower()) for line in conflict_descr.splitlines():
# Select the first. LOG.warning(line)
if req_list and req_list[0] not in self.pips_to_install: if line.endswith(": incompatible requirements"):
self.pips_to_install.append(req_list[0]) forced_keys.add(line.split(":", 1)[0].lower())
self.pips_to_install = [
pkg
for pkg in utils.splitlines_not_empty(output[0])
if pkg.lower() not in OPENSTACK_PACKAGES]
sh.write_file(self.gathered_requires_filename,
"\n".join(self.pips_to_install))
if not self.pips_to_install: if not self.pips_to_install:
LOG.error("No dependencies for OpenStack found. " LOG.error("No dependencies for OpenStack found."
"Something went wrong. Please check:") "Something went wrong. Please check:")
LOG.error("'%s'" % "' '".join(cmdline)) LOG.error("'%s'" % "' '".join(cmdline))
raise excp.AnvilException("No dependencies for OpenStack found!") raise RuntimeError("No dependencies for OpenStack found")
utils.log_iterable(sorted(self.pips_to_install), utils.log_iterable(sorted(self.pips_to_install),
logger=LOG, logger=LOG,
header="Python dependency list (all)") header="Full known Python dependency list")
sh.write_file(self.gathered_requires_filename,
"\n".join(self.pips_to_install))
self.forced_packages = [] self.forced_packages = []
for k in forced_keys: for pip in self.pips_to_install:
for pip in self.pips_to_install: req = pkg_resources.Requirement.parse(pip)
req = pkg_resources.Requirement.parse(pip) if req.key in forced_keys:
if req.key == k: self.forced_packages.append(req)
self.forced_packages.append(req)
break
if self.forced_packages:
utils.log_iterable(sorted(self.forced_packages),
logger=LOG,
header="Python dependency list (forced)")
sh.write_file(self.forced_requires_filename, sh.write_file(self.forced_requires_filename,
"\n".join(str(req) for req in self.forced_packages)) "\n".join(str(req) for req in self.forced_packages))

View File

@@ -12,23 +12,34 @@ multipip
Use `multipip` to join these requirements:: Use `multipip` to join these requirements::
$ multipip 'nose>=1.2' 'nose>=2' 'nose<4' $ multipip 'nose>=1.2' 'nose>=2' 'nose<4'
{"compatibles": ["nose>=2,<4"], "incompatibles": {}} nose>=2,<4
`multipip` can be used to run `pip`::
$ pip install $(multipip -r pip-requires)
...
Files of requirements can be used as well:: Files of requirements can be used as well::
$ cat pip-requires $ cat pip-requires
nose<4 nose<4
$ multipip 'nose>=1.2' 'nose>=2' -r pip-requires $ multipip 'nose>=1.2' 'nose>=2' -r pip-requires
{"compatibles": ["nose>=2,<4"], "incompatibles": {}} nose>=2,<4
`multipip` prints error messages for badly formated requirements and exits early `multipip` prints error messages for incompatible requirements to
and for incompatible requirements provides you which package was incompatible stderr and chooses the first one (note: command-line requirements take
and which versions were found to be problematic:: precedence over files)::
$ cat pip-requires $ cat pip-requires
pip==1.3 pip==1.3
$ multipip 'pip==1.2' -r pip-requires $ multipip 'pip==1.2' -r pip-requires
{"compatibles": [], "incompatibles": {"pip": ["pip==1.2", "pip==1.3"]}} pip: incompatible requirements
Choosing:
command line: pip==1.2
Conflicting:
-r pip-requires (line 1): pip==1.3
pip==1.2
It is possible to filter some packages from printed output. This can It is possible to filter some packages from printed output. This can
be useful for a huge `pip-requires` file:: be useful for a huge `pip-requires` file::
@@ -38,7 +49,7 @@ be useful for a huge `pip-requires` file::
pip==1.2 pip==1.2
nose>=1.2 nose>=1.2
$ multipip -r pip-requires --ignore-packages nose $ multipip -r pip-requires --ignore-packages nose
{"compatibles": ["pip==1.2"], "incompatibles": {}} pip==1.2
Installed packages can be filtered, too (they are taken from `pip Installed packages can be filtered, too (they are taken from `pip
freeze`):: freeze`)::
@@ -50,7 +61,7 @@ freeze`)::
$ pip freeze | grep nose $ pip freeze | grep nose
nose==1.1.2 nose==1.1.2
$ multipip -r pip-requires --ignore-installed $ multipip -r pip-requires --ignore-installed
{"compatibles": ["pip==1.2"], "incompatibles": {}} pip==1.2
py2rpm py2rpm
------ ------

View File

@@ -1,9 +1,7 @@
#!/usr/bin/python #!/usr/bin/python
import argparse import argparse
import collections
import distutils.spawn import distutils.spawn
import json
import logging import logging
import os import os
import subprocess import subprocess
@@ -16,6 +14,7 @@ import pkg_resources
BAD_REQUIREMENTS = 2 BAD_REQUIREMENTS = 2
INCOMPATIBLE_REQUIREMENTS = 3
logger = logging.getLogger() logger = logging.getLogger()
@@ -85,7 +84,7 @@ def setup_logging(options):
logger.setLevel(level) logger.setLevel(level)
incompatibles = collections.defaultdict(list) incompatibles = set()
joined_requirements = [] joined_requirements = []
@@ -118,15 +117,18 @@ def install_requirement_parse(line, comes_from):
return install_requirement_ensure_req_field(req) return install_requirement_ensure_req_field(req)
def add_incompatible_requirement(chosen, conflicting): def incompatible_requirement(chosen, conflicting):
global incompatibles if chosen.req.key not in incompatibles:
incompatibles.add(chosen.req.key)
key = chosen.req.key print >> sys.stderr, "%s: incompatible requirements" % chosen.req.key
if key not in incompatibles: print >> sys.stderr, "Choosing:"
incompatibles[key].extend([install_requirement_str(conflicting), print >> sys.stderr, ("\t%s: %s" %
install_requirement_str(chosen)]) (chosen.comes_from,
else: install_requirement_str(chosen)))
incompatibles[key].append(install_requirement_str(chosen)) print >> sys.stderr, "Conflicting:"
print >> sys.stderr, ("\t%s: %s" %
(conflicting.comes_from,
install_requirement_str(conflicting)))
def parse_requirements(options): def parse_requirements(options):
@@ -134,24 +136,22 @@ def parse_requirements(options):
:return: tuple (all, ignored) of InstallRequirement :return: tuple (all, ignored) of InstallRequirement
""" """
all_requirements = collections.defaultdict(list) all_requirements = {}
# CLI directly provided requirements first. for req_spec in options.requirement_specs:
try:
req = install_requirement_parse(req_spec, "command line")
all_requirements.setdefault(req.req.key, []).append(req)
except Exception as ex:
logger.error("Cannot parse `%s': %s" % (req_spec, ex))
sys.exit(BAD_REQUIREMENTS)
for filename in options.requirements: for filename in options.requirements:
try: try:
for req in pip.req.parse_requirements(filename, options=options): for req in pip.req.parse_requirements(filename, options=options):
req = install_requirement_ensure_req_field(req) req = install_requirement_ensure_req_field(req)
all_requirements[req.req.key].append(req) all_requirements.setdefault(req.req.key, []).append(req)
except Exception as ex: except Exception as ex:
logger.error("Cannot parse `%s': %s" % (filename, ex)) logger.error("Cannot parse `%s': %s" % (filename, ex))
sys.exit(BAD_REQUIREMENTS) sys.exit(BAD_REQUIREMENTS)
# CLI provided files after that.
for req_spec in options.requirement_specs:
try:
req = install_requirement_parse(req_spec, "command line")
all_requirements[req.req.key].append(req)
except Exception as ex:
logger.error("Cannot parse `%s': %s" % (req_spec, ex))
sys.exit(BAD_REQUIREMENTS)
ignored_requirements = [] ignored_requirements = []
for req_spec in options.ignore_packages: for req_spec in options.ignore_packages:
try: try:
@@ -249,7 +249,6 @@ def join_one_requirement(req_list):
def join_requirements(options): def join_requirements(options):
global joined_requirements global joined_requirements
all_requirements, ignored_requirements = parse_requirements(options) all_requirements, ignored_requirements = parse_requirements(options)
skip_keys = set(pkg.req.key for pkg in ignored_requirements) skip_keys = set(pkg.req.key for pkg in ignored_requirements)
installed_by_key = {} installed_by_key = {}
@@ -276,7 +275,7 @@ def join_requirements(options):
"%s>=%s" % (installed_req.project_name, "%s>=%s" % (installed_req.project_name,
installed_req.specs[0][1]), installed_req.specs[0][1]),
"pip freeze") "pip freeze")
add_incompatible_requirement(frozen_req, joined_req) incompatible_requirement(frozen_req, joined_req)
joined_req = frozen_req joined_req = frozen_req
joined_requirements.append(joined_req.req) joined_requirements.append(joined_req.req)
@@ -298,36 +297,26 @@ def join_requirements(options):
if exact_version: if exact_version:
for req in req_list: for req in req_list:
if not exact_version in req.req: if not exact_version in req.req:
add_incompatible_requirement(joined_req, req) incompatible_requirement(joined_req, req)
else: else:
for req in req_list: for req in req_list:
for parsed, trans, op, ver in req.req.index: for parsed, trans, op, ver in req.req.index:
if op[0] == "=": if op[0] == "=":
if parsed in conflicts: if parsed in conflicts:
add_incompatible_requirement(joined_req, req) incompatible_requirement(joined_req, req)
break break
elif not segment_ok and op[0] == "<": elif not segment_ok and op[0] == "<":
# analyse lower bound: x >= A or x > A # analyse lower bound: x >= A or x > A
if (lower_version > parsed or ( if (lower_version > parsed or (
lower_version == parsed and lower_version == parsed and
(lower_strict or len(op) != 2))): (lower_strict or len(op) != 2))):
add_incompatible_requirement(joined_req, req) incompatible_requirement(joined_req, req)
break break
def print_requirements(): def print_requirements():
global joined_requirements for req in sorted(joined_requirements, key=lambda x: x.key):
global incompatibles print req
requirements = {
'compatibles': [],
'incompatibles': {},
}
for r in joined_requirements:
if r.key not in incompatibles:
requirements['compatibles'].append(str(r))
requirements['incompatibles'].update(incompatibles)
print(json.dumps(requirements))
def main(): def main():
@@ -336,6 +325,8 @@ def main():
setup_logging(options) setup_logging(options)
join_requirements(options) join_requirements(options)
print_requirements() print_requirements()
if incompatibles:
sys.exit(INCOMPATIBLE_REQUIREMENTS)
if __name__ == "__main__": if __name__ == "__main__":