From 3aa925bebb500820e306bd44b6524b9537f594fc Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 24 Jun 2021 21:32:27 -0400 Subject: [PATCH] Adds support for multiple requirements for a single package name (#12232) Closes #12186 by grouping requirements that reference the same pip dependency name. [ci skip-build-wheels] --- .../python/macros/python_requirements.py | 15 ++++-- .../python/macros/python_requirements_test.py | 52 ++++++++++++++++++- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/python/pants/backend/python/macros/python_requirements.py b/src/python/pants/backend/python/macros/python_requirements.py index edce1ea0a60..690534a6191 100644 --- a/src/python/pants/backend/python/macros/python_requirements.py +++ b/src/python/pants/backend/python/macros/python_requirements.py @@ -1,6 +1,7 @@ # Copyright 2014 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). import os +from itertools import groupby from pathlib import Path from typing import Iterable, Mapping, Optional @@ -69,16 +70,20 @@ def __call__( requirements = parse_requirements_file( req_file.read_text(), rel_path=str(req_file.relative_to(get_buildroot())) ) - for parsed_req in requirements: + + grouped_requirements = groupby(requirements, lambda parsed_req: parsed_req.project_name) + + for project_name, parsed_reqs_ in grouped_requirements: + parsed_reqs = list(parsed_reqs_) req_module_mapping = ( - {parsed_req.project_name: module_mapping[parsed_req.project_name]} - if module_mapping and parsed_req.project_name in module_mapping + {project_name: module_mapping[project_name]} + if module_mapping and project_name in module_mapping else None ) self._parse_context.create_object( "python_requirement_library", - name=parsed_req.project_name, - requirements=[parsed_req], + name=project_name, + requirements=parsed_reqs, module_mapping=req_module_mapping, dependencies=[requirements_dep], ) diff --git a/src/python/pants/backend/python/macros/python_requirements_test.py b/src/python/pants/backend/python/macros/python_requirements_test.py index 932d8310127..caf650816bc 100644 --- a/src/python/pants/backend/python/macros/python_requirements_test.py +++ b/src/python/pants/backend/python/macros/python_requirements_test.py @@ -40,12 +40,13 @@ def assert_python_requirements( Targets, [Specs(AddressSpecs([DescendantAddresses("")]), FilesystemSpecs([]))], ) + assert {expected_file_dep, *expected_targets} == set(targets) def test_requirements_txt(rule_runner: RuleRunner) -> None: """This tests that we correctly create a new python_requirement_library for each entry in a - requirements.txt file. + requirements.txt file, where each dependency is unique. Some edge cases: * We ignore comments and options (values that start with `--`). @@ -104,6 +105,55 @@ def test_requirements_txt(rule_runner: RuleRunner) -> None: ) +def test_multiple_versions(rule_runner: RuleRunner) -> None: + """This tests that we correctly create a new python_requirement_library for each unique + dependency name in a requirements.txt file, grouping duplicated dependency names to handle + multiple requirement strings per PEP 508.""" + + assert_python_requirements( + rule_runner, + "python_requirements(module_mapping={'ansicolors': ['colors']})", + dedent( + """\ + Django>=3.2 + Django==3.2.7 + confusedmonkey==86 + repletewateringcan>=7 + """ + ), + expected_file_dep=PythonRequirementsFile( + {"sources": ["requirements.txt"]}, + Address("", target_name="requirements.txt"), + ), + expected_targets=[ + PythonRequirementLibrary( + { + "dependencies": [":requirements.txt"], + "requirements": [ + Requirement.parse("Django>=3.2"), + Requirement.parse("Django==3.2.7"), + ], + }, + Address("", target_name="Django"), + ), + PythonRequirementLibrary( + { + "dependencies": [":requirements.txt"], + "requirements": [Requirement.parse("confusedmonkey==86")], + }, + Address("", target_name="confusedmonkey"), + ), + PythonRequirementLibrary( + { + "dependencies": [":requirements.txt"], + "requirements": [Requirement.parse("repletewateringcan>=7")], + }, + Address("", target_name="repletewateringcan"), + ), + ], + ) + + def test_invalid_req(rule_runner: RuleRunner) -> None: """Test that we give a nice error message.""" fake_file_tgt = PythonRequirementsFile({"sources": ["doesnt matter"]}, Address("doesnt_matter"))