Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
40 changes: 26 additions & 14 deletions python/private/pypi/pep508_deps.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
19 changes: 19 additions & 0 deletions tests/pypi/pep508/deps_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand 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",
Expand Down