diff --git a/CHANGELOG.md b/CHANGELOG.md index d7a0ab3534..40ae51bfc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,14 @@ END_UNRELEASED_TEMPLATE Use the `bazel_binary_info` module to access it. The {flag}`--stamp` flag will add {flag}`--workspace_status` information. +{#v1-8-1} +## [1.8.1] - 2026-01-20 + +{#v1-8-1-fixed} +### Fixed +* (pipstar) Extra resolution that refers back to the package being resolved works again. + Fixes [#3524](https://github.com/bazel-contrib/rules_python/issues/3524). + {#v1-8-0} ## [1.8.0] - 2025-12-19 diff --git a/python/private/pypi/pep508_deps.bzl b/python/private/pypi/pep508_deps.bzl index cdb449bdc2..ad6589cfac 100644 --- a/python/private/pypi/pep508_deps.bzl +++ b/python/private/pypi/pep508_deps.bzl @@ -115,7 +115,9 @@ def _resolve_extras(self_name, reqs, extras): # extras The empty string in the set is just a way to make the handling # of no extras and a single extra easier and having a set of {"", "foo"} # is equivalent to having {"foo"}. - extras = extras or [""] + # + # Use a dict as a set here to simplify operations. + extras = {x: None for x in (extras or [""])} self_reqs = [] for req in reqs: @@ -128,26 +130,36 @@ def _resolve_extras(self_name, reqs, extras): # easy to handle, lets do it. # # TODO @aignas 2023-12-08: add a test - extras = extras + req.extras + extras = extras | {x: None for x in req.extras} else: # process these in a separate loop self_reqs.append(req) - # A double loop is not strictly optimal, but always correct without recursion - for req in self_reqs: - if [True for extra in extras if evaluate(req.marker, env = {"extra": extra})]: - extras = extras + req.extras - else: - continue + for _ in range(10000): + # handles packages with up to 10000 recursive extras + new_extras = {} + for req in self_reqs: + if _evaluate_any(req, extras): + new_extras.update({x: None for x in req.extras}) + else: + continue - # Iterate through all packages to ensure that we include all of the extras from previously - # visited packages. - for req_ in self_reqs: - if [True for extra in extras if evaluate(req.marker, env = {"extra": extra})]: - extras = extras + req_.extras + num_extras_before = len(extras) + extras = extras | new_extras + num_extras_after = len(new_extras) + + if num_extras_before == num_extras_after: + break # Poor mans set - return sorted({x: None for x in extras}) + return sorted(extras) + +def _evaluate_any(req, extras): + for extra in extras: + if evaluate(req.marker, env = {"extra": extra}): + return True + + return False def _add_reqs(deps, deps_select, dep, reqs, *, extras): for req in reqs: diff --git a/tests/pypi/pep508/deps_tests.bzl b/tests/pypi/pep508/deps_tests.bzl index 679ba58396..f566845b70 100644 --- a/tests/pypi/pep508/deps_tests.bzl +++ b/tests/pypi/pep508/deps_tests.bzl @@ -90,6 +90,7 @@ def test_self_dependencies_can_come_in_any_order(env): "baz; extra == 'feat'", "foo[feat2]; extra == 'all'", "foo[feat]; extra == 'feat2'", + "foo[feat3]; extra == 'all'", "zdep; extra == 'all'", ], extras = ["all"], @@ -100,6 +101,24 @@ def test_self_dependencies_can_come_in_any_order(env): _tests.append(test_self_dependencies_can_come_in_any_order) +def test_self_include_deps_from_previously_visited(env): + got = deps( + "foo", + requires_dist = [ + "bar", + "baz; extra == 'feat'", + "foo[dev]; extra == 'all'", + "foo[feat]; extra == 'feat2'", + "dev_dep; extra == 'dev'", + ], + extras = ["feat2"], + ) + + env.expect.that_collection(got.deps).contains_exactly(["bar", "baz"]) + env.expect.that_dict(got.deps_select).contains_exactly({}) + +_tests.append(test_self_include_deps_from_previously_visited) + def _test_can_get_deps_based_on_specific_python_version(env): requires_dist = [ "bar",