Add 'incompatible_default_to_explicit_init_py' flag
This effectively changes the default value of legacy_create_init to false.
Technically, the attribute type has changed from boolean (default true) to tristate (default auto). Therefore, it is possible to see a semantic change in code that introspects a py_binary's or py_test's attrs dict (via native.existing_rules). We think it is unlikely this will break anyone, and in any case it is not currently possible to gate a rule's definition on an incompatible change flag.
Closes #9271. Work toward #7386 and #10076.
RELNOTES: [Python] Added flag --incomaptible_default_to_explicit_init_py to switch the default value of legacy_create_init to True. With this flag enabled, your py_binary and py_test targets will no longer behave as if empty __init__.py files were implicitly littered in your runfiles tree. See [#10076](https://github.com/bazelbuild/bazel/issues/10076).
PiperOrigin-RevId: 276526057
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuleClasses.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuleClasses.java
index 1736797..61aeafc 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuleClasses.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuleClasses.java
@@ -19,7 +19,6 @@
import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST;
import static com.google.devtools.build.lib.packages.BuildType.NODEP_LABEL;
import static com.google.devtools.build.lib.packages.BuildType.TRISTATE;
-import static com.google.devtools.build.lib.packages.Type.BOOLEAN;
import static com.google.devtools.build.lib.packages.Type.STRING;
import static com.google.devtools.build.lib.packages.Type.STRING_LIST;
@@ -229,11 +228,12 @@
Whether to implicitly create empty __init__.py files in the runfiles tree.
These are created in every directory containing Python source code or
shared libraries, and every parent directory of those directories, excluding the repo root
- directory. The default is true for backward compatibility. If false, the user is
+ directory. The default, auto, means true unless
+ <code>--incompatible_remove_legacy_create_init</code> is used. If false, the user is
responsible for creating (possibly empty) __init__.py files and adding them to the
<code>srcs</code> of Python targets as required.
<!-- #END_BLAZE_RULE.ATTRIBUTE --> */
- .add(attr("legacy_create_init", BOOLEAN).value(true))
+ .add(attr("legacy_create_init", TRISTATE).value(TriState.AUTO))
/* <!-- #BLAZE_RULE($base_py_binary).ATTRIBUTE(stamp) -->
Enable link stamping.
Whether to encode build information into the binary. Possible values:
diff --git a/src/main/java/com/google/devtools/build/lib/rules/python/PyExecutable.java b/src/main/java/com/google/devtools/build/lib/rules/python/PyExecutable.java
index db957f7..7c7be40 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/python/PyExecutable.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/python/PyExecutable.java
@@ -24,7 +24,8 @@
import com.google.devtools.build.lib.analysis.RunfilesProvider;
import com.google.devtools.build.lib.analysis.RunfilesSupport;
import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode;
-import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.packages.BuildType;
+import com.google.devtools.build.lib.packages.TriState;
import com.google.devtools.build.lib.rules.cpp.CcCommon.CcFlagsSupplier;
import com.google.devtools.build.lib.rules.cpp.CcInfo;
import java.util.ArrayList;
@@ -117,6 +118,31 @@
.build();
}
+ /**
+ * If requested, creates empty __init__.py files for each manifest file.
+ *
+ * <p>We do this if the rule defines {@code legacy_create_init} and its value is true. Auto is
+ * treated as false iff {@code --incompatible_remove_legacy_Create_init_py} is given.
+ *
+ * <p>See {@link PythonUtils#getInitPyFiles} for details about how the files are created.
+ */
+ private static void maybeCreateInitFiles(RuleContext ruleContext, Runfiles.Builder builder) {
+ boolean createFiles;
+ if (!ruleContext.attributes().has("legacy_create_init", BuildType.TRISTATE)) {
+ createFiles = true;
+ } else {
+ TriState legacy = ruleContext.attributes().get("legacy_create_init", BuildType.TRISTATE);
+ if (legacy == TriState.AUTO) {
+ createFiles = !ruleContext.getFragment(PythonConfiguration.class).defaultToExplicitInitPy();
+ } else {
+ createFiles = legacy != TriState.NO;
+ }
+ }
+ if (createFiles) {
+ builder.setEmptyFilesSupplier(PythonUtils.GET_INIT_PY_FILES);
+ }
+ }
+
private static Runfiles collectCommonRunfiles(
RuleContext ruleContext, PyCommon common, PythonSemantics semantics, CcInfo ccInfo)
throws InterruptedException, RuleErrorException {
@@ -131,10 +157,8 @@
semantics.collectDefaultRunfiles(ruleContext, builder);
builder.add(ruleContext, PythonRunfilesProvider.TO_RUNFILES);
- if (!ruleContext.attributes().has("legacy_create_init", Type.BOOLEAN)
- || ruleContext.attributes().get("legacy_create_init", Type.BOOLEAN)) {
- builder.setEmptyFilesSupplier(PythonUtils.GET_INIT_PY_FILES);
- }
+ maybeCreateInitFiles(ruleContext, builder);
+
semantics.collectRunfilesForBinary(ruleContext, builder, common, ccInfo);
return builder.build();
}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfiguration.java b/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfiguration.java
index 4a65e7a..1a5feb0 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfiguration.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfiguration.java
@@ -58,6 +58,8 @@
// TODO(brandjon): Remove this once migration for native rule access is complete.
private final boolean loadPythonRulesFromBzl;
+ private final boolean defaultToExplicitInitPy;
+
PythonConfiguration(
PythonVersion version,
PythonVersion defaultVersion,
@@ -68,7 +70,8 @@
boolean py2OutputsAreSuffixed,
boolean disallowLegacyPyProvider,
boolean useToolchains,
- boolean loadPythonRulesFromBzl) {
+ boolean loadPythonRulesFromBzl,
+ boolean defaultToExplicitInitPy) {
this.version = version;
this.defaultVersion = defaultVersion;
this.buildPythonZip = buildPythonZip;
@@ -79,6 +82,7 @@
this.disallowLegacyPyProvider = disallowLegacyPyProvider;
this.useToolchains = useToolchains;
this.loadPythonRulesFromBzl = loadPythonRulesFromBzl;
+ this.defaultToExplicitInitPy = defaultToExplicitInitPy;
}
/**
@@ -204,4 +208,12 @@
public boolean loadPythonRulesFromBzl() {
return loadPythonRulesFromBzl;
}
+
+ /**
+ * Returns true if executable Python rules should only write out empty __init__ files to their
+ * runfiles tree when explicitly requested via {@code legacy_create_init}.
+ */
+ public boolean defaultToExplicitInitPy() {
+ return defaultToExplicitInitPy;
+ }
}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfigurationLoader.java b/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfigurationLoader.java
index 67bc073..a898e2c 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfigurationLoader.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/python/PythonConfigurationLoader.java
@@ -44,7 +44,8 @@
/*py2OutputsAreSuffixed=*/ pythonOptions.incompatiblePy2OutputsAreSuffixed,
/*disallowLegacyPyProvider=*/ pythonOptions.incompatibleDisallowLegacyPyProvider,
/*useToolchains=*/ pythonOptions.incompatibleUsePythonToolchains,
- /*loadPythonRulesFromBzl=*/ pythonOptions.loadPythonRulesFromBzl);
+ /*loadPythonRulesFromBzl=*/ pythonOptions.loadPythonRulesFromBzl,
+ /*defaultToExplicitInitPy=*/ pythonOptions.incompatibleDefaultToExplicitInitPy);
}
@Override
diff --git a/src/main/java/com/google/devtools/build/lib/rules/python/PythonOptions.java b/src/main/java/com/google/devtools/build/lib/rules/python/PythonOptions.java
index ad09446..aca45d8 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/python/PythonOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/python/PythonOptions.java
@@ -269,6 +269,23 @@
+ "data runfiles of another binary.")
public boolean buildTransitiveRunfilesTrees;
+ @Option(
+ name = "incompatible_default_to_explicit_init_py",
+ defaultValue = "false",
+ documentationCategory = OptionDocumentationCategory.GENERIC_INPUTS,
+ effectTags = {OptionEffectTag.AFFECTS_OUTPUTS},
+ metadataTags = {
+ OptionMetadataTag.INCOMPATIBLE_CHANGE,
+ OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES
+ },
+ help =
+ "This flag changes the default behavior so that __init__.py files are no longer "
+ + "automatically created in the runfiles of Python targets. Precisely, when a "
+ + "py_binary or py_test target has legacy_create_init set to \"auto\" (the default), "
+ + "it is treated as false if and only if this flag is set. See "
+ + "https://github.com/bazelbuild/bazel/issues/10076.")
+ public boolean incompatibleDefaultToExplicitInitPy;
+
@Override
public Map<OptionDefinition, SelectRestriction> getSelectRestrictions() {
// TODO(brandjon): Instead of referencing the python_version target, whose path depends on the
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyBinaryConfiguredTargetTest.java b/src/test/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyBinaryConfiguredTargetTest.java
index 7fbe0e1..65f8995 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyBinaryConfiguredTargetTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyBinaryConfiguredTargetTest.java
@@ -40,6 +40,10 @@
private static final String TOOLCHAIN_TYPE =
TestConstants.TOOLS_REPOSITORY + "//tools/python:toolchain_type";
+ private static String join(String... lines) {
+ return String.join("\n", lines);
+ }
+
/**
* Given a {@code py_binary} or {@code py_test} target, returns the path of the Python interpreter
* used by the generated stub script.
@@ -281,7 +285,7 @@
if (lines.length == 0) {
indentedBody = " pass";
} else {
- indentedBody = " " + String.join("\n", lines).replace("\n", "\n ");
+ indentedBody = " " + join(lines).replace("\n", "\n ");
}
scratch.file(
"toolchains/rules.bzl",
@@ -372,4 +376,60 @@
"Error retrieving the Python runtime from the toolchain: Expected field 'py3_runtime' to "
+ "have a runtime with python_version = 'PY3', but got python_version = 'PY2'");
}
+
+ @Test
+ public void explicitInitPy_CanBeGloballyEnabled() throws Exception {
+ scratch.file(
+ "pkg/BUILD",
+ join(
+ "py_binary(", //
+ " name = 'foo',",
+ " srcs = ['foo.py'],",
+ ")"));
+ useConfiguration("--incompatible_default_to_explicit_init_py=true");
+ assertThat(getDefaultRunfiles(getConfiguredTarget("//pkg:foo")).getEmptyFilenames()).isEmpty();
+ }
+
+ @Test
+ public void explicitInitPy_CanBeSelectivelyDisabled() throws Exception {
+ scratch.file(
+ "pkg/BUILD",
+ join(
+ "py_binary(", //
+ " name = 'foo',",
+ " srcs = ['foo.py'],",
+ " legacy_create_init = True,",
+ ")"));
+ useConfiguration("--incompatible_default_to_explicit_init_py=true");
+ assertThat(getDefaultRunfiles(getConfiguredTarget("//pkg:foo")).getEmptyFilenames())
+ .containsExactly("pkg/__init__.py");
+ }
+
+ @Test
+ public void explicitInitPy_CanBeGloballyDisabled() throws Exception {
+ scratch.file(
+ "pkg/BUILD",
+ join(
+ "py_binary(", //
+ " name = 'foo',",
+ " srcs = ['foo.py'],",
+ ")"));
+ useConfiguration("--incompatible_default_to_explicit_init_py=false");
+ assertThat(getDefaultRunfiles(getConfiguredTarget("//pkg:foo")).getEmptyFilenames())
+ .containsExactly("pkg/__init__.py");
+ }
+
+ @Test
+ public void explicitInitPy_CanBeSelectivelyEnabled() throws Exception {
+ scratch.file(
+ "pkg/BUILD",
+ join(
+ "py_binary(", //
+ " name = 'foo',",
+ " srcs = ['foo.py'],",
+ " legacy_create_init = False,",
+ ")"));
+ useConfiguration("--incompatible_default_to_explicit_init_py=false");
+ assertThat(getDefaultRunfiles(getConfiguredTarget("//pkg:foo")).getEmptyFilenames()).isEmpty();
+ }
}