Skip to content
Open
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
3 changes: 3 additions & 0 deletions news/4054.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(pypi) Fixed analysis failures in {obj}`pip.parse` for source-less wheels with
dependencies.
([#4053](https://github.com/bazel-contrib/rules_python/issues/4053))
35 changes: 35 additions & 0 deletions python/private/py_library.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,31 @@ This allows optimizing the generation of symlinks to be cheaper at analysis time
},
)

def _validate_srcs(ctx):
"""Validate that srcs targets provide Python sources or Python metadata."""
for target in ctx.attr.srcs:
files = target[DefaultInfo].files.to_list()
if not files and (
PyInfo in target or
(BuiltinPyInfo != None and BuiltinPyInfo in target)
):
continue

if any([
file.is_directory or file.extension in ["py", "py3"]
for file in files
]):
continue

fail(
("{} does not produce any py_library srcs files " +
"(expected .py or .py3) and is not an empty target providing " +
"PyInfo").format(
target.label,
),
attr = "srcs",
)

def py_library_impl(ctx):
"""Abstract implementation of py_library rule.

Expand All @@ -127,6 +152,7 @@ def py_library_impl(ctx):
Returns:
A list of modern providers to propagate.
"""
_validate_srcs(ctx)
direct_sources = filter_to_py_srcs(ctx.files.srcs)

precompile_result = maybe_precompile(ctx, direct_sources)
Expand Down Expand Up @@ -297,4 +323,13 @@ def create_py_library_rule_builder():
ruleb.ToolchainType(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False),
],
)
srcs_attr = builder.attrs.get("srcs")
srcs_attr.set_allow_files(True)
srcs_attr.set_doc(srcs_attr.doc() + """

:::{versionchanged} VERSION_NEXT_FEATURE
As an exception, empty targets in `srcs` that provide {obj}`PyInfo` are
allowed. Ordinary library dependencies should remain in `deps`.
:::
""")
return builder
133 changes: 133 additions & 0 deletions tests/base_rules/py_library/py_library_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,28 @@ load("//tests/support:py_info_subject.bzl", "py_info_subject")

_tests = []

def _tree_artifact_impl(ctx):
out = ctx.actions.declare_directory(ctx.label.name + ".dir")
ctx.actions.run_shell(
outputs = [out],
command = "mkdir -p \"$1\"",
arguments = [out.path],
)
return [DefaultInfo(files = depset([out]))]

_tree_artifact = rule(implementation = _tree_artifact_impl)

def _non_py_py_info_impl(ctx):
return [
DefaultInfo(files = depset(ctx.files.srcs)),
PyInfo(transitive_sources = depset()),
]

_non_py_py_info = rule(
implementation = _non_py_py_info_impl,
attrs = {"srcs": attr.label_list(allow_files = True)},
)

def _test_py_runtime_info_not_present(name, config):
rt_util.helper_target(
config.rule,
Expand Down Expand Up @@ -74,6 +96,117 @@ def _test_srcs_can_contain_rule_generating_py_and_nonpy_files_impl(env, target):

_tests.append(_test_srcs_can_contain_rule_generating_py_and_nonpy_files)

def _test_srcs_can_contain_empty_py_library(name, config):
rt_util.helper_target(
config.rule,
name = name + "_empty",
)
rt_util.helper_target(
config.rule,
name = name + "_subject",
srcs = [name + "_empty"],
)
analysis_test(
name = name,
target = name + "_subject",
impl = _test_srcs_can_contain_empty_py_library_impl,
)

def _test_srcs_can_contain_empty_py_library_impl(env, target):
env.expect.that_target(target).default_outputs().contains_exactly([])

_tests.append(_test_srcs_can_contain_empty_py_library)

def _test_srcs_can_contain_tree_artifact(name, config):
rt_util.helper_target(
_tree_artifact,
name = name + "_tree",
)
rt_util.helper_target(
config.rule,
name = name + "_subject",
srcs = [name + "_tree"],
)
analysis_test(
name = name,
target = name + "_subject",
impl = _test_srcs_can_contain_tree_artifact_impl,
)

def _test_srcs_can_contain_tree_artifact_impl(env, target):
env.expect.that_target(target).default_outputs().contains_exactly([])

_tests.append(_test_srcs_can_contain_tree_artifact)

def _test_srcs_direct_non_py_file_is_error(name, config):
rt_util.helper_target(
config.rule,
name = name + "_subject",
srcs = [rt_util.empty_file(name + ".txt")],
)
analysis_test(
name = name,
target = name + "_subject",
impl = _test_srcs_direct_non_py_file_is_error_impl,
expect_failure = True,
)

def _test_srcs_direct_non_py_file_is_error_impl(env, target):
env.expect.that_target(target).failures().contains_predicate(
matching.str_matches("does not produce*srcs files"),
)

_tests.append(_test_srcs_direct_non_py_file_is_error)

def _test_srcs_empty_filegroup_is_error(name, config):
rt_util.helper_target(
native.filegroup,
name = name + "_empty",
)
rt_util.helper_target(
config.rule,
name = name + "_subject",
srcs = [name + "_empty"],
)
analysis_test(
name = name,
target = name + "_subject",
impl = _test_srcs_empty_filegroup_is_error_impl,
expect_failure = True,
)

def _test_srcs_empty_filegroup_is_error_impl(env, target):
env.expect.that_target(target).failures().contains_predicate(
matching.str_matches("does not produce*srcs files"),
)

_tests.append(_test_srcs_empty_filegroup_is_error)

def _test_srcs_py_info_with_only_non_py_files_is_error(name, config):
rt_util.helper_target(
_non_py_py_info,
name = name + "_non_py",
srcs = [rt_util.empty_file(name + ".txt")],
)
rt_util.helper_target(
config.rule,
name = name + "_subject",
srcs = [name + "_non_py"],
)
analysis_test(
name = name,
target = name + "_subject",
impl = _test_srcs_py_info_with_only_non_py_files_is_error_impl,
expect_failure = True,
)

def _test_srcs_py_info_with_only_non_py_files_is_error_impl(env, target):
env.expect.that_target(target).failures().contains_predicate(
matching.str_matches("does not produce*srcs files"),
)

_tests.append(_test_srcs_py_info_with_only_non_py_files_is_error)

def _test_srcs_generating_no_py_files_is_error(name, config):
rt_util.helper_target(
config.rule,
Expand Down