Repository: tox-dev/tox

Issue:
Isolated build with 'enscons' as backend build system fails
I'm trying to use `tox` with `isolated_build` set to `true` and [`enscons`](https://github.com/dholth/enscons) as the backend build system. When `isolated_build` is not set (default is `false` I think), `tox` works fine. However, when `isolated_build` is set to `true`, the build fails with the following error:

```
action: .package, msg: get-build-requires
cwd: C:\Users\banga\Projects\Personal\labeling-tool
cmd: 'C:\Users\banga\Projects\Personal\labeling-tool\.tox\.package\Scripts\python' '.venv\Lib\site-packages\tox\helper\build_requires.py' enscons.api '' ''
Traceback (most recent call last):
  File ".venv\Lib\site-packages\tox\helper\build_requires.py", line 15, in <module>
    for_build_requires = backend.get_requires_for_build_sdist(None)
AttributeError: module 'enscons.api' has no attribute 'get_requires_for_build_sdist'
```

I might be wrong but I thought `get_requires_for_build_sdist` and `get_requires_for_build_wheel` were optional hooks in PEP 517 and should be assumed to return empty list if absent.

The tox configuration in `pyproject.toml` is:

```toml
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py37, py38, py39
isolated_build = true

[testenv]
deps = pytest
commands = pytest
"""
```

Comments:

Patch:
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index e794efc5..14d9bbad 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -99,6 +99,7 @@ Philip Thiem
 Pierre-Jean Campigotto
 Pierre-Luc Tessier Gagné
 Prakhar Gurunani
+Rahul Bangar
 Ronald Evers
 Ronny Pfannschmidt
 Ryuichi Ohori
diff --git a/docs/changelog/2130.bugfix.rst b/docs/changelog/2130.bugfix.rst
new file mode 100644
index 00000000..aac4524c
--- /dev/null
+++ b/docs/changelog/2130.bugfix.rst
@@ -0,0 +1,1 @@
+``get_requires_for_build_sdist`` hook (PEP 517) is assumed to return an empty list if left unimplemented by the backend build system - by :user:`oczkoisse`
diff --git a/src/tox/helper/build_requires.py b/src/tox/helper/build_requires.py
index aafb258c..a91671c0 100644
--- a/src/tox/helper/build_requires.py
+++ b/src/tox/helper/build_requires.py
@@ -12,6 +12,13 @@ backend = __import__(backend_spec, fromlist=["_trash"])
 if backend_obj:
     backend = getattr(backend, backend_obj)
 
-for_build_requires = backend.get_requires_for_build_sdist(None)
+try:
+    for_build_requires = backend.get_requires_for_build_sdist(None)
+except AttributeError:
+    # PEP 517 states that get_requires_for_build_sdist is optional for a build
+    # backend object. When the backend object omits it, the default
+    # implementation must be equivalent to return []
+    for_build_requires = []
+
 output = json.dumps(for_build_requires)
 print(output)


Test patch:
diff --git a/tests/unit/package/builder/test_package_builder_isolated.py b/tests/unit/package/builder/test_package_builder_isolated.py
index dd783d85..f05d8ae8 100644
--- a/tests/unit/package/builder/test_package_builder_isolated.py
+++ b/tests/unit/package/builder/test_package_builder_isolated.py
@@ -202,3 +202,71 @@ def test_isolated_build_script_args(tmp_path):
     # cannot import build_isolated because of its side effects
     script_path = os.path.join(os.path.dirname(tox.helper.__file__), "build_isolated.py")
     subprocess.check_call(("python", script_path, str(tmp_path), "setuptools.build_meta"))
+
+
+def test_isolated_build_backend_missing_hook(initproj, cmd):
+    """Verify that tox works with a backend missing optional hooks
+
+    PEP 517 allows backends to omit get_requires_for_build_sdist hook, in which
+    case a default implementation that returns an empty list should be assumed
+    instead of raising an error.
+    """
+    name = "ensconsproj"
+    version = "0.1"
+    src_root = "src"
+
+    initproj(
+        (name, version),
+        filedefs={
+            "pyproject.toml": """
+            [build-system]
+            requires = ["pytoml>=0.1", "enscons==0.26.0"]
+            build-backend = "enscons.api"
+
+            [tool.enscons]
+            name = "{name}"
+            version = "{version}"
+            description = "Example enscons project"
+            license = "MIT"
+            packages = ["{name}"]
+            src_root = "{src_root}"
+            """.format(
+                name=name, version=version, src_root=src_root
+            ),
+            "tox.ini": """
+            [tox]
+            isolated_build = true
+            """,
+            "SConstruct": """
+            import enscons
+
+            env = Environment(
+                tools=["default", "packaging", enscons.generate],
+                PACKAGE_METADATA=dict(
+                    name = "{name}",
+                    version = "{version}"
+                ),
+                WHEEL_TAG="py2.py3-none-any"
+            )
+
+            py_source = env.Glob("src/{name}/*.py")
+
+            purelib = env.Whl("purelib", py_source, root="{src_root}")
+            whl = env.WhlFile(purelib)
+
+            sdist = env.SDist(source=FindSourceFiles() + ["PKG-INFO"])
+            env.NoClean(sdist)
+            env.Alias("sdist", sdist)
+
+            develop = env.Command("#DEVELOP", enscons.egg_info_targets(env), enscons.develop)
+            env.Alias("develop", develop)
+
+            env.Default(whl, sdist)
+            """.format(
+                name=name, version=version, src_root=src_root
+            ),
+        },
+    )
+
+    result = cmd("--sdistonly", "-v", "-v", "-e", "py")
+    assert "scons: done building targets" in result.out, result.out