diff --git a/piptools/resolver.py b/piptools/resolver.py index 4a475e7d1..c759b0ab0 100644 --- a/piptools/resolver.py +++ b/piptools/resolver.py @@ -79,6 +79,8 @@ def combine_install_requirements( combined_ireq.req.specifier &= ireq.req.specifier combined_ireq.constraint &= ireq.constraint combined_ireq.extras = {*combined_ireq.extras, *ireq.extras} + if combined_ireq.req is not None: + combined_ireq.req.extras = set(combined_ireq.extras) # InstallRequirements objects are assumed to come from only one source, and # so they support only a single comes_from entry. This function breaks this diff --git a/tests/test_resolver.py b/tests/test_resolver.py index 03f6e9d9b..7978fb1f7 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -1,4 +1,5 @@ import pytest +from pip._internal.utils.urls import path_to_url from piptools.exceptions import NoCandidateFound from piptools.resolver import RequirementSummary, combine_install_requirements @@ -299,6 +300,43 @@ def test_combine_install_requirements(repository, from_line): assert str(combined_all.req.specifier) == "<3.2,==3.1.1,>3.0" +def test_combine_install_requirements_extras(repository, from_line, make_package): + """ + Extras should be unioned in combined install requirements + (whether or not InstallRequirement.req is None, and testing either order of the inputs) + """ + with_extra = from_line("edx-opaque-keys[django]==1.0.1") + assert with_extra.req is not None + without_extra = from_line("edx-opaque-keys") + assert without_extra.req is not None + + combined = combine_install_requirements(repository, [without_extra, with_extra]) + assert str(combined) == str(with_extra) + assert combined.extras == with_extra.extras + + combined = combine_install_requirements(repository, [with_extra, without_extra]) + assert str(combined) == str(with_extra) + assert combined.extras == with_extra.extras + + test_package = make_package("test-package", extras_require={"extra": []}) + local_package_with_extra = from_line(f"{test_package}[extra]") + assert local_package_with_extra.req is None + local_package_without_extra = from_line(path_to_url(test_package)) + assert local_package_without_extra.req is None + + combined = combine_install_requirements( + repository, [local_package_without_extra, local_package_with_extra] + ) + assert str(combined) == str(local_package_with_extra) + assert combined.extras == local_package_with_extra.extras + + combined = combine_install_requirements( + repository, [local_package_with_extra, local_package_without_extra] + ) + assert str(combined) == str(local_package_with_extra) + assert combined.extras == local_package_with_extra.extras + + def test_compile_failure_shows_provenance(resolver, from_line): """ Provenance of conflicting dependencies should be printed on failure.