diff --git a/CHANGELOG.md b/CHANGELOG.md index 020ac6bb2..902b16430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Added a `--max-rounds` argument to the pip-compile command to allow for solving large requirement sets ([#472](https://github.com/jazzband/pip-tools/pull/472)) - Exclude unsafe packages' dependencies when `--allow-unsafe` is not in use (#445) +- Exclude irrelevant pip constraints ([#471](https://github.com/jazzband/pip-tools/pull/471)) # 1.8.2 diff --git a/piptools/repositories/local.py b/piptools/repositories/local.py index 1454a0d07..ea3a39b98 100644 --- a/piptools/repositories/local.py +++ b/piptools/repositories/local.py @@ -53,7 +53,7 @@ def find_best_match(self, ireq, prereleases=None): if existing_pin and ireq_satisfied_by_existing_pin(ireq, existing_pin): project, version, _ = as_tuple(existing_pin) return make_install_requirement( - project, version, ireq.extras + project, version, ireq.extras, constraint=ireq.constraint ) else: return self.repository.find_best_match(ireq, prereleases) diff --git a/piptools/repositories/pypi.py b/piptools/repositories/pypi.py index 5541aca4c..080874ae9 100644 --- a/piptools/repositories/pypi.py +++ b/piptools/repositories/pypi.py @@ -118,7 +118,7 @@ def find_best_match(self, ireq, prereleases=None): # Turn the candidate into a pinned InstallRequirement return make_install_requirement( - best_candidate.project, best_candidate.version, ireq.extras + best_candidate.project, best_candidate.version, ireq.extras, constraint=ireq.constraint ) def get_dependencies(self, ireq): diff --git a/piptools/resolver.py b/piptools/resolver.py index 19610e9f1..d4e348da0 100644 --- a/piptools/resolver.py +++ b/piptools/resolver.py @@ -90,7 +90,8 @@ def resolve(self, max_rounds=10): self.dependency_cache.clear() self.repository.clear_caches() - self._check_constraints() + self.check_constraints(chain(self.our_constraints, + self.their_constraints)) # Ignore existing packages os.environ[str('PIP_EXISTS_ACTION')] = str('i') # NOTE: str() wrapping necessary for Python 2/3 compat @@ -118,10 +119,12 @@ def resolve(self, max_rounds=10): self.repository.freshen_build_caches() del os.environ['PIP_EXISTS_ACTION'] - return best_matches + # Only include hard requirements and not pip constraints + return {req for req in best_matches if not req.constraint} - def _check_constraints(self): - for constraint in chain(self.our_constraints, self.their_constraints): + @staticmethod + def check_constraints(constraints): + for constraint in constraints: if constraint.link is not None and not constraint.editable: msg = ('pip-compile does not support URLs as packages, unless they are editable. ' 'Perhaps add -e option?') @@ -157,6 +160,7 @@ def _group_constraints(self, constraints): for ireq in ireqs: # NOTE we may be losing some info on dropped reqs here combined_ireq.req.specifier &= ireq.req.specifier + combined_ireq.constraint &= ireq.constraint # Return a sorted, de-duped tuple of extras combined_ireq.extras = tuple(sorted(set(tuple(combined_ireq.extras) + tuple(ireq.extras)))) yield combined_ireq @@ -275,7 +279,7 @@ def _iter_dependencies(self, ireq): log.debug(' {:25} requires {}'.format(format_requirement(ireq), ', '.join(sorted(dependency_strings, key=lambda s: s.lower())) or '-')) for dependency_string in dependency_strings: - yield InstallRequirement.from_line(dependency_string) + yield InstallRequirement.from_line(dependency_string, constraint=ireq.constraint) def reverse_dependencies(self, ireqs): non_editable = [ireq for ireq in ireqs if not ireq.editable] diff --git a/piptools/scripts/compile.py b/piptools/scripts/compile.py index 7e8f40d2f..bb743ef8b 100755 --- a/piptools/scripts/compile.py +++ b/piptools/scripts/compile.py @@ -176,6 +176,12 @@ def cli(verbose, dry_run, pre, rebuild, find_links, index_url, extra_index_url, constraints.extend(parse_requirements( src_file, finder=repository.finder, session=repository.session, options=pip_options)) + # Check the given base set of constraints first + Resolver.check_constraints(constraints) + + # The requirement objects are modified in-place so we need to save off the list of primary packages first + primary_packages = {key_from_req(ireq.req) for ireq in constraints if not ireq.constraint} + try: resolver = Resolver(constraints, repository, prereleases=pre, clear_caches=rebuild, allow_unsafe=allow_unsafe) @@ -230,7 +236,7 @@ def cli(verbose, dry_run, pre, rebuild, find_links, index_url, extra_index_url, format_control=repository.finder.format_control) writer.write(results=results, reverse_dependencies=reverse_dependencies, - primary_packages={key_from_req(ireq.req) for ireq in constraints}, + primary_packages=primary_packages, markers={key_from_req(ireq.req): ireq.markers for ireq in constraints if ireq.markers}, hashes=hashes) diff --git a/piptools/utils.py b/piptools/utils.py index 6638e756a..679a145f0 100644 --- a/piptools/utils.py +++ b/piptools/utils.py @@ -51,14 +51,14 @@ def comment(text): return style(text, fg='green') -def make_install_requirement(name, version, extras): +def make_install_requirement(name, version, extras, constraint=False): # If no extras are specified, the extras string is blank extras_string = "" if extras: # Sort extras for stability extras_string = "[{}]".format(",".join(sorted(extras))) - return InstallRequirement.from_line('{}{}=={}'.format(name, extras_string, str(version))) + return InstallRequirement.from_line('{}{}=={}'.format(name, extras_string, str(version)), constraint=constraint) def format_requirement(ireq, marker=None): diff --git a/tests/conftest.py b/tests/conftest.py index 82db5fadf..62de67f94 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,7 +37,7 @@ def find_best_match(self, ireq, prereleases=False): if not versions: raise NoCandidateFound(ireq, self.index[key_from_req(ireq.req)]) best_version = max(versions, key=Version) - return make_install_requirement(key_from_req(ireq.req), best_version, ireq.extras) + return make_install_requirement(key_from_req(ireq.req), best_version, ireq.extras, constraint=ireq.constraint) def get_dependencies(self, ireq): if ireq.editable: @@ -47,7 +47,7 @@ def get_dependencies(self, ireq): # Store non-extra dependencies under the empty string extras += ("",) dependencies = [dep for extra in extras for dep in self.index[name][version][extra]] - return [InstallRequirement.from_line(dep) for dep in dependencies] + return [InstallRequirement.from_line(dep, constraint=ireq.constraint) for dep in dependencies] class FakeInstalledDistribution(object): diff --git a/tests/test_resolver.py b/tests/test_resolver.py index f0d60afb6..470e8cf28 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -10,7 +10,7 @@ (['Flask'], ['flask==0.10.1', 'itsdangerous==0.24', 'markupsafe==0.23', - 'jinja2==2.7.3', 'werkzeug==0.10.4']), + 'jinja2==2.7.3', 'werkzeug==0.10.4']), (['Jinja2', 'markupsafe'], ['jinja2==2.7.3', 'markupsafe==0.23']), @@ -98,10 +98,18 @@ # Exclude package dependcy of setuptools as it is unsafe. (['html5lib'], ['html5lib==0.999999999']), + + # We shouldn't include irrelevant pip constraints + # See: GH-471 + (['Flask', ('click', True), ('itsdangerous', True)], + ['flask==0.10.1', 'itsdangerous==0.24', 'markupsafe==0.23', + 'jinja2==2.7.3', 'werkzeug==0.10.4'] + ), ]) ) def test_resolver(resolver, from_line, input, expected, prereleases): - input = [from_line(line) for line in input] + input = [line if isinstance(line, tuple) else (line, False) for line in input] + input = [from_line(req[0], constraint=req[1]) for req in input] output = resolver(input, prereleases=prereleases).resolve() output = {str(line) for line in output} assert output == {str(line) for line in expected}