Repository: pypa/setuptools (python)

Issue:
pbr broke with removal of sys_executable and ScriptsWriter names
### setuptools version

80.3.0

### Python version

any

### OS

any

### Additional environment information

_No response_

### Description

setuptools release 80.3.0 broke [pbr](https://pypi.org/project/pbr). The pbr backend is commonly used, e.g. the entire OpenStack project is powered by pbr. Several years ago, setuptools 12.0 introduced the same regression.

### Expected behavior

A new release of setuptools should never break existing and commonly used packages and backends.

### How to Reproduce

`pip wheel --no-cache --no-deps --no-binary :all: stevedore`


### Output

```console
...
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/build_meta.py", line 404, in _build_with_temp_dir
          self.run_setup()
          ~~~~~~~~~~~~~~^^
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/build_meta.py", line 512, in run_setup
          super().run_setup(setup_script=setup_script)
          ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/build_meta.py", line 317, in run_setup
          exec(code, locals())
          ~~~~^^^^^^^^^^^^^^^^
        File "<string>", line 19, in <module>
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/__init__.py", line 117, in setup
          return distutils.core.setup(**attrs)
                 ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/_distutils/core.py", line 186, in setup
          return run_commands(dist)
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/_distutils/core.py", line 202, in run_commands
          dist.run_commands()
          ~~~~~~~~~~~~~~~~~^^
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/_distutils/dist.py", line 1002, in run_commands
          self.run_command(cmd)
          ~~~~~~~~~~~~~~~~^^^^^
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/dist.py", line 1106, in run_command
          super().run_command(command)
          ~~~~~~~~~~~~~~~~~~~^^^^^^^^^
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/_distutils/dist.py", line 1021, in run_command
          cmd_obj.run()
          ~~~~~~~~~~~^^
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/command/bdist_wheel.py", line 405, in run
          self.run_command("install")
          ~~~~~~~~~~~~~~~~^^^^^^^^^^^
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/_distutils/cmd.py", line 357, in run_command
          self.distribution.run_command(command)
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/dist.py", line 1106, in run_command
          super().run_command(command)
          ~~~~~~~~~~~~~~~~~~~^^^^^^^^^
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/_distutils/dist.py", line 1021, in run_command
          cmd_obj.run()
          ~~~~~~~~~~~^^
        File "/tmp/pip-build-env-fob8wgry/normal/lib/python3.13/site-packages/pbr/packaging.py", line 243, in run
          return du_install.install.run(self)
                 ~~~~~~~~~~~~~~~~~~~~~~^^^^^^
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/_distutils/command/install.py", line 700, in run
          self.run_command(cmd_name)
          ~~~~~~~~~~~~~~~~^^^^^^^^^^
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/_distutils/cmd.py", line 357, in run_command
          self.distribution.run_command(command)
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/dist.py", line 1106, in run_command
          super().run_command(command)
          ~~~~~~~~~~~~~~~~~~~^^^^^^^^^
        File "/tmp/pip-build-env-fob8wgry/overlay/lib/python3.13/site-packages/setuptools/_distutils/dist.py", line 1021, in run_command
          cmd_obj.run()
          ~~~~~~~~~~~^^
        File "/tmp/pip-build-env-fob8wgry/normal/lib/python3.13/site-packages/pbr/packaging.py", line 492, in run
          bs_cmd, 'executable', easy_install.sys_executable)
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^
      AttributeError: module 'setuptools.command.easy_install' has no attribute 'sys_executable'
      [end of output]

```


Pull request:
Restore pbr compatibility
- **Moved pbr setup into a fixture.**
- **Add a failing integration test. Ref #4976**
- **Restore ScriptWriter and sys_executable properties.**
- **Render the attributes dynamically.**
- **Add the deprecation warning to attribute access.**

<!-- First time contributors: Take a moment to review https://setuptools.pypa.io/en/latest/development/developer-guide.html! -->
<!-- Remove sections if not applicable -->

## Summary of changes

<!-- Summary goes here -->

Closes #4976

### Pull Request Checklist
- [x] Changes have tests
- [x] News fragment added in [`newsfragments/`].
  _(See [documentation][PR docs] for details)_


[`newsfragments/`]: https://github.com/pypa/setuptools/tree/main/newsfragments
[PR docs]:
https://setuptools.pypa.io/en/latest/development/developer-guide.html#making-a-pull-request


Patch:
diff --git a/newsfragments/4976.bugfix.rst b/newsfragments/4976.bugfix.rst
new file mode 100644
index 000000000..27a988a5f
--- /dev/null
+++ b/newsfragments/4976.bugfix.rst
@@ -0,0 +1,1 @@
+Restored select attributes in easy_install for temporary pbr compatibility.
diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py
index c4bc88d58..8765793d4 100644
--- a/setuptools/command/easy_install.py
+++ b/setuptools/command/easy_install.py
@@ -1,5 +1,30 @@
+import os
+import sys
+import types
+
 from setuptools import Command
 
+from .. import _scripts, warnings
+
 
 class easy_install(Command):
     """Stubbed command for temporary pbr compatibility."""
+
+
+def __getattr__(name):
+    attr = getattr(
+        types.SimpleNamespace(
+            ScriptWriter=_scripts.ScriptWriter,
+            sys_executable=os.environ.get(
+                "__PYVENV_LAUNCHER__", os.path.normpath(sys.executable)
+            ),
+        ),
+        name,
+    )
+    warnings.SetuptoolsDeprecationWarning.emit(
+        summary="easy_install module is deprecated",
+        details="Avoid accessing attributes of setuptools.command.easy_install.",
+        due_date=(2025, 10, 31),
+        see_url="https://github.com/pypa/setuptools/issues/4976",
+    )
+    return attr


Test patch:
diff --git a/setuptools/tests/fixtures.py b/setuptools/tests/fixtures.py
index 0736f14c1..27a169896 100644
--- a/setuptools/tests/fixtures.py
+++ b/setuptools/tests/fixtures.py
@@ -7,6 +7,7 @@ import tarfile
 import time
 from pathlib import Path
 
+import jaraco.path
 import path
 import pytest
 
@@ -347,3 +348,45 @@ def create_setup_requires_package(
     make_package(foobar_path, distname, version)
 
     return test_pkg
+
+
+@pytest.fixture
+def pbr_package(tmp_path, monkeypatch, venv):
+    files = {
+        "pyproject.toml": DALS(
+            """
+            [build-system]
+            requires = ["setuptools"]
+            build-backend = "setuptools.build_meta"
+            """
+        ),
+        "setup.py": DALS(
+            """
+            __import__('setuptools').setup(
+                pbr=True,
+                setup_requires=["pbr"],
+            )
+            """
+        ),
+        "setup.cfg": DALS(
+            """
+            [metadata]
+            name = mypkg
+
+            [files]
+            packages =
+                mypkg
+            """
+        ),
+        "mypkg": {
+            "__init__.py": "",
+            "hello.py": "print('Hello world!')",
+        },
+        "other": {"test.txt": "Another file in here."},
+    }
+    venv.run(["python", "-m", "pip", "install", "pbr"])
+    prefix = tmp_path / 'mypkg'
+    prefix.mkdir()
+    jaraco.path.build(files, prefix=prefix)
+    monkeypatch.setenv('PBR_VERSION', "0.42")
+    return prefix
diff --git a/setuptools/tests/integration/test_pbr.py b/setuptools/tests/integration/test_pbr.py
new file mode 100644
index 000000000..a3e4e095c
--- /dev/null
+++ b/setuptools/tests/integration/test_pbr.py
@@ -0,0 +1,17 @@
+import subprocess
+
+
+def test_pbr_integration(pbr_package, venv):
+    """Ensure pbr packages install."""
+    cmd = [
+        'python',
+        '-m',
+        'pip',
+        '-v',
+        'install',
+        '--no-build-isolation',
+        pbr_package,
+    ]
+    venv.run(cmd, stderr=subprocess.STDOUT)
+    out = venv.run(["python", "-c", "import mypkg.hello"])
+    assert "Hello world!" in out
diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py
index 038dcadf9..5d85f2fb6 100644
--- a/setuptools/tests/test_editable_install.py
+++ b/setuptools/tests/test_editable_install.py
@@ -1068,45 +1068,19 @@ def test_compat_install(tmp_path, venv):
     assert "cannot import name 'subpackage'" in out
 
 
-def test_pbr_integration(tmp_path, venv, editable_opts):
+def test_pbr_integration(pbr_package, venv, editable_opts):
     """Ensure editable installs work with pbr, issue #3500"""
-    files = {
-        "pyproject.toml": dedent(
-            """\
-            [build-system]
-            requires = ["setuptools"]
-            build-backend = "setuptools.build_meta"
-            """
-        ),
-        "setup.py": dedent(
-            """\
-            __import__('setuptools').setup(
-                pbr=True,
-                setup_requires=["pbr"],
-            )
-            """
-        ),
-        "setup.cfg": dedent(
-            """\
-            [metadata]
-            name = mypkg
-
-            [files]
-            packages =
-                mypkg
-            """
-        ),
-        "mypkg": {
-            "__init__.py": "",
-            "hello.py": "print('Hello world!')",
-        },
-        "other": {"test.txt": "Another file in here."},
-    }
-    venv.run(["python", "-m", "pip", "install", "pbr"])
-
-    with contexts.environment(PBR_VERSION="0.42"):
-        install_project("mypkg", venv, tmp_path, files, *editable_opts)
-
+    cmd = [
+        'python',
+        '-m',
+        'pip',
+        '-v',
+        'install',
+        '--editable',
+        pbr_package,
+        *editable_opts,
+    ]
+    venv.run(cmd, stderr=subprocess.STDOUT)
     out = venv.run(["python", "-c", "import mypkg.hello"])
     assert "Hello world!" in out