Repository: pantsbuild/pants

Issue:
Include explicit transitive 3rdparty dependencies in packaged `python_distribution`s install requires
**Is your feature request related to a problem? Please describe.**
When using `overrides` for a `python_requirement` to add missing dependencies (in other words likely for a bad 3rd party dep) it would be sweet if I didn't have to do it again for all the `python_distribution` targets that depend on this 3rd party library.

**Describe the solution you'd like**
Include the explicitly added dependencies on 3rd party deps in the list of required dists in `setup.py`.

**Describe alternatives you've considered**
Manually duplicating the dependency on the `python_distribution`. Tedious and error prone.

**Additional context**
Take this dist for example:

https://github.com/kaos/lockfile-diff/blob/f631695c838811acf5c7b8b00a02d2e4cc5db638/src/lockfile_diff/BUILD#L3-L10

Where I figured the `phx-class-registry` library requires `pkg_resources` from `setuptools` but it isn't listed, so sorted it with this override:

https://github.com/kaos/lockfile-diff/blob/f631695c838811acf5c7b8b00a02d2e4cc5db638/3rdparty/BUILD#L6-L10

However, this version of my lockfile-diff is still broken, as I didn't also include `setuptools` as a dependency for my distribution. Foot-gun blast.


Discussion:

Resolving patch:
diff --git a/src/python/pants/backend/python/goals/setup_py.py b/src/python/pants/backend/python/goals/setup_py.py
--- a/src/python/pants/backend/python/goals/setup_py.py
+++ b/src/python/pants/backend/python/goals/setup_py.py
@@ -91,7 +91,7 @@
 from pants.util.logging import LogLevel
 from pants.util.memo import memoized_property
 from pants.util.meta import frozen_after_init
-from pants.util.ordered_set import FrozenOrderedSet
+from pants.util.ordered_set import FrozenOrderedSet, OrderedSet
 from pants.util.strutil import softwrap
 
 logger = logging.getLogger(__name__)
@@ -862,6 +862,15 @@ async def get_requirements(
     direct_deps_tgts = await MultiGet(
         Get(Targets, DependenciesRequest(tgt.get(Dependencies))) for tgt in owned_by_us
     )
+    direct_deps_chained = OrderedSet(itertools.chain.from_iterable(direct_deps_tgts))
+    # If a python_requirement T has an undeclared requirement R, we recommend fixing that by adding
+    # an explicit dependency from T to a python_requirement target for R. In that case we want to
+    # represent these explicit deps in T's distribution metadata. See issue #17593.
+    transitive_explicit_reqs = await MultiGet(
+        Get(TransitiveTargets, TransitiveTargetsRequest([tgt.address]))
+        for tgt in direct_deps_chained
+        if tgt.has_field(PythonRequirementsField)
+    )
 
     transitive_excludes: FrozenOrderedSet[Target] = FrozenOrderedSet()
     uneval_trans_excl = [
@@ -875,7 +884,9 @@ async def get_requirements(
             itertools.chain.from_iterable(excludes for excludes in nested_trans_excl)
         )
 
-    direct_deps_chained = FrozenOrderedSet(itertools.chain.from_iterable(direct_deps_tgts))
+    direct_deps_chained.update(
+        itertools.chain.from_iterable(t.dependencies for t in transitive_explicit_reqs)
+    )
     direct_deps_with_excl = direct_deps_chained.difference(transitive_excludes)
 
     req_strs = list(


Test patch:
diff --git a/src/python/pants/backend/python/goals/setup_py_test.py b/src/python/pants/backend/python/goals/setup_py_test.py
--- a/src/python/pants/backend/python/goals/setup_py_test.py
+++ b/src/python/pants/backend/python/goals/setup_py_test.py
@@ -980,6 +980,50 @@ def test_get_requirements_with_exclude() -> None:
     )
 
 
+def test_get_requirements_with_override_dependency_issue_17593() -> None:
+    rule_runner = create_setup_py_rule_runner(
+        rules=[
+            determine_explicitly_provided_setup_kwargs,
+            get_requirements,
+            get_owned_dependencies,
+            get_exporting_owner,
+            *target_types_rules.rules(),
+            SubsystemRule(SetupPyGeneration),
+            QueryRule(ExportedTargetRequirements, (DependencyOwner,)),
+        ]
+    )
+    rule_runner.write_files(
+        {
+            "3rdparty/BUILD": textwrap.dedent(
+                """
+                python_requirement(name='ext1', requirements=['ext1==1.22.333'], dependencies=[':ext2'])
+                python_requirement(name='ext2', requirements=['ext2==4.5.6'])
+                """
+            ),
+            "src/python/foo/bar/baz/a.py": "",
+            "src/python/foo/bar/baz/BUILD": "python_sources(dependencies=['3rdparty:ext1'])",
+            "src/python/foo/bar/a.py": "",
+            "src/python/foo/bar/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name='bar-dist',
+                    dependencies=[':bar'],
+                    provides=python_artifact(name='bar', version='9.8.7'),
+                )
+
+                python_sources(dependencies=['src/python/foo/bar/baz'])
+              """
+            ),
+        }
+    )
+
+    assert_requirements(
+        rule_runner,
+        ["ext1==1.22.333", "ext2==4.5.6"],
+        Address("src/python/foo/bar", target_name="bar-dist"),
+    )
+
+
 def assert_requirements(
     rule_runner,
     expected_req_strs,