diff --git a/news/4054.fixed.md b/news/4054.fixed.md new file mode 100644 index 0000000000..e4b259b889 --- /dev/null +++ b/news/4054.fixed.md @@ -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)) diff --git a/python/private/py_library.bzl b/python/private/py_library.bzl index 3d8ba4aa71..de99840c5c 100644 --- a/python/private/py_library.bzl +++ b/python/private/py_library.bzl @@ -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. @@ -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) @@ -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 diff --git a/tests/base_rules/py_library/py_library_tests.bzl b/tests/base_rules/py_library/py_library_tests.bzl index c375e72794..a3be7c4bcf 100644 --- a/tests/base_rules/py_library/py_library_tests.bzl +++ b/tests/base_rules/py_library/py_library_tests.bzl @@ -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, @@ -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,