Repository: pantsbuild/pants

Issue:
Deprecate testutil method RuleRunner.create_file
We should use `RuleRunner.write_files` instead.

_Origin comment posted by @Eric-Arellano in https://github.com/pantsbuild/pants/pull/12431#discussion_r678069407_


Discussion:
@pantsbuild/maintainers Are there consensus to deprecate `create_file` in favour of `write_files`? 

If so, I can start looking into it..
Yea, I'd be fine with that.

Resolving patch:
diff --git a/src/python/pants/testutil/rule_runner.py b/src/python/pants/testutil/rule_runner.py
--- a/src/python/pants/testutil/rule_runner.py
+++ b/src/python/pants/testutil/rule_runner.py
@@ -17,6 +17,7 @@
 from typing import Any, Callable, Generic, Iterable, Iterator, Mapping, Sequence, TypeVar, cast
 
 from pants.base.build_root import BuildRoot
+from pants.base.deprecated import deprecated
 from pants.base.specs_parser import SpecsParser
 from pants.build_graph.build_configuration import BuildConfiguration
 from pants.build_graph.build_file_aliases import BuildFileAliases
@@ -398,11 +399,29 @@ def create_dir(self, relpath: str) -> str:
         self._invalidate_for(relpath)
         return path
 
+    @deprecated(
+        "2.10.0.dev0",
+        (
+            "Use `RuleRunner.write_files({<relpath>: <contents>}) instead of "
+            "`RuleRunner.create_file(<relpath>, <contents>)`"
+        ),
+    )
     def create_file(
         self, relpath: str | PurePath, contents: bytes | str = "", mode: str = "w"
     ) -> str:
         """Writes to a file under the buildroot.
 
+        relpath: The relative path to the file from the build root.
+        contents: A string containing the contents of the file - '' by default..
+        mode: The mode to write to the file in - over-write by default.
+        """
+        return self._create_file(relpath, contents, mode)
+
+    def _create_file(
+        self, relpath: str | PurePath, contents: bytes | str = "", mode: str = "w"
+    ) -> str:
+        """Writes to a file under the buildroot.
+
         relpath: The relative path to the file from the build root.
         contents: A string containing the contents of the file - '' by default..
         mode: The mode to write to the file in - over-write by default.
@@ -413,6 +432,14 @@ def create_file(
         self._invalidate_for(str(relpath))
         return path
 
+    @deprecated(
+        "2.10.0.dev0",
+        (
+            'Use `RuleRunner.write_files({"path/to/file": "file contents"})` instead of '
+            '`RuleRunner.create_files("path/to", ["file"])`.\n'
+            "This allows creating all files with specific content in one call."
+        ),
+    )
     def create_files(self, path: str | PurePath, files: Iterable[str]) -> None:
         """Writes to a file under the buildroot with contents same as file name.
 
@@ -420,8 +447,15 @@ def create_files(self, path: str | PurePath, files: Iterable[str]) -> None:
         files: List of file names.
         """
         for f in files:
-            self.create_file(os.path.join(path, f), contents=f)
+            self._create_file(os.path.join(path, f), contents=f)
 
+    @deprecated(
+        "2.10.0.dev0",
+        (
+            'Use `RuleRunner.write_files({"<relpath>/BUILD": <contents>})` instead of '
+            "`RuleRunner.add_to_build_file(<relpath>, <contents>)`."
+        ),
+    )
     def add_to_build_file(
         self, relpath: str | PurePath, target: str, *, overwrite: bool = False
     ) -> str:
@@ -435,15 +469,20 @@ def add_to_build_file(
             relpath if PurePath(relpath).name.startswith("BUILD") else PurePath(relpath, "BUILD")
         )
         mode = "w" if overwrite else "a"
-        return self.create_file(str(build_path), target, mode=mode)
+        return self._create_file(str(build_path), target, mode=mode)
 
-    def write_files(self, files: Mapping[str | PurePath, str]) -> None:
+    def write_files(self, files: Mapping[str | PurePath, str | bytes]) -> tuple[str, ...]:
         """Write the files to the build root.
 
         :API: public
+
+        files: A mapping of file names to contents.
+        returns: A tuple of absolute file paths created.
         """
+        paths = []
         for path, content in files.items():
-            self.create_file(path, content)
+            paths.append(self._create_file(path, content))
+        return tuple(paths)
 
     def make_snapshot(self, files: Mapping[str, str | bytes]) -> Snapshot:
         """Makes a snapshot from a map of file name to file content.


Test patch:
diff --git a/src/python/pants/backend/docker/util_rules/dependencies_test.py b/src/python/pants/backend/docker/util_rules/dependencies_test.py
--- a/src/python/pants/backend/docker/util_rules/dependencies_test.py
+++ b/src/python/pants/backend/docker/util_rules/dependencies_test.py
@@ -39,31 +39,26 @@ def rule_runner() -> RuleRunner:
 
 
 def test_inject_docker_dependencies(rule_runner: RuleRunner) -> None:
-    rule_runner.add_to_build_file(
-        "project/image/test",
-        dedent(
-            """\
-            docker_image(name="image")
-            """
-        ),
-    )
-    rule_runner.create_file(
-        "project/image/test/Dockerfile",
-        dedent(
-            """\
-            FROM baseimage
-            ENTRYPOINT ["./entrypoint"]
-            COPY project.hello.main/main_binary.pex /entrypoint
-            """
-        ),
-    )
-    rule_runner.add_to_build_file(
-        "project/hello/main",
-        dedent(
-            """\
-            pex_binary(name="main_binary")
-            """
-        ),
+    rule_runner.write_files(
+        {
+            "project/image/test/BUILD": dedent(
+                """\
+                docker_image(name="image")
+                """
+            ),
+            "project/image/test/Dockerfile": dedent(
+                """\
+                FROM baseimage
+                ENTRYPOINT ["./entrypoint"]
+                COPY project.hello.main/main_binary.pex /entrypoint
+                """
+            ),
+            "project/hello/main/BUILD": dedent(
+                """\
+                pex_binary(name="main_binary")
+                """
+            ),
+        }
     )
     tgt = rule_runner.get_target(Address("project/image/test", target_name="image"))
     injected = rule_runner.request(
diff --git a/src/python/pants/backend/docker/util_rules/docker_build_context_test.py b/src/python/pants/backend/docker/util_rules/docker_build_context_test.py
--- a/src/python/pants/backend/docker/util_rules/docker_build_context_test.py
+++ b/src/python/pants/backend/docker/util_rules/docker_build_context_test.py
@@ -76,30 +76,38 @@ def assert_build_context(
 
 
 def test_file_dependencies(rule_runner: RuleRunner) -> None:
-    # img_A -> files_A
-    # img_A -> img_B -> files_B
-    rule_runner.add_to_build_file(
-        "src/a",
-        dedent(
-            """\
-            docker_image(name="img_A", dependencies=[":files_A", "src/b:img_B"])
-            files(name="files_A", sources=["files/**"])
-            """
-        ),
-    )
-    rule_runner.add_to_build_file(
-        "src/b",
-        dedent(
-            """\
-            docker_image(name="img_B", dependencies=[":files_B"])
-            files(name="files_B", sources=["files/**"])
-            """
-        ),
+    rule_runner.write_files(
+        {
+            # img_A -> files_A
+            # img_A -> img_B
+            "src/a/BUILD": dedent(
+                """\
+                docker_image(name="img_A", dependencies=[":files_A", "src/b:img_B"])
+                files(name="files_A", sources=["files/**"])
+                """
+            ),
+            "src/a/Dockerfile": "FROM base",
+            "src/a/files/a01": "",
+            "src/a/files/a02": "",
+            # img_B -> files_B
+            "src/b/BUILD": dedent(
+                """\
+                docker_image(name="img_B", dependencies=[":files_B"])
+                files(name="files_B", sources=["files/**"])
+                """
+            ),
+            "src/b/Dockerfile": "FROM base",
+            "src/b/files/b01": "",
+            "src/b/files/b02": "",
+            # Mixed
+            "src/c/BUILD": dedent(
+                """\
+                docker_image(name="img_C", dependencies=["src/a:files_A", "src/b:files_B"])
+                """
+            ),
+            "src/c/Dockerfile": "FROM base",
+        }
     )
-    rule_runner.create_files("src/a", ["Dockerfile"])
-    rule_runner.create_files("src/a/files", ["a01", "a02"])
-    rule_runner.create_files("src/b", ["Dockerfile"])
-    rule_runner.create_files("src/b/files", ["b01", "b02"])
 
     # We want files_B in build context for img_B
     assert_build_context(
@@ -116,16 +124,6 @@ def test_file_dependencies(rule_runner: RuleRunner) -> None:
     )
 
     # Mixed.
-    rule_runner.add_to_build_file(
-        "src/c",
-        dedent(
-            """\
-            docker_image(name="img_C", dependencies=["src/a:files_A", "src/b:files_B"])
-            """
-        ),
-    )
-    rule_runner.create_files("src/c", ["Dockerfile"])
-
     assert_build_context(
         rule_runner,
         Address("src/c", target_name="img_C"),
@@ -141,25 +139,24 @@ def test_file_dependencies(rule_runner: RuleRunner) -> None:
 
 def test_files_out_of_tree(rule_runner: RuleRunner) -> None:
     # src/a:img_A -> res/static:files
-    rule_runner.add_to_build_file(
-        "src/a",
-        dedent(
-            """\
-            docker_image(name="img_A", dependencies=["res/static:files"])
-            """
-        ),
-    )
-    rule_runner.add_to_build_file(
-        "res/static",
-        dedent(
-            """\
-            files(name="files", sources=["!BUILD", "**/*"])
-            """
-        ),
+    rule_runner.write_files(
+        {
+            "src/a/BUILD": dedent(
+                """\
+                docker_image(name="img_A", dependencies=["res/static:files"])
+                """
+            ),
+            "res/static/BUILD": dedent(
+                """\
+                files(name="files", sources=["!BUILD", "**/*"])
+                """
+            ),
+            "src/a/Dockerfile": "FROM base",
+            "res/static/s01": "",
+            "res/static/s02": "",
+            "res/static/sub/s03": "",
+        }
     )
-    rule_runner.create_files("src/a", ["Dockerfile"])
-    rule_runner.create_files("res/static", ["s01", "s02"])
-    rule_runner.create_files("res/static/sub", ["s03"])
 
     assert_build_context(
         rule_runner,
diff --git a/src/python/pants/backend/docker/util_rules/dockerfile_test.py b/src/python/pants/backend/docker/util_rules/dockerfile_test.py
--- a/src/python/pants/backend/docker/util_rules/dockerfile_test.py
+++ b/src/python/pants/backend/docker/util_rules/dockerfile_test.py
@@ -150,7 +150,7 @@ def test_generate_dockerfile_for_generated_target(rule_runner: RuleRunner) -> No
 
 
 def test_missing_dockerfile_is_error(rule_runner: RuleRunner) -> None:
-    rule_runner.add_to_build_file("test", "docker_image()")
+    rule_runner.write_files({"test/BUILD": "docker_image()"})
     with pytest.raises(ExecutionError, match=r"The `docker_image` test:test does not specify any"):
         assert_dockerfile(rule_runner, filename="", content="")
 
diff --git a/src/python/pants/backend/project_info/dependees_test.py b/src/python/pants/backend/project_info/dependees_test.py
--- a/src/python/pants/backend/project_info/dependees_test.py
+++ b/src/python/pants/backend/project_info/dependees_test.py
@@ -23,9 +23,13 @@ class MockTarget(Target):
 @pytest.fixture
 def rule_runner() -> RuleRunner:
     runner = RuleRunner(rules=dependee_rules(), target_types=[MockTarget])
-    runner.add_to_build_file("base", "tgt()")
-    runner.add_to_build_file("intermediate", "tgt(dependencies=['base'])")
-    runner.add_to_build_file("leaf", "tgt(dependencies=['intermediate'])")
+    runner.write_files(
+        {
+            "base/BUILD": "tgt()",
+            "intermediate/BUILD": "tgt(dependencies=['base'])",
+            "leaf/BUILD": "tgt(dependencies=['intermediate'])",
+        }
+    )
     return runner
 
 
@@ -84,7 +88,7 @@ def test_multiple_specified_targets(rule_runner: RuleRunner) -> None:
 
 
 def test_special_cased_dependencies(rule_runner: RuleRunner) -> None:
-    rule_runner.add_to_build_file("special", "tgt(special_deps=['intermediate'])")
+    rule_runner.write_files({"special/BUILD": "tgt(special_deps=['intermediate'])"})
     assert_dependees(
         rule_runner, targets=["intermediate"], expected=["leaf:leaf", "special:special"]
     )
diff --git a/src/python/pants/backend/project_info/dependencies_test.py b/src/python/pants/backend/project_info/dependencies_test.py
--- a/src/python/pants/backend/project_info/dependencies_test.py
+++ b/src/python/pants/backend/project_info/dependencies_test.py
@@ -35,22 +35,26 @@ def rule_runner() -> RuleRunner:
 def create_python_sources(
     rule_runner: RuleRunner, path: str, *, dependencies: Optional[List[str]] = None
 ) -> None:
-    rule_runner.add_to_build_file(
-        path, f"python_sources(name='target', sources=[], dependencies={dependencies or []})"
+    rule_runner.write_files(
+        {path: f"python_sources(name='target', sources=[], dependencies={dependencies or []})"}
     )
 
 
-def create_python_requirement_tgt(rule_runner: RuleRunner, name: str) -> None:
-    rule_runner.add_to_build_file(
-        "3rdparty/python",
-        dedent(
-            f"""\
-            python_requirement(
-                name='{name}',
-                requirements=['{name}==1.0.0'],
+def create_python_requirement_tgts(rule_runner: RuleRunner, *names: str) -> None:
+    rule_runner.write_files(
+        {
+            "3rdparty/python/BUILD": "\n".join(
+                dedent(
+                    f"""\
+                    python_requirement(
+                        name='{name}',
+                        requirements=['{name}==1.0.0'],
+                    )
+                    """
+                )
+                for name in names
             )
-            """
-        ),
+        }
     )
 
 
@@ -77,7 +81,7 @@ def test_no_target(rule_runner: RuleRunner) -> None:
 
 
 def test_no_dependencies(rule_runner: RuleRunner) -> None:
-    create_python_sources(rule_runner, path="some/target")
+    create_python_sources(rule_runner, path="some/target/BUILD")
     assert_dependencies(rule_runner, specs=["some/target"], expected=[])
     assert_dependencies(rule_runner, specs=["some/target"], expected=[], transitive=True)
     assert_dependencies(
@@ -93,29 +97,31 @@ def test_no_dependencies(rule_runner: RuleRunner) -> None:
 
 
 def test_special_cased_dependencies(rule_runner: RuleRunner) -> None:
-    rule_runner.add_to_build_file(
-        "",
-        dedent(
-            """\
-            special_deps_tgt(name='t1')
-            special_deps_tgt(name='t2', special_deps=[':t1'])
-            special_deps_tgt(name='t3', special_deps=[':t2'])
-            """
-        ),
+    rule_runner.write_files(
+        {
+            "BUILD": dedent(
+                """\
+                special_deps_tgt(name='t1')
+                special_deps_tgt(name='t2', special_deps=[':t1'])
+                special_deps_tgt(name='t3', special_deps=[':t2'])
+                """
+            ),
+        }
     )
     assert_dependencies(rule_runner, specs=["//:t3"], expected=["//:t2"])
     assert_dependencies(rule_runner, specs=["//:t3"], expected=["//:t1", "//:t2"], transitive=True)
 
 
 def test_python_dependencies(rule_runner: RuleRunner) -> None:
-    create_python_requirement_tgt(rule_runner, name="req1")
-    create_python_requirement_tgt(rule_runner, name="req2")
-    create_python_sources(rule_runner, path="dep/target")
+    create_python_requirement_tgts(rule_runner, "req1", "req2")
+    create_python_sources(rule_runner, path="dep/target/BUILD")
     create_python_sources(
-        rule_runner, path="some/target", dependencies=["dep/target", "3rdparty/python:req1"]
+        rule_runner, path="some/target/BUILD", dependencies=["dep/target", "3rdparty/python:req1"]
     )
     create_python_sources(
-        rule_runner, path="some/other/target", dependencies=["some/target", "3rdparty/python:req2"]
+        rule_runner,
+        path="some/other/target/BUILD",
+        dependencies=["some/target", "3rdparty/python:req2"],
     )
 
     assert_deps = partial(assert_dependencies, rule_runner)
diff --git a/src/python/pants/backend/project_info/paths_test.py b/src/python/pants/backend/project_info/paths_test.py
--- a/src/python/pants/backend/project_info/paths_test.py
+++ b/src/python/pants/backend/project_info/paths_test.py
@@ -20,10 +20,14 @@ class MockTarget(Target):
 @pytest.fixture
 def rule_runner() -> RuleRunner:
     runner = RuleRunner(rules=paths_rules(), target_types=[MockTarget])
-    runner.add_to_build_file("base", "tgt()")
-    runner.add_to_build_file("intermediate", "tgt(dependencies=['base'])")
-    runner.add_to_build_file("intermediate2", "tgt(dependencies=['base'])")
-    runner.add_to_build_file("leaf", "tgt(dependencies=['intermediate', 'intermediate2'])")
+    runner.write_files(
+        {
+            "base/BUILD": "tgt()",
+            "intermediate/BUILD": "tgt(dependencies=['base'])",
+            "intermediate2/BUILD": "tgt(dependencies=['base'])",
+            "leaf/BUILD": "tgt(dependencies=['intermediate', 'intermediate2'])",
+        }
+    )
     return runner
 
 
diff --git a/src/python/pants/backend/project_info/peek_test.py b/src/python/pants/backend/project_info/peek_test.py
--- a/src/python/pants/backend/project_info/peek_test.py
+++ b/src/python/pants/backend/project_info/peek_test.py
@@ -173,7 +173,7 @@ def rule_runner() -> RuleRunner:
 
 
 def test_non_matching_build_target(rule_runner: RuleRunner) -> None:
-    rule_runner.add_to_build_file("some_name", "files(sources=[])")
+    rule_runner.write_files({"some_name/BUILD": "files(sources=[])"})
     result = rule_runner.run_goal_rule(Peek, args=["other_name"])
     assert result.stdout == "[]\n"
 
diff --git a/src/python/pants/backend/python/dependency_inference/rules_test.py b/src/python/pants/backend/python/dependency_inference/rules_test.py
--- a/src/python/pants/backend/python/dependency_inference/rules_test.py
+++ b/src/python/pants/backend/python/dependency_inference/rules_test.py
@@ -43,52 +43,44 @@ def test_infer_python_imports(caplog) -> None:
         ],
         target_types=[PythonSourcesGeneratorTarget, PythonRequirementTarget],
     )
-    rule_runner.add_to_build_file(
-        "3rdparty/python",
-        dedent(
-            """\
-            python_requirement(
-              name='Django',
-              requirements=['Django==1.21'],
-            )
-            """
-        ),
-    )
-
-    # If there's a `.py` and `.pyi` file for the same module, we should infer a dependency on both.
-    rule_runner.create_file("src/python/str_import/subdir/f.py")
-    rule_runner.create_file("src/python/str_import/subdir/f.pyi")
-    rule_runner.add_to_build_file("src/python/str_import/subdir", "python_sources()")
-
-    rule_runner.create_file("src/python/util/dep.py")
-    rule_runner.add_to_build_file("src/python/util", "python_sources()")
-
-    rule_runner.create_file(
-        "src/python/app.py",
-        dedent(
-            """\
-            import django
-            import unrecognized.module
-
-            from util.dep import Demo
-            from util import dep
-            """
-        ),
-    )
-    rule_runner.create_file(
-        "src/python/f2.py",
-        dedent(
-            """\
-            import typing
-            # Import from another file in the same target.
-            from app import main
-
-            # Dynamic string import.
-            importlib.import_module('str_import.subdir.f')
-            """
-        ),
+    rule_runner.write_files(
+        {
+            "3rdparty/python/BUILD": dedent(
+                """\
+                python_requirement(
+                  name='Django',
+                  requirements=['Django==1.21'],
+                )
+                """
+            ),
+            # If there's a `.py` and `.pyi` file for the same module, we should infer a dependency on both.
+            "src/python/str_import/subdir/f.py": "",
+            "src/python/str_import/subdir/f.pyi": "",
+            "src/python/str_import/subdir/BUILD": "python_sources()",
+            "src/python/util/dep.py": "",
+            "src/python/util/BUILD": "python_sources()",
+            "src/python/app.py": dedent(
+                """\
+                import django
+                import unrecognized.module
+
+                from util.dep import Demo
+                from util import dep
+                """
+            ),
+            "src/python/f2.py": dedent(
+                """\
+                import typing
+                # Import from another file in the same target.
+                from app import main
+
+                # Dynamic string import.
+                importlib.import_module('str_import.subdir.f')
+                """
+            ),
+            "src/python/BUILD": "python_sources()",
+        }
     )
-    rule_runner.add_to_build_file("src/python", "python_sources()")
 
     def run_dep_inference(
         address: Address, *, enable_string_imports: bool = False
@@ -126,24 +118,25 @@ def run_dep_inference(
     # Test handling of ambiguous imports. We should warn on the ambiguous dependency, but not warn
     # on the disambiguated one and should infer a dep.
     caplog.clear()
-    rule_runner.create_files("src/python/ambiguous", ["dep.py", "disambiguated_via_ignores.py"])
-    rule_runner.create_file(
-        "src/python/ambiguous/main.py",
-        "import ambiguous.dep\nimport ambiguous.disambiguated_via_ignores\n",
-    )
-    rule_runner.add_to_build_file(
-        "src/python/ambiguous",
-        dedent(
-            """\
-            python_sources(name='dep1', sources=['dep.py', 'disambiguated_via_ignores.py'])
-            python_sources(name='dep2', sources=['dep.py', 'disambiguated_via_ignores.py'])
-            python_sources(
-                name='main',
-                sources=['main.py'],
-                dependencies=['!./disambiguated_via_ignores.py:dep2'],
-            )
-            """
-        ),
+    rule_runner.write_files(
+        {
+            "src/python/ambiguous/dep.py": "",
+            "src/python/ambiguous/disambiguated_via_ignores.py": "",
+            "src/python/ambiguous/main.py": (
+                "import ambiguous.dep\nimport ambiguous.disambiguated_via_ignores\n"
+            ),
+            "src/python/ambiguous/BUILD": dedent(
+                """\
+                python_sources(name='dep1', sources=['dep.py', 'disambiguated_via_ignores.py'])
+                python_sources(name='dep2', sources=['dep.py', 'disambiguated_via_ignores.py'])
+                python_sources(
+                    name='main',
+                    sources=['main.py'],
+                    dependencies=['!./disambiguated_via_ignores.py:dep2'],
+                )
+                """
+            ),
+        }
     )
     assert run_dep_inference(
         Address("src/python/ambiguous", target_name="main", relative_file_path="main.py")
@@ -182,15 +175,17 @@ def test_infer_python_inits() -> None:
         env_inherit={"PATH", "PYENV_ROOT", "HOME"},
     )
 
-    rule_runner.create_file("src/python/root/__init__.py")
-    rule_runner.add_to_build_file("src/python/root", "python_sources()")
-
-    rule_runner.create_file("src/python/root/mid/__init__.py")
-    rule_runner.add_to_build_file("src/python/root/mid", "python_sources()")
-
-    rule_runner.create_file("src/python/root/mid/leaf/__init__.py")
-    rule_runner.create_file("src/python/root/mid/leaf/f.py")
-    rule_runner.add_to_build_file("src/python/root/mid/leaf", "python_sources()")
+    rule_runner.write_files(
+        {
+            "src/python/root/__init__.py": "",
+            "src/python/root/BUILD": "python_sources()",
+            "src/python/root/mid/__init__.py": "",
+            "src/python/root/mid/BUILD": "python_sources()",
+            "src/python/root/mid/leaf/__init__.py": "",
+            "src/python/root/mid/leaf/f.py": "",
+            "src/python/root/mid/leaf/BUILD": "python_sources()",
+        }
+    )
 
     def run_dep_inference(address: Address) -> InferredDependencies:
         target = rule_runner.get_target(address)
@@ -226,16 +221,16 @@ def test_infer_python_conftests() -> None:
         env_inherit={"PATH", "PYENV_ROOT", "HOME"},
     )
 
-    rule_runner.create_file("src/python/root/conftest.py")
-    rule_runner.add_to_build_file("src/python/root", "python_test_utils()")
-
-    rule_runner.create_file("src/python/root/mid/conftest.py")
-    rule_runner.add_to_build_file("src/python/root/mid", "python_test_utils()")
-
-    rule_runner.create_file("src/python/root/mid/leaf/conftest.py")
-    rule_runner.create_file("src/python/root/mid/leaf/this_is_a_test.py")
-    rule_runner.add_to_build_file(
-        "src/python/root/mid/leaf", "python_test_utils()\npython_tests(name='tests')"
+    rule_runner.write_files(
+        {
+            "src/python/root/conftest.py": "",
+            "src/python/root/BUILD": "python_test_utils()",
+            "src/python/root/mid/conftest.py": "",
+            "src/python/root/mid/BUILD": "python_test_utils()",
+            "src/python/root/mid/leaf/conftest.py": "",
+            "src/python/root/mid/leaf/this_is_a_test.py": "",
+            "src/python/root/mid/leaf/BUILD": "python_test_utils()\npython_tests(name='tests')",
+        }
     )
 
     def run_dep_inference(address: Address) -> InferredDependencies:
@@ -273,11 +268,12 @@ def test_infer_python_strict(caplog) -> None:
         context_aware_object_factories={"python_requirements": PythonRequirementsCAOF},
     )
 
-    rule_runner.create_file(
-        "src/python/cheesey.py",
-        "import venezuelan_beaver_cheese",
+    rule_runner.write_files(
+        {
+            "src/python/cheesey.py": "import venezuelan_beaver_cheese",
+            "src/python/BUILD": "python_sources()",
+        }
     )
-    rule_runner.add_to_build_file("src/python", "python_sources()")
 
     def run_dep_inference(
         address: Address,
@@ -316,45 +312,47 @@ def run_dep_inference(
     caplog.clear()
 
     # All modes should be fine if the module is explictly declared as a requirement
-    rule_runner.add_to_build_file(
-        "src/python",
-        dedent(
-            """\
-                python_requirement(
-                    name="venezuelan_beaver_cheese",
-                    modules=["venezuelan_beaver_cheese"],
-                    requirements=["venezuelan_beaver_cheese==1.0.0"],
-                )
-                python_sources(dependencies=[":venezuelan_beaver_cheese"])
-            """
-        ),
-        overwrite=True,
+    rule_runner.write_files(
+        {
+            "src/python/BUILD": dedent(
+                """\
+                    python_requirement(
+                        name="venezuelan_beaver_cheese",
+                        modules=["venezuelan_beaver_cheese"],
+                        requirements=["venezuelan_beaver_cheese==1.0.0"],
+                    )
+                    python_sources(dependencies=[":venezuelan_beaver_cheese"])
+                """
+            ),
+        }
     )
     for mode in UnownedDependencyUsage:
         run_dep_inference(Address("src/python", relative_file_path="cheesey.py"), mode.value)
         assert not caplog.records
 
-    rule_runner.add_to_build_file("src/python", "python_sources()", overwrite=True)  # Cleanup
-
     # All modes should be fine if the module is implictly found via requirements.txt
-    rule_runner.create_file("src/python/requirements.txt", "venezuelan_beaver_cheese==1.0.0")
-    rule_runner.add_to_build_file(
-        "src/python",
-        dedent(
-            """\
-                python_requirements()
-                python_sources()
-            """
-        ),
-        overwrite=True,
+    rule_runner.write_files(
+        {
+            "src/python/requirements.txt": "venezuelan_beaver_cheese==1.0.0",
+            "src/python/BUILD": dedent(
+                """\
+                    python_requirements()
+                    python_sources()
+                """
+            ),
+        }
     )
     for mode in UnownedDependencyUsage:
         run_dep_inference(Address("src/python", relative_file_path="cheesey.py"), mode.value)
         assert not caplog.records
 
     # All modes should be fine if the module is owned by a first party
-    rule_runner.create_file("src/python/venezuelan_beaver_cheese.py")
-    rule_runner.add_to_build_file("src/python", "python_sources()", overwrite=True)
+    rule_runner.write_files(
+        {
+            "src/python/venezuelan_beaver_cheese.py": "",
+            "src/python/BUILD": "python_sources()",
+        }
+    )
     for mode in UnownedDependencyUsage:
         run_dep_inference(Address("src/python", relative_file_path="cheesey.py"), mode.value)
         assert not caplog.records
diff --git a/src/python/pants/backend/python/goals/publish_test.py b/src/python/pants/backend/python/goals/publish_test.py
--- a/src/python/pants/backend/python/goals/publish_test.py
+++ b/src/python/pants/backend/python/goals/publish_test.py
@@ -203,7 +203,7 @@ def test_skip_twine(rule_runner, packages) -> None:
     ],
 )
 def test_twine_cert_arg(rule_runner, packages, options, cert_arg) -> None:
-    ca_cert_path = rule_runner.create_file("conf/ca_certs.pem", "")
+    ca_cert_path = rule_runner.write_files({"conf/ca_certs.pem": ""})[0]
     rule_runner.write_files(project_files(repositories=["@private"]))
     set_options(rule_runner, [opt.format(ca_cert_path) for opt in options])
     result = request_publish_processes(rule_runner, packages)
diff --git a/src/python/pants/backend/python/goals/setup_py_test.py b/src/python/pants/backend/python/goals/setup_py_test.py
--- a/src/python/pants/backend/python/goals/setup_py_test.py
+++ b/src/python/pants/backend/python/goals/setup_py_test.py
@@ -151,66 +151,59 @@ def assert_chroot_error(rule_runner: RuleRunner, addr: Address, exc_cls: type[Ex
 
 
 def test_use_existing_setup_script(chroot_rule_runner) -> None:
-    chroot_rule_runner.add_to_build_file("src/python/foo/bar", "python_sources()")
-    chroot_rule_runner.create_file("src/python/foo/bar/__init__.py")
-    chroot_rule_runner.create_file("src/python/foo/bar/bar.py")
-    # Add a `.pyi` stub file to ensure we include it in the final result.
-    chroot_rule_runner.create_file("src/python/foo/bar/bar.pyi")
-    chroot_rule_runner.add_to_build_file(
-        "src/python/foo/resources", 'resource(source="js/code.js")'
-    )
-    chroot_rule_runner.create_file("src/python/foo/resources/js/code.js")
-    chroot_rule_runner.add_to_build_file("files", 'file(source="README.txt")')
-    chroot_rule_runner.create_file("files/README.txt")
-    chroot_rule_runner.add_to_build_file(
-        "src/python/",
-        textwrap.dedent(
-            """
-            python_distribution(
-                name='foo-dist',
-                dependencies=[
-                    ':setup',
-                ],
-                generate_setup=False,
-                provides=setup_py(
-                    name='foo', version='1.2.3',
+    chroot_rule_runner.write_files(
+        {
+            "src/python/foo/bar/BUILD": "python_sources()",
+            "src/python/foo/bar/__init__.py": "",
+            "src/python/foo/bar/bar.py": "",
+            # Add a `.pyi` stub file to ensure we include it in the final result.
+            "src/python/foo/bar/bar.pyi": "",
+            "src/python/foo/resources/BUILD": 'resource(source="js/code.js")',
+            "src/python/foo/resources/js/code.js": "",
+            "files/BUILD": 'file(source="README.txt")',
+            "files/README.txt": "",
+            "src/python/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name='foo-dist',
+                    dependencies=[
+                        ':setup',
+                    ],
+                    generate_setup=False,
+                    provides=setup_py(
+                        name='foo', version='1.2.3',
+                    )
                 )
-            )
 
-            python_sources(name="setup", dependencies=["src/python/foo"])
-            """
-        ),
-    )
-    chroot_rule_runner.create_file(
-        "src/python/setup.py",
-        textwrap.dedent(
-            """
-        from setuptools import setup
-
-        setup(
-            name = "foo",
-            version = "1.2.3",
-            packages = ["foo"],
-        )
-    """
-        ),
-    )
-    chroot_rule_runner.add_to_build_file(
-        "src/python/foo",
-        textwrap.dedent(
-            """
-            python_sources(
-                dependencies=[
-                    'src/python/foo/bar',
-                    'src/python/foo/resources',
-                    'files',
-                ]
-            )
-            """
-        ),
+                python_sources(name="setup", dependencies=["src/python/foo"])
+                """
+            ),
+            "src/python/setup.py": textwrap.dedent(
+                """
+                from setuptools import setup
+
+                setup(
+                    name = "foo",
+                    version = "1.2.3",
+                    packages = ["foo"],
+                )
+                """
+            ),
+            "src/python/foo/BUILD": textwrap.dedent(
+                """
+                python_sources(
+                    dependencies=[
+                        'src/python/foo/bar',
+                        'src/python/foo/resources',
+                        'files',
+                    ]
+                )
+                """
+            ),
+            "src/python/foo/__init__.py": _namespace_decl,
+            "src/python/foo/foo.py": "",
+        }
     )
-    chroot_rule_runner.create_file("src/python/foo/__init__.py", _namespace_decl)
-    chroot_rule_runner.create_file("src/python/foo/foo.py")
     assert_chroot(
         chroot_rule_runner,
         [
@@ -266,76 +259,69 @@ def test_merge_entry_points() -> None:
 
 
 def test_generate_chroot(chroot_rule_runner: RuleRunner) -> None:
-    chroot_rule_runner.add_to_build_file(
-        "src/python/foo/bar/baz",
-        textwrap.dedent(
-            """
-            python_distribution(
-                name="baz-dist",
-                dependencies=[':baz'],
-                provides=setup_py(
-                    name='baz',
-                    version='1.1.1'
+    chroot_rule_runner.write_files(
+        {
+            "src/python/foo/bar/baz/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name="baz-dist",
+                    dependencies=[':baz'],
+                    provides=setup_py(
+                        name='baz',
+                        version='1.1.1'
+                    )
                 )
-            )
 
-            python_sources()
-            """
-        ),
-    )
-    chroot_rule_runner.create_file("src/python/foo/bar/baz/baz.py")
-    chroot_rule_runner.add_to_build_file(
-        "src/python/foo/qux",
-        textwrap.dedent(
-            """
-            python_sources()
-
-            pex_binary(name="bin", entry_point="foo.qux.bin:main")
-            """
-        ),
-    )
-    chroot_rule_runner.create_file("src/python/foo/qux/__init__.py")
-    chroot_rule_runner.create_file("src/python/foo/qux/qux.py")
-    # Add a `.pyi` stub file to ensure we include it in the final result.
-    chroot_rule_runner.create_file("src/python/foo/qux/qux.pyi")
-    chroot_rule_runner.add_to_build_file(
-        "src/python/foo/resources", 'resource(source="js/code.js")'
-    )
-    chroot_rule_runner.create_file("src/python/foo/resources/js/code.js")
-    chroot_rule_runner.add_to_build_file("files", 'file(source="README.txt")')
-    chroot_rule_runner.create_file("files/README.txt")
-    chroot_rule_runner.add_to_build_file(
-        "src/python/foo",
-        textwrap.dedent(
-            """
-            python_distribution(
-                name='foo-dist',
-                dependencies=[
-                    ':foo',
-                ],
-                provides=setup_py(
-                    name='foo', version='1.2.3'
-                ),
-                entry_points={
-                    "console_scripts":{
-                        "foo_main": "src/python/foo/qux:bin",
+                python_sources()
+                """
+            ),
+            "src/python/foo/bar/baz/baz.py": "",
+            "src/python/foo/qux/BUILD": textwrap.dedent(
+                """
+                python_sources()
+
+                pex_binary(name="bin", entry_point="foo.qux.bin:main")
+                """
+            ),
+            "src/python/foo/qux/__init__.py": "",
+            "src/python/foo/qux/qux.py": "",
+            # Add a `.pyi` stub file to ensure we include it in the final result.
+            "src/python/foo/qux/qux.pyi": "",
+            "src/python/foo/resources/BUILD": 'resource(source="js/code.js")',
+            "src/python/foo/resources/js/code.js": "",
+            "files/BUILD": 'file(source="README.txt")',
+            "files/README.txt": "",
+            "src/python/foo/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name='foo-dist',
+                    dependencies=[
+                        ':foo',
+                    ],
+                    provides=setup_py(
+                        name='foo', version='1.2.3'
+                    ),
+                    entry_points={
+                        "console_scripts":{
+                            "foo_main": "src/python/foo/qux:bin",
+                        },
                     },
-                },
-            )
+                )
 
-            python_sources(
-                dependencies=[
-                    'src/python/foo/bar/baz',
-                    'src/python/foo/qux',
-                    'src/python/foo/resources',
-                    'files',
-                ]
-            )
-            """
-        ),
+                python_sources(
+                    dependencies=[
+                        'src/python/foo/bar/baz',
+                        'src/python/foo/qux',
+                        'src/python/foo/resources',
+                        'files',
+                    ]
+                )
+                """
+            ),
+            "src/python/foo/__init__.py": _namespace_decl,
+            "src/python/foo/foo.py": "",
+        }
     )
-    chroot_rule_runner.create_file("src/python/foo/__init__.py", _namespace_decl)
-    chroot_rule_runner.create_file("src/python/foo/foo.py")
     assert_chroot(
         chroot_rule_runner,
         [
@@ -364,57 +350,55 @@ def test_generate_chroot(chroot_rule_runner: RuleRunner) -> None:
 
 
 def test_generate_chroot_entry_points(chroot_rule_runner: RuleRunner) -> None:
-    chroot_rule_runner.add_to_build_file(
-        "src/python/foo/qux",
-        textwrap.dedent(
-            """
-            python_sources()
-
-            pex_binary(name="bin", entry_point="foo.qux.bin:main")
-            """
-        ),
-    )
-    chroot_rule_runner.add_to_build_file(
-        "src/python/foo",
-        textwrap.dedent(
-            """
-            python_distribution(
-                name='foo-dist',
-                entry_points={
-                    "console_scripts":{
-                        "foo_main": "src/python/foo/qux:bin",
-                        "foo_tool":"foo.bar.baz:Tool.main",
-                        "bin_tool":"//src/python/foo/qux:bin",
-                        "bin_tool2":"src/python/foo/qux:bin",
-                        "hello":":foo-bin",
-                    },
-                    "foo_plugins":{
-                        "qux":"foo.qux",
-                    },
-                },
-                provides=setup_py(
-                    name='foo', version='1.2.3',
+    chroot_rule_runner.write_files(
+        {
+            "src/python/foo/qux/BUILD": textwrap.dedent(
+                """
+                python_sources()
+
+                pex_binary(name="bin", entry_point="foo.qux.bin:main")
+                """
+            ),
+            "src/python/foo/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name='foo-dist',
                     entry_points={
                         "console_scripts":{
-                            "foo_qux":"foo.baz.qux:main",
-                            "foo_bin":":foo-bin",
+                            "foo_main": "src/python/foo/qux:bin",
+                            "foo_tool":"foo.bar.baz:Tool.main",
+                            "bin_tool":"//src/python/foo/qux:bin",
+                            "bin_tool2":"src/python/foo/qux:bin",
+                            "hello":":foo-bin",
+                        },
+                        "foo_plugins":{
+                            "qux":"foo.qux",
                         },
-                        "foo_plugins":[
-                            "foo-bar=foo.bar:plugin",
-                        ],
                     },
+                    provides=setup_py(
+                        name='foo', version='1.2.3',
+                        entry_points={
+                            "console_scripts":{
+                                "foo_qux":"foo.baz.qux:main",
+                                "foo_bin":":foo-bin",
+                            },
+                            "foo_plugins":[
+                                "foo-bar=foo.bar:plugin",
+                            ],
+                        },
+                    )
                 )
-            )
 
-            python_sources(
-                dependencies=[
-                    'src/python/foo/qux',
-                ]
-            )
+                python_sources(
+                    dependencies=[
+                        'src/python/foo/qux',
+                    ]
+                )
 
-            pex_binary(name="foo-bin", entry_point="foo.bin:main")
-            """
-        ),
+                pex_binary(name="foo-bin", entry_point="foo.bin:main")
+                """
+            ),
+        }
     )
     assert_chroot(
         chroot_rule_runner,
@@ -451,49 +435,51 @@ def test_generate_chroot_entry_points(chroot_rule_runner: RuleRunner) -> None:
 
 
 def test_invalid_binary(chroot_rule_runner: RuleRunner) -> None:
-    chroot_rule_runner.create_files("src/python/invalid_binary", ["app1.py", "app2.py"])
-    chroot_rule_runner.add_to_build_file(
-        "src/python/invalid_binary",
-        textwrap.dedent(
-            """
-            python_sources(name='not_a_binary', sources=[])
-            pex_binary(name='invalid_entrypoint_unowned1', entry_point='app1.py')
-            pex_binary(name='invalid_entrypoint_unowned2', entry_point='invalid_binary.app2')
-            python_distribution(
-                name='invalid_bin1',
-                provides=setup_py(
-                    name='invalid_bin1', version='1.1.1'
-                ),
-                entry_points={
-                    "console_scripts":{
-                        "foo": ":not_a_binary",
+    chroot_rule_runner.write_files(
+        {
+            "src/python/invalid_binary/app1.py": "",
+            "src/python/invalid_binary/app2.py": "",
+            "src/python/invalid_binary/BUILD": textwrap.dedent(
+                """\
+                python_sources(name='not_a_binary', sources=[])
+                pex_binary(name='invalid_entrypoint_unowned1', entry_point='app1.py')
+                pex_binary(name='invalid_entrypoint_unowned2', entry_point='invalid_binary.app2')
+                python_distribution(
+                    name='invalid_bin1',
+                    provides=setup_py(
+                        name='invalid_bin1', version='1.1.1'
+                    ),
+                    entry_points={
+                        "console_scripts":{
+                            "foo": ":not_a_binary",
+                        },
                     },
-                },
-            )
-            python_distribution(
-                name='invalid_bin2',
-                provides=setup_py(
-                    name='invalid_bin2', version='1.1.1'
-                ),
-                entry_points={
-                    "console_scripts":{
-                        "foo": ":invalid_entrypoint_unowned1",
+                )
+                python_distribution(
+                    name='invalid_bin2',
+                    provides=setup_py(
+                        name='invalid_bin2', version='1.1.1'
+                    ),
+                    entry_points={
+                        "console_scripts":{
+                            "foo": ":invalid_entrypoint_unowned1",
+                        },
                     },
-                },
-            )
-            python_distribution(
-                name='invalid_bin3',
-                provides=setup_py(
-                    name='invalid_bin3', version='1.1.1'
-                ),
-                entry_points={
-                    "console_scripts":{
-                        "foo": ":invalid_entrypoint_unowned2",
+                )
+                python_distribution(
+                    name='invalid_bin3',
+                    provides=setup_py(
+                        name='invalid_bin3', version='1.1.1'
+                    ),
+                    entry_points={
+                        "console_scripts":{
+                            "foo": ":invalid_entrypoint_unowned2",
+                        },
                     },
-                },
-            )
-            """
-        ),
+                )
+                """
+            ),
+        }
     )
 
     assert_chroot_error(
@@ -514,26 +500,27 @@ def test_invalid_binary(chroot_rule_runner: RuleRunner) -> None:
 
 
 def test_binary_shorthand(chroot_rule_runner: RuleRunner) -> None:
-    chroot_rule_runner.create_file("src/python/project/app.py")
-    chroot_rule_runner.add_to_build_file(
-        "src/python/project",
-        textwrap.dedent(
-            """
-            python_sources()
-            pex_binary(name='bin', entry_point='app.py:func')
-            python_distribution(
-                name='dist',
-                provides=setup_py(
-                    name='bin', version='1.1.1'
-                ),
-                entry_points={
-                    "console_scripts":{
-                        "foo": ":bin",
+    chroot_rule_runner.write_files(
+        {
+            "src/python/project/app.py": "",
+            "src/python/project/BUILD": textwrap.dedent(
+                """
+                python_sources()
+                pex_binary(name='bin', entry_point='app.py:func')
+                python_distribution(
+                    name='dist',
+                    provides=setup_py(
+                        name='bin', version='1.1.1'
+                    ),
+                    entry_points={
+                        "console_scripts":{
+                            "foo": ":bin",
+                        },
                     },
-                },
-            )
-            """
-        ),
+                )
+                """
+            ),
+        }
     )
     assert_chroot(
         chroot_rule_runner,
@@ -572,37 +559,34 @@ def assert_sources(
             ]
         )
 
-        rule_runner.add_to_build_file(
-            "src/python/foo/bar/baz",
-            textwrap.dedent(
-                """
-                python_sources(name='baz1', sources=['baz1.py'])
-                python_sources(name='baz2', sources=['baz2.py'])
-                """
-            ),
-        )
-        rule_runner.create_file("src/python/foo/bar/baz/baz1.py")
-        rule_runner.create_file("src/python/foo/bar/baz/baz2.py")
-        rule_runner.create_file("src/python/foo/bar/__init__.py", _namespace_decl)
-        rule_runner.add_to_build_file("src/python/foo/qux", "python_sources()")
-        rule_runner.create_file("src/python/foo/qux/__init__.py")
-        rule_runner.create_file("src/python/foo/qux/qux.py")
-        rule_runner.add_to_build_file("src/python/foo/resources", 'resource(source="js/code.js")')
-        rule_runner.create_file("src/python/foo/resources/js/code.js")
-        rule_runner.create_file("src/python/foo/__init__.py")
-
-        # We synthesize an owner for the addrs, so we have something to put in SetupPyChrootRequest.
-        rule_runner.create_file(
-            "src/python/foo/BUILD",
-            textwrap.dedent(
-                f"""
-                python_distribution(
-                  name="dist",
-                  dependencies=["{'","'.join(addr.spec for addr in addrs)}"],
-                  provides=setup_py(name="foo", version="3.2.1"),
-                )
-            """
-            ),
+        rule_runner.write_files(
+            {
+                "src/python/foo/bar/baz/BUILD": textwrap.dedent(
+                    """
+                    python_sources(name='baz1', sources=['baz1.py'])
+                    python_sources(name='baz2', sources=['baz2.py'])
+                    """
+                ),
+                "src/python/foo/bar/baz/baz1.py": "",
+                "src/python/foo/bar/baz/baz2.py": "",
+                "src/python/foo/bar/__init__.py": _namespace_decl,
+                "src/python/foo/qux/BUILD": "python_sources()",
+                "src/python/foo/qux/__init__.py": "",
+                "src/python/foo/qux/qux.py": "",
+                "src/python/foo/resources/BUILD": 'resource(source="js/code.js")',
+                "src/python/foo/resources/js/code.js": "",
+                "src/python/foo/__init__.py": "",
+                # We synthesize an owner for the addrs, so we have something to put in SetupPyChrootRequest.
+                "src/python/foo/BUILD": textwrap.dedent(
+                    f"""
+                    python_distribution(
+                      name="dist",
+                      dependencies=["{'","'.join(addr.spec for addr in addrs)}"],
+                      provides=setup_py(name="foo", version="3.2.1"),
+                    )
+                    """
+                ),
+            }
         )
         owner_tgt = rule_runner.get_target(Address("src/python/foo", target_name="dist"))
         srcs = rule_runner.request(
@@ -692,58 +676,47 @@ def test_get_requirements() -> None:
             QueryRule(ExportedTargetRequirements, (DependencyOwner,)),
         ]
     )
-    rule_runner.add_to_build_file(
-        "3rdparty",
-        textwrap.dedent(
-            """
-            python_requirement(name='ext1', requirements=['ext1==1.22.333'])
-            python_requirement(name='ext2', requirements=['ext2==4.5.6'])
-            python_requirement(name='ext3', requirements=['ext3==0.0.1'])
-            """
-        ),
-    )
-    rule_runner.add_to_build_file(
-        "src/python/foo/bar/baz",
-        "python_sources(dependencies=['3rdparty:ext1'], sources=[])",
-    )
-    rule_runner.add_to_build_file(
-        "src/python/foo/bar/qux",
-        "python_sources(dependencies=['3rdparty:ext2', 'src/python/foo/bar/baz'], sources=[])",
-    )
-    rule_runner.add_to_build_file(
-        "src/python/foo/bar",
-        textwrap.dedent(
-            """
-            python_distribution(
-                name='bar-dist',
-                dependencies=[':bar'],
-                provides=setup_py(name='bar', version='9.8.7'),
-            )
+    rule_runner.write_files(
+        {
+            "3rdparty/BUILD": textwrap.dedent(
+                """
+                python_requirement(name='ext1', requirements=['ext1==1.22.333'])
+                python_requirement(name='ext2', requirements=['ext2==4.5.6'])
+                python_requirement(name='ext3', requirements=['ext3==0.0.1'])
+                """
+            ),
+            "src/python/foo/bar/baz/BUILD": "python_sources(dependencies=['3rdparty:ext1'], sources=[])",
+            "src/python/foo/bar/qux/BUILD": "python_sources(dependencies=['3rdparty:ext2', 'src/python/foo/bar/baz'], sources=[])",
+            "src/python/foo/bar/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name='bar-dist',
+                    dependencies=[':bar'],
+                    provides=setup_py(name='bar', version='9.8.7'),
+                )
 
-            python_sources(
-                sources=[],
-                dependencies=['src/python/foo/bar/baz', 'src/python/foo/bar/qux'],
-            )
-          """
-        ),
-    )
-    rule_runner.add_to_build_file(
-        "src/python/foo/corge",
-        textwrap.dedent(
-            """
-            python_distribution(
-                name='corge-dist',
-                # Tests having a 3rdparty requirement directly on a python_distribution.
-                dependencies=[':corge', '3rdparty:ext3'],
-                provides=setup_py(name='corge', version='2.2.2'),
-            )
+                python_sources(
+                    sources=[],
+                    dependencies=['src/python/foo/bar/baz', 'src/python/foo/bar/qux'],
+                )
+              """
+            ),
+            "src/python/foo/corge/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name='corge-dist',
+                    # Tests having a 3rdparty requirement directly on a python_distribution.
+                    dependencies=[':corge', '3rdparty:ext3'],
+                    provides=setup_py(name='corge', version='2.2.2'),
+                )
 
-            python_sources(
-                sources=[],
-                dependencies=['src/python/foo/bar'],
-            )
-            """
-        ),
+                python_sources(
+                    sources=[],
+                    dependencies=['src/python/foo/bar'],
+                )
+                """
+            ),
+        }
     )
 
     assert_requirements(
@@ -782,40 +755,32 @@ def test_get_requirements_with_exclude() -> None:
             QueryRule(ExportedTargetRequirements, (DependencyOwner,)),
         ]
     )
-    rule_runner.add_to_build_file(
-        "3rdparty",
-        textwrap.dedent(
-            """
-            python_requirement(name='ext1', requirements=['ext1==1.22.333'])
-            python_requirement(name='ext2', requirements=['ext2==4.5.6'])
-            python_requirement(name='ext3', requirements=['ext3==0.0.1'])
-            """
-        ),
-    )
-    rule_runner.add_to_build_file(
-        "src/python/foo/bar/baz",
-        "python_sources(dependencies=['3rdparty:ext1'], sources=[])",
-    )
-    rule_runner.add_to_build_file(
-        "src/python/foo/bar/qux",
-        "python_sources(dependencies=['3rdparty:ext2', 'src/python/foo/bar/baz'], sources=[])",
-    )
-    rule_runner.add_to_build_file(
-        "src/python/foo/bar",
-        textwrap.dedent(
-            """
-            python_distribution(
-                name='bar-dist',
-                dependencies=['!!3rdparty:ext2',':bar'],
-                provides=setup_py(name='bar', version='9.8.7'),
-            )
+    rule_runner.write_files(
+        {
+            "3rdparty/BUILD": textwrap.dedent(
+                """
+                python_requirement(name='ext1', requirements=['ext1==1.22.333'])
+                python_requirement(name='ext2', requirements=['ext2==4.5.6'])
+                python_requirement(name='ext3', requirements=['ext3==0.0.1'])
+                """
+            ),
+            "src/python/foo/bar/baz/BUILD": "python_sources(dependencies=['3rdparty:ext1'], sources=[])",
+            "src/python/foo/bar/qux/BUILD": "python_sources(dependencies=['3rdparty:ext2', 'src/python/foo/bar/baz'], sources=[])",
+            "src/python/foo/bar/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name='bar-dist',
+                    dependencies=['!!3rdparty:ext2',':bar'],
+                    provides=setup_py(name='bar', version='9.8.7'),
+                )
 
-            python_sources(
-                sources=[],
-                dependencies=['src/python/foo/bar/baz', 'src/python/foo/bar/qux'],
-            )
-          """
-        ),
+                python_sources(
+                    sources=[],
+                    dependencies=['src/python/foo/bar/baz', 'src/python/foo/bar/qux'],
+                )
+              """
+            ),
+        }
     )
 
     assert_requirements(
@@ -850,57 +815,52 @@ def test_owned_dependencies() -> None:
             QueryRule(OwnedDependencies, (DependencyOwner,)),
         ]
     )
-    rule_runner.add_to_build_file(
-        "src/python/foo/bar/baz",
-        textwrap.dedent(
-            """
-            python_sources(name='baz1', sources=[])
-            python_sources(name='baz2', sources=[])
-            """
-        ),
-    )
-    rule_runner.create_file("src/python/foo/bar/resource.txt")
-    rule_runner.add_to_build_file(
-        "src/python/foo/bar",
-        textwrap.dedent(
-            """
-            python_distribution(
-                name='bar1-dist',
-                dependencies=[':bar1'],
-                provides=setup_py(name='bar1', version='1.1.1'),
-            )
+    rule_runner.write_files(
+        {
+            "src/python/foo/bar/baz/BUILD": textwrap.dedent(
+                """
+                python_sources(name='baz1', sources=[])
+                python_sources(name='baz2', sources=[])
+                """
+            ),
+            "src/python/foo/bar/resource.txt": "",
+            "src/python/foo/bar/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name='bar1-dist',
+                    dependencies=[':bar1'],
+                    provides=setup_py(name='bar1', version='1.1.1'),
+                )
 
-            python_sources(
-                name='bar1',
-                sources=[],
-                dependencies=['src/python/foo/bar/baz:baz1'],
-            )
+                python_sources(
+                    name='bar1',
+                    sources=[],
+                    dependencies=['src/python/foo/bar/baz:baz1'],
+                )
 
-            python_sources(
-                name='bar2',
-                sources=[],
-                dependencies=[':bar-resources', 'src/python/foo/bar/baz:baz2'],
-            )
-            resource(name='bar-resources', source='resource.txt')
-            """
-        ),
-    )
-    rule_runner.add_to_build_file(
-        "src/python/foo",
-        textwrap.dedent(
-            """
-            python_distribution(
-                name='foo-dist',
-                dependencies=[':foo'],
-                provides=setup_py(name='foo', version='3.4.5'),
-            )
+                python_sources(
+                    name='bar2',
+                    sources=[],
+                    dependencies=[':bar-resources', 'src/python/foo/bar/baz:baz2'],
+                )
+                resource(name='bar-resources', source='resource.txt')
+                """
+            ),
+            "src/python/foo/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name='foo-dist',
+                    dependencies=[':foo'],
+                    provides=setup_py(name='foo', version='3.4.5'),
+                )
 
-            python_sources(
-                sources=[],
-                dependencies=['src/python/foo/bar:bar1', 'src/python/foo/bar:bar2'],
-            )
-            """
-        ),
+                python_sources(
+                    sources=[],
+                    dependencies=['src/python/foo/bar:bar1', 'src/python/foo/bar:bar2'],
+                )
+                """
+            ),
+        }
     )
 
     def assert_owned(owned: Iterable[str], exported: Address):
@@ -971,51 +931,46 @@ def assert_ambiguous_owner(rule_runner: RuleRunner, owned: Address):
 
 
 def test_get_owner_simple(exporting_owner_rule_runner: RuleRunner) -> None:
-    exporting_owner_rule_runner.add_to_build_file(
-        "src/python/foo/bar/baz",
-        textwrap.dedent(
-            """
-            python_sources(name='baz1', sources=[])
-            python_sources(name='baz2', sources=[])
-            """
-        ),
-    )
-    exporting_owner_rule_runner.create_file("src/python/foo/bar/resource.ext")
-    exporting_owner_rule_runner.add_to_build_file(
-        "src/python/foo/bar",
-        textwrap.dedent(
-            """
-            python_distribution(
-                name='bar1',
-                dependencies=['src/python/foo/bar/baz:baz1'],
-                provides=setup_py(name='bar1', version='1.1.1'),
-            )
-            python_sources(
-                name='bar2',
-                sources=[],
-                dependencies=[':bar-resources', 'src/python/foo/bar/baz:baz2'],
-            )
-            resource(name='bar-resources', source='resource.ext')
-            """
-        ),
-    )
-    exporting_owner_rule_runner.add_to_build_file(
-        "src/python/foo",
-        textwrap.dedent(
-            """
-            python_distribution(
-                name='foo1',
-                dependencies=['src/python/foo/bar/baz:baz2'],
-                provides=setup_py(name='foo1', version='0.1.2'),
-            )
-            python_sources(name='foo2', sources=[])
-            python_distribution(
-                name='foo3',
-                dependencies=['src/python/foo/bar:bar2'],
-                provides=setup_py(name='foo3', version='3.4.5'),
-            )
-            """
-        ),
+    exporting_owner_rule_runner.write_files(
+        {
+            "src/python/foo/bar/baz/BUILD": textwrap.dedent(
+                """
+                python_sources(name='baz1', sources=[])
+                python_sources(name='baz2', sources=[])
+                """
+            ),
+            "src/python/foo/bar/resource.ext": "",
+            "src/python/foo/bar/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name='bar1',
+                    dependencies=['src/python/foo/bar/baz:baz1'],
+                    provides=setup_py(name='bar1', version='1.1.1'),
+                )
+                python_sources(
+                    name='bar2',
+                    sources=[],
+                    dependencies=[':bar-resources', 'src/python/foo/bar/baz:baz2'],
+                )
+                resource(name='bar-resources', source='resource.ext')
+                """
+            ),
+            "src/python/foo/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name='foo1',
+                    dependencies=['src/python/foo/bar/baz:baz2'],
+                    provides=setup_py(name='foo1', version='0.1.2'),
+                )
+                python_sources(name='foo2', sources=[])
+                python_distribution(
+                    name='foo3',
+                    dependencies=['src/python/foo/bar:bar2'],
+                    provides=setup_py(name='foo3', version='3.4.5'),
+                )
+                """
+            ),
+        }
     )
 
     assert_is_owner(
@@ -1058,18 +1013,19 @@ def test_get_owner_simple(exporting_owner_rule_runner: RuleRunner) -> None:
 
 
 def test_get_owner_siblings(exporting_owner_rule_runner: RuleRunner) -> None:
-    exporting_owner_rule_runner.add_to_build_file(
-        "src/python/siblings",
-        textwrap.dedent(
-            """
-            python_sources(name='sibling1', sources=[])
-            python_distribution(
-                name='sibling2',
-                dependencies=['src/python/siblings:sibling1'],
-                provides=setup_py(name='siblings', version='2.2.2'),
-            )
-            """
-        ),
+    exporting_owner_rule_runner.write_files(
+        {
+            "src/python/siblings/BUILD": textwrap.dedent(
+                """
+                python_sources(name='sibling1', sources=[])
+                python_distribution(
+                    name='sibling2',
+                    dependencies=['src/python/siblings:sibling1'],
+                    provides=setup_py(name='siblings', version='2.2.2'),
+                )
+                """
+            ),
+        }
     )
 
     assert_is_owner(
@@ -1085,25 +1041,23 @@ def test_get_owner_siblings(exporting_owner_rule_runner: RuleRunner) -> None:
 
 
 def test_get_owner_not_an_ancestor(exporting_owner_rule_runner: RuleRunner) -> None:
-    exporting_owner_rule_runner.add_to_build_file(
-        "src/python/notanancestor/aaa",
-        textwrap.dedent(
-            """
-            python_sources(name='aaa', sources=[])
-            """
-        ),
-    )
-    exporting_owner_rule_runner.add_to_build_file(
-        "src/python/notanancestor/bbb",
-        textwrap.dedent(
-            """
-            python_distribution(
-                name='bbb',
-                dependencies=['src/python/notanancestor/aaa'],
-                provides=setup_py(name='bbb', version='11.22.33'),
-            )
-            """
-        ),
+    exporting_owner_rule_runner.write_files(
+        {
+            "src/python/notanancestor/aaa/BUILD": textwrap.dedent(
+                """
+                python_sources(name='aaa', sources=[])
+                """
+            ),
+            "src/python/notanancestor/bbb/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name='bbb',
+                    dependencies=['src/python/notanancestor/aaa'],
+                    provides=setup_py(name='bbb', version='11.22.33'),
+                )
+                """
+            ),
+        }
     )
 
     assert_no_owner(exporting_owner_rule_runner, Address("src/python/notanancestor/aaa"))
@@ -1115,37 +1069,32 @@ def test_get_owner_not_an_ancestor(exporting_owner_rule_runner: RuleRunner) -> N
 
 
 def test_get_owner_multiple_ancestor_generations(exporting_owner_rule_runner: RuleRunner) -> None:
-    exporting_owner_rule_runner.add_to_build_file(
-        "src/python/aaa/bbb/ccc",
-        textwrap.dedent(
-            """
-            python_sources(name='ccc', sources=[])
-            """
-        ),
-    )
-    exporting_owner_rule_runner.add_to_build_file(
-        "src/python/aaa/bbb",
-        textwrap.dedent(
-            """
-            python_distribution(
-                name='bbb',
-                dependencies=['src/python/aaa/bbb/ccc'],
-                provides=setup_py(name='bbb', version='1.1.1'),
-            )
-            """
-        ),
-    )
-    exporting_owner_rule_runner.add_to_build_file(
-        "src/python/aaa",
-        textwrap.dedent(
-            """
-            python_distribution(
-                name='aaa',
-                dependencies=['src/python/aaa/bbb/ccc'],
-                provides=setup_py(name='aaa', version='2.2.2'),
-            )
-            """
-        ),
+    exporting_owner_rule_runner.write_files(
+        {
+            "src/python/aaa/bbb/ccc/BUILD": textwrap.dedent(
+                """
+                python_sources(name='ccc', sources=[])
+                """
+            ),
+            "src/python/aaa/bbb/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name='bbb',
+                    dependencies=['src/python/aaa/bbb/ccc'],
+                    provides=setup_py(name='bbb', version='1.1.1'),
+                )
+                """
+            ),
+            "src/python/aaa/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name='aaa',
+                    dependencies=['src/python/aaa/bbb/ccc'],
+                    provides=setup_py(name='aaa', version='2.2.2'),
+                )
+                """
+            ),
+        }
     )
 
     assert_is_owner(
@@ -1219,18 +1168,19 @@ def test_no_dist_type_selected() -> None:
         target_types=[PythonDistribution],
         objects={"setup_py": PythonArtifact},
     )
-    rule_runner.add_to_build_file(
-        "src/python/aaa",
-        textwrap.dedent(
-            """
-            python_distribution(
-                name='aaa',
-                provides=setup_py(name='aaa', version='2.2.2'),
-                wheel=False,
-                sdist=False
-            )
-            """
-        ),
+    rule_runner.write_files(
+        {
+            "src/python/aaa/BUILD": textwrap.dedent(
+                """
+                python_distribution(
+                    name='aaa',
+                    provides=setup_py(name='aaa', version='2.2.2'),
+                    wheel=False,
+                    sdist=False
+                )
+                """
+            ),
+        }
     )
     address = Address("src/python/aaa", target_name="aaa")
     with pytest.raises(ExecutionError) as exc_info:
diff --git a/src/python/pants/backend/python/goals/tailor_test.py b/src/python/pants/backend/python/goals/tailor_test.py
--- a/src/python/pants/backend/python/goals/tailor_test.py
+++ b/src/python/pants/backend/python/goals/tailor_test.py
@@ -194,10 +194,13 @@ def test_find_putative_targets_for_entry_points(rule_runner: RuleRunner) -> None
             for name in mains
         }
     )
-    rule_runner.add_to_build_file(
-        "src/python/foo",
-        "pex_binary(name='main1', entry_point='main1.py')\n"
-        "pex_binary(name='main2', entry_point='foo.main2')\n",
+    rule_runner.write_files(
+        {
+            "src/python/foo/BUILD": (
+                "pex_binary(name='main1', entry_point='main1.py')\n"
+                "pex_binary(name='main2', entry_point='foo.main2')\n"
+            ),
+        }
     )
     pts = rule_runner.request(
         PutativeTargets,
diff --git a/src/python/pants/backend/python/macros/pants_requirement_caof_test.py b/src/python/pants/backend/python/macros/pants_requirement_caof_test.py
--- a/src/python/pants/backend/python/macros/pants_requirement_caof_test.py
+++ b/src/python/pants/backend/python/macros/pants_requirement_caof_test.py
@@ -32,7 +32,7 @@ def assert_pants_requirement(
     expected_dist: str = "pantsbuild.pants",
     expected_module: str = "pants",
 ) -> None:
-    rule_runner.add_to_build_file("3rdparty/python", f"{build_file_entry}\n")
+    rule_runner.write_files({"3rdparty/python/BUILD": f"{build_file_entry}\n"})
     target = rule_runner.get_target(Address("3rdparty/python", target_name=expected_target_name))
     assert isinstance(target, PythonRequirementTarget)
     assert target[PythonRequirementsField].value == (
diff --git a/src/python/pants/backend/python/target_types_test.py b/src/python/pants/backend/python/target_types_test.py
--- a/src/python/pants/backend/python/target_types_test.py
+++ b/src/python/pants/backend/python/target_types_test.py
@@ -151,8 +151,12 @@ def assert_resolved(
         *, entry_point: str | None, expected: EntryPoint | None, is_file: bool
     ) -> None:
         addr = Address("src/python/project")
-        rule_runner.create_file("src/python/project/app.py")
-        rule_runner.create_file("src/python/project/f2.py")
+        rule_runner.write_files(
+            {
+                "src/python/project/app.py": "",
+                "src/python/project/f2.py": "",
+            }
+        )
         ep_field = PexEntryPointField(entry_point, addr)
         result = rule_runner.request(ResolvedPexEntryPoint, [ResolvePexEntryPointRequest(ep_field)])
         assert result.val == expected
@@ -396,88 +400,83 @@ def test_inject_python_distribution_dependencies() -> None:
         ],
         objects={"setup_py": PythonArtifact},
     )
-    rule_runner.add_to_build_file(
-        "",
-        dedent(
-            """\
-            python_requirement(
-                name='ansicolors',
-                requirements=['ansicolors'],
-                modules=['colors'],
-            )
-            """
-        ),
-    )
-    rule_runner.create_file("project/app.py")
-    rule_runner.add_to_build_file(
-        "project",
-        dedent(
-            """\
-            pex_binary(name="my_binary", entry_point="who_knows.module:main")
-
-            python_sources(name="my_library", sources=["app.py"])
-
-            python_distribution(
-                name="dist-a",
-                provides=setup_py(
-                    name='my-dist-a'
-                ),
-                entry_points={
-                    "console_scripts":{
-                        "my_cmd": ":my_binary",
-                    },
-                },
-            )
-
-            python_distribution(
-                name="dist-b",
-                provides=setup_py(
-                    name="my-dist-b"
-                ),
-                entry_points={
-                    "console_scripts":{
-                        "b_cmd": "project.app:main",
-                        "cmd_2": "//project:my_binary",
-                    }
-                },
-            )
-
-            python_distribution(
-                name="third_dep",
-                provides=setup_py(name="my-third"),
-                entry_points={
-                    "color-plugins":{
-                        "my-ansi-colors": "colors",
-                    }
-                }
-            )
+    rule_runner.write_files(
+        {
+            "BUILD": dedent(
+                """\
+                python_requirement(
+                    name='ansicolors',
+                    requirements=['ansicolors'],
+                    modules=['colors'],
+                )
+                """
+            ),
+            "project/app.py": "",
+            "project/BUILD": dedent(
+                """\
+                pex_binary(name="my_binary", entry_point="who_knows.module:main")
 
-            python_distribution(
-                name="third_dep2",
-                provides=setup_py(
-                    name="my-third",
+                python_sources(name="my_library", sources=["app.py"])
+
+                python_distribution(
+                    name="dist-a",
+                    provides=setup_py(
+                        name='my-dist-a'
+                    ),
                     entry_points={
                         "console_scripts":{
-                            "my-cmd": ":my_binary",
-                            "main": "project.app:main",
+                            "my_cmd": ":my_binary",
                         },
+                    },
+                )
+
+                python_distribution(
+                    name="dist-b",
+                    provides=setup_py(
+                        name="my-dist-b"
+                    ),
+                    entry_points={
+                        "console_scripts":{
+                            "b_cmd": "project.app:main",
+                            "cmd_2": "//project:my_binary",
+                        }
+                    },
+                )
+
+                python_distribution(
+                    name="third_dep",
+                    provides=setup_py(name="my-third"),
+                    entry_points={
                         "color-plugins":{
                             "my-ansi-colors": "colors",
                         }
                     }
                 )
-            )
-            """
-        ),
-    )
-    rule_runner.create_file("who_knows/module.py")
-    rule_runner.add_to_build_file(
-        "who_knows",
-        dedent(
-            """\
-            python_sources(name="random_lib", sources=["module.py"])
-            """
-        ),
+
+                python_distribution(
+                    name="third_dep2",
+                    provides=setup_py(
+                        name="my-third",
+                        entry_points={
+                            "console_scripts":{
+                                "my-cmd": ":my_binary",
+                                "main": "project.app:main",
+                            },
+                            "color-plugins":{
+                                "my-ansi-colors": "colors",
+                            }
+                        }
+                    )
+                )
+                """
+            ),
+            "who_knows/module.py": "",
+            "who_knows/BUILD": dedent(
+                """\
+                python_sources(name="random_lib", sources=["module.py"])
+                """
+            ),
+        }
     )
 
     def assert_injected(address: Address, expected: list[Address]) -> None:
diff --git a/src/python/pants/backend/python/util_rules/ancestor_files_test.py b/src/python/pants/backend/python/util_rules/ancestor_files_test.py
--- a/src/python/pants/backend/python/util_rules/ancestor_files_test.py
+++ b/src/python/pants/backend/python/util_rules/ancestor_files_test.py
@@ -34,8 +34,7 @@ def assert_injected(
     expected_discovered: List[str],
 ) -> None:
     rule_runner.set_options([f"--source-root-patterns={source_roots}"])
-    for f in original_undeclared_files:
-        rule_runner.create_file(f, "# undeclared")
+    rule_runner.write_files({f: "# undeclared" for f in original_undeclared_files})
     request = AncestorFilesRequest(
         "__init__.py",
         rule_runner.make_snapshot({fp: "# declared" for fp in original_declared_files}),
diff --git a/src/python/pants/backend/python/util_rules/pex_from_targets_test.py b/src/python/pants/backend/python/util_rules/pex_from_targets_test.py
--- a/src/python/pants/backend/python/util_rules/pex_from_targets_test.py
+++ b/src/python/pants/backend/python/util_rules/pex_from_targets_test.py
@@ -149,33 +149,31 @@ def test_constraints_validation(tmp_path_factory: TempPathFactory, rule_runner:
     # so we explicitly mention localhost.
     url_req = f"foorl@ git+file://localhost{foorl_dir.as_posix()}@9.8.7"
 
-    rule_runner.add_to_build_file(
-        "",
-        dedent(
-            f"""
-            python_requirement(name="foo", requirements=["foo-bar>=0.1.2"])
-            python_requirement(name="bar", requirements=["bar==5.5.5"])
-            python_requirement(name="baz", requirements=["baz"])
-            python_requirement(name="foorl", requirements=["{url_req}"])
-            python_sources(name="util", sources=[], dependencies=[":foo", ":bar"])
-            python_sources(name="app", sources=[], dependencies=[":util", ":baz", ":foorl"])
-            """
-        ),
-    )
-    rule_runner.create_file(
-        "constraints1.txt",
-        dedent(
-            """
-            # Comment.
-            --find-links=https://duckduckgo.com
-            Foo._-BAR==1.0.0  # Inline comment.
-            bar==5.5.5
-            baz==2.2.2
-            qux==3.4.5
-            # Note that pip does not allow URL requirements in constraints files,
-            # so there is no mention of foorl here.
-        """
-        ),
+    rule_runner.write_files(
+        {
+            "BUILD": dedent(
+                f"""
+                python_requirement(name="foo", requirements=["foo-bar>=0.1.2"])
+                python_requirement(name="bar", requirements=["bar==5.5.5"])
+                python_requirement(name="baz", requirements=["baz"])
+                python_requirement(name="foorl", requirements=["{url_req}"])
+                python_sources(name="util", sources=[], dependencies=[":foo", ":bar"])
+                python_sources(name="app", sources=[], dependencies=[":util", ":baz", ":foorl"])
+                """
+            ),
+            "constraints1.txt": dedent(
+                """
+                # Comment.
+                --find-links=https://duckduckgo.com
+                Foo._-BAR==1.0.0  # Inline comment.
+                bar==5.5.5
+                baz==2.2.2
+                qux==3.4.5
+                # Note that pip does not allow URL requirements in constraints files,
+                # so there is no mention of foorl here.
+                """
+            ),
+        }
     )
 
     def get_pex_request(
diff --git a/src/python/pants/backend/python/util_rules/pex_test.py b/src/python/pants/backend/python/util_rules/pex_test.py
--- a/src/python/pants/backend/python/util_rules/pex_test.py
+++ b/src/python/pants/backend/python/util_rules/pex_test.py
@@ -457,7 +457,7 @@ def assert_direct_requirements(pex_info):
         "idna==2.5",
         "urllib3==1.21.1",
     ]
-    rule_runner.create_file("constraints.txt", "\n".join(constraints))
+    rule_runner.write_files({"constraints.txt": "\n".join(constraints)})
     constrained_pex_info = create_pex_and_get_pex_info(
         rule_runner,
         requirements=PexRequirements(direct_deps, apply_constraints=True),
@@ -559,7 +559,7 @@ def test_venv_pex_resolve_info(rule_runner: RuleRunner, pex_type: type[Pex | Ven
         "idna==2.10",
         "urllib3==1.25.11",
     ]
-    rule_runner.create_file("constraints.txt", "\n".join(constraints))
+    rule_runner.write_files({"constraints.txt": "\n".join(constraints)})
     pex = create_pex_and_get_all_data(
         rule_runner,
         pex_type=pex_type,
diff --git a/src/python/pants/backend/terraform/lint/tffmt/tffmt_integration_test.py b/src/python/pants/backend/terraform/lint/tffmt/tffmt_integration_test.py
--- a/src/python/pants/backend/terraform/lint/tffmt/tffmt_integration_test.py
+++ b/src/python/pants/backend/terraform/lint/tffmt/tffmt_integration_test.py
@@ -83,11 +83,11 @@ def rule_runner() -> RuleRunner:
 def make_target(
     rule_runner: RuleRunner, source_files: List[FileContent], *, target_name="target"
 ) -> Target:
-    for source_file in source_files:
-        rule_runner.create_file(f"{source_file.path}", source_file.content.decode())
-    rule_runner.add_to_build_file(
-        "",
-        f"terraform_module(name='{target_name}')\n",
+    rule_runner.write_files(
+        {
+            "BUILD": f"terraform_module(name='{target_name}')\n",
+            **{source_file.path: source_file.content.decode() for source_file in source_files},
+        }
     )
     return rule_runner.get_target(Address("", target_name=target_name))
 
diff --git a/src/python/pants/engine/fs_test.py b/src/python/pants/engine/fs_test.py
--- a/src/python/pants/engine/fs_test.py
+++ b/src/python/pants/engine/fs_test.py
@@ -85,7 +85,7 @@ def prime_store_with_roland_digest(rule_runner: RuleRunner) -> None:
 def setup_fs_test_tar(rule_runner: RuleRunner) -> None:
     """Extract fs_test.tar into the rule_runner's build root.
 
-    Note that we use a tar, rather than rule_runner.create_file(), because it has symlinks set up a
+    Note that we use a tar, rather than rule_runner.write_files(), because it has symlinks set up a
     certain way.
 
     Contents:
diff --git a/src/python/pants/engine/internals/engine_test.py b/src/python/pants/engine/internals/engine_test.py
--- a/src/python/pants/engine/internals/engine_test.py
+++ b/src/python/pants/engine/internals/engine_test.py
@@ -889,17 +889,17 @@ def test_streaming_workunits_expanded_specs(run_tracker: RunTracker) -> None:
             QueryRule(ProcessResult, (Process,)),
         ],
     )
-
     rule_runner.set_options(["--backend-packages=pants.backend.python"])
-
-    rule_runner.create_file("src/python/somefiles/BUILD", "python_sources()")
-    rule_runner.create_file("src/python/somefiles/a.py", "print('')")
-    rule_runner.create_file("src/python/somefiles/b.py", "print('')")
-
-    rule_runner.create_file("src/python/others/BUILD", "python_sources()")
-    rule_runner.create_file("src/python/others/a.py", "print('')")
-    rule_runner.create_file("src/python/others/b.py", "print('')")
-
+    rule_runner.write_files(
+        {
+            "src/python/somefiles/BUILD": "python_sources()",
+            "src/python/somefiles/a.py": "print('')",
+            "src/python/somefiles/b.py": "print('')",
+            "src/python/others/BUILD": "python_sources()",
+            "src/python/others/a.py": "print('')",
+            "src/python/others/b.py": "print('')",
+        }
+    )
     specs = SpecsParser(get_buildroot()).parse_specs(
         ["src/python/somefiles::", "src/python/others/b.py"]
     )
diff --git a/src/python/pants/engine/internals/graph_test.py b/src/python/pants/engine/internals/graph_test.py
--- a/src/python/pants/engine/internals/graph_test.py
+++ b/src/python/pants/engine/internals/graph_test.py
@@ -1150,8 +1150,8 @@ def sources_rule_runner() -> RuleRunner:
 
 def test_sources_normal_hydration(sources_rule_runner: RuleRunner) -> None:
     addr = Address("src/fortran", target_name="lib")
-    sources_rule_runner.create_files(
-        "src/fortran", files=["f1.f95", "f2.f95", "f1.f03", "ignored.f03"]
+    sources_rule_runner.write_files(
+        {f"src/fortran/{f}": "" for f in ["f1.f95", "f2.f95", "f1.f03", "ignored.f03"]}
     )
     sources = MultipleSourcesField(["f1.f95", "*.f03", "!ignored.f03", "!**/ignore*"], addr)
     hydrated_sources = sources_rule_runner.request(
@@ -1195,7 +1195,7 @@ class SingleSourceSubclass(SingleSourceField):
         pass
 
     addr = Address("", target_name="lib")
-    sources_rule_runner.create_files("", files=["f1.f95"])
+    sources_rule_runner.write_files({f: "" for f in ["f1.f95"]})
 
     valid_sources = SourcesSubclass(["*"], addr)
     hydrated_valid_sources = sources_rule_runner.request(
@@ -1232,7 +1232,7 @@ class SingleSourceSubclass(SingleSourceField):
 
 def test_sources_unmatched_globs(sources_rule_runner: RuleRunner) -> None:
     sources_rule_runner.set_options(["--files-not-found-behavior=error"])
-    sources_rule_runner.create_files("", files=["f1.f95"])
+    sources_rule_runner.write_files({f: "" for f in ["f1.f95"]})
     sources = MultipleSourcesField(["non_existent.f95"], Address("", target_name="lib"))
     with engine_error(contains="non_existent.f95"):
         sources_rule_runner.request(HydratedSources, [HydrateSourcesRequest(sources)])
@@ -1250,7 +1250,9 @@ class DefaultSources(MultipleSourcesField):
     # NB: Not all globs will be matched with these files, specifically `default.f03` will not
     # be matched. This is intentional to ensure that we use `any` glob conjunction rather
     # than the normal `all` conjunction.
-    sources_rule_runner.create_files("src/fortran", files=["default.f95", "f1.f08", "ignored.f08"])
+    sources_rule_runner.write_files(
+        {f"src/fortran/{f}": "" for f in ["default.f95", "f1.f08", "ignored.f08"]}
+    )
     sources = DefaultSources(None, addr)
     assert set(sources.value or ()) == set(DefaultSources.default)
 
@@ -1268,7 +1270,9 @@ class ExpectedExtensionsSingleSource(SingleSourceField):
         expected_file_extensions = (".f95", ".f03", "")
 
     addr = Address("src/fortran", target_name="lib")
-    sources_rule_runner.create_files("src/fortran", ["s.f95", "s.f03", "s.f08", "s"])
+    sources_rule_runner.write_files(
+        {f"src/fortran/{f}": "" for f in ["s.f95", "s.f03", "s.f08", "s"]}
+    )
 
     def get_source(src: str) -> str:
         sources = sources_rule_runner.request(
@@ -1297,7 +1301,7 @@ class ExpectedRange(MultipleSourcesField):
         # We allow for 1 or 3 files
         expected_num_files = range(1, 4, 2)
 
-    sources_rule_runner.create_files("", files=["f1.txt", "f2.txt", "f3.txt", "f4.txt"])
+    sources_rule_runner.write_files({f: "" for f in ["f1.txt", "f2.txt", "f3.txt", "f4.txt"]})
 
     def hydrate(sources_cls: Type[MultipleSourcesField], sources: Iterable[str]) -> HydratedSources:
         return sources_rule_runner.request(
diff --git a/src/python/pants/option/options_fingerprinter_test.py b/src/python/pants/option/options_fingerprinter_test.py
--- a/src/python/pants/option/options_fingerprinter_test.py
+++ b/src/python/pants/option/options_fingerprinter_test.py
@@ -48,7 +48,7 @@ def test_fingerprint_list() -> None:
 
 def test_fingerprint_file(rule_runner: RuleRunner) -> None:
     fp1, fp2, fp3 = (
-        OptionsFingerprinter().fingerprint(file_option, rule_runner.create_file(f, contents=c))
+        OptionsFingerprinter().fingerprint(file_option, rule_runner.write_files({f: c})[0])
         for (f, c) in (
             ("foo/bar.config", "blah blah blah"),
             ("foo/bar.config", "meow meow meow"),
@@ -61,14 +61,14 @@ def test_fingerprint_file(rule_runner: RuleRunner) -> None:
 
 
 def test_fingerprint_file_outside_buildroot(tmp_path: Path, rule_runner: RuleRunner) -> None:
-    outside_buildroot = rule_runner.create_file((tmp_path / "foobar").as_posix(), contents="foobar")
+    outside_buildroot = rule_runner.write_files({(tmp_path / "foobar").as_posix(): "foobar"})[0]
     with pytest.raises(ValueError):
         OptionsFingerprinter().fingerprint(file_option, outside_buildroot)
 
 
 def test_fingerprint_file_list(rule_runner: RuleRunner) -> None:
     f1, f2, f3 = (
-        rule_runner.create_file(f, contents=c)
+        rule_runner.write_files({f: c})[0]
         for (f, c) in (
             ("foo/bar.config", "blah blah blah"),
             ("foo/bar.config", "meow meow meow"),
@@ -98,14 +98,15 @@ def test_fingerprint_dir(rule_runner: RuleRunner) -> None:
     d2 = rule_runner.create_dir("b")
     d3 = rule_runner.create_dir("c")
 
-    for f, c in [
-        ("a/bar/bar.config", "blah blah blah"),
-        ("a/foo/foo.config", "meow meow meow"),
-        ("b/foo/foo.config", "meow meow meow"),
-        ("b/bar/bar.config", "blah blah blah"),
-        ("c/bar/bar.config", "blah meow blah"),
-    ]:
-        rule_runner.create_file(f, contents=c)
+    rule_runner.write_files(
+        {
+            "a/bar/bar.config": "blah blah blah",
+            "a/foo/foo.config": "meow meow meow",
+            "b/foo/foo.config": "meow meow meow",
+            "b/bar/bar.config": "blah blah blah",
+            "c/bar/bar.config": "blah meow blah",
+        }
+    )
 
     dp1 = OptionsFingerprinter().fingerprint(dir_option, [d1])
     dp2 = OptionsFingerprinter().fingerprint(dir_option, [d1, d2])
@@ -121,7 +122,7 @@ def test_fingerprint_dir(rule_runner: RuleRunner) -> None:
 
 def test_fingerprint_dict_with_files_order(rule_runner: RuleRunner) -> None:
     f1, f2 = (
-        rule_runner.create_file(f, contents=c)
+        rule_runner.write_files({f: c})[0]
         for (f, c) in (
             ("foo/bar.config", "blah blah blah"),
             ("foo/bar.config", "meow meow meow"),
@@ -134,7 +135,7 @@ def test_fingerprint_dict_with_files_order(rule_runner: RuleRunner) -> None:
 
 def test_fingerprint_dict_with_file_content_change(rule_runner: RuleRunner) -> None:
     f1, f2 = (
-        rule_runner.create_file(f, contents=c)
+        rule_runner.write_files({f: c})[0]
         for (f, c) in (
             ("foo/bar.config", "blah blah blah"),
             ("foo/bar.config", "meow meow meow"),