Repository: pypa/build

Issue:
Varying `config_settings` between hook calls
Configuration settings should be allowed to vary between distributions, hooks and hook calls, e.g. an option which is valid for a wheel might not be valid for an sdist or a different hook.  The `config_settings` parameter can either be removed from the constructor or hook wrappers can have their own `config_settings` parameter to overwrite the init value.

Comments:

Patch:
diff --git a/src/build/__init__.py b/src/build/__init__.py
index 1abf0eb..f6513c4 100644
--- a/src/build/__init__.py
+++ b/src/build/__init__.py
@@ -116,17 +116,15 @@ def _working_directory(path):  # type: (str) -> Iterator[None]
 
 
 class ProjectBuilder(object):
-    def __init__(self, srcdir, config_settings=None, python_executable=sys.executable):
-        # type: (str, Optional[ConfigSettings], Union[bytes, Text]) -> None
+    def __init__(self, srcdir, python_executable=sys.executable):
+        # type: (str, Union[bytes, Text]) -> None
         """
         Create a project builder.
 
         :param srcdir: the source directory
-        :param config_settings: config settings for the build backend
         :param python_executable: the python executable where the backend lives
         """
         self.srcdir = os.path.abspath(srcdir)  # type: str
-        self.config_settings = config_settings if config_settings else {}  # type: ConfigSettings
 
         spec_file = os.path.join(srcdir, 'pyproject.toml')
 
@@ -188,42 +186,46 @@ class ProjectBuilder(object):
         """
         return set(self._build_system['requires'])
 
-    def get_dependencies(self, distribution):  # type: (str) -> Set[str]
+    def get_dependencies(self, distribution, config_settings=None):  # type: (str, Optional[ConfigSettings]) -> Set[str]
         """
         Return the dependencies defined by the backend in addition to
         :attr:`build_dependencies` for a given distribution.
 
         :param distribution: Distribution to get the dependencies of
             (``sdist`` or ``wheel``)
+        :param config_settings: Config settings for the build backend
         """
         get_requires = getattr(self._hook, 'get_requires_for_build_{}'.format(distribution))
 
         try:
             with _working_directory(self.srcdir):
-                return set(get_requires(self.config_settings))
+                return set(get_requires(config_settings))
         except pep517.wrappers.BackendUnavailable:
             raise BuildException("Backend '{}' is not available.".format(self._backend))
         except Exception as e:  # noqa: E722
             raise BuildBackendException('Backend operation failed: {}'.format(e))
 
-    def check_dependencies(self, distribution):  # type: (str) -> Set[Tuple[str, ...]]
+    def check_dependencies(self, distribution, config_settings=None):
+        # type: (str, Optional[ConfigSettings]) -> Set[Tuple[str, ...]]
         """
         Return the dependencies which are not satisfied from the combined set of
         :attr:`build_dependencies` and :meth:`get_dependencies` for a given
         distribution.
 
         :param distribution: Distribution to check (``sdist`` or ``wheel``)
+        :param config_settings: Config settings for the build backend
         :returns: Set of variable-length unmet dependency tuples
         """
-        dependencies = self.get_dependencies(distribution).union(self.build_dependencies)
+        dependencies = self.get_dependencies(distribution, config_settings).union(self.build_dependencies)
         return {u for d in dependencies for u in check_dependency(d)}
 
-    def build(self, distribution, outdir):  # type: (str, str) -> str
+    def build(self, distribution, outdir, config_settings=None):  # type: (str, str, Optional[ConfigSettings]) -> str
         """
         Build a distribution.
 
         :param distribution: Distribution to build (``sdist`` or ``wheel``)
         :param outdir: Output directory
+        :param config_settings: Config settings for the build backend
         :returns: The full path to the built distribution
         """
         build = getattr(self._hook, 'build_{}'.format(distribution))
@@ -237,7 +239,7 @@ class ProjectBuilder(object):
 
         try:
             with _working_directory(self.srcdir):
-                basename = build(outdir, self.config_settings)  # type: str
+                basename = build(outdir, config_settings)  # type: str
                 return os.path.join(outdir, basename)
         except pep517.wrappers.BackendUnavailable:
             raise BuildException("Backend '{}' is not available.".format(self._backend))
diff --git a/src/build/__main__.py b/src/build/__main__.py
index 4f6ef61..b08d84e 100644
--- a/src/build/__main__.py
+++ b/src/build/__main__.py
@@ -46,8 +46,8 @@ def _format_dep_chain(dep_chain):  # type: (Sequence[str]) -> str
     return ' -> '.join(dep.partition(';')[0].strip() for dep in dep_chain)
 
 
-def _build_in_isolated_env(builder, outdir, distributions):
-    # type: (ProjectBuilder, str, List[str]) -> None
+def _build_in_isolated_env(builder, outdir, distributions, config_settings):
+    # type: (ProjectBuilder, str, List[str], ConfigSettings) -> None
     for distribution in distributions:
         with IsolatedEnvBuilder() as env:
             builder.python_executable = env.executable
@@ -55,11 +55,11 @@ def _build_in_isolated_env(builder, outdir, distributions):
             env.install(builder.build_dependencies)
             # then get the extra required dependencies from the backend (which was installed in the call above :P)
             env.install(builder.get_dependencies(distribution))
-            builder.build(distribution, outdir)
+            builder.build(distribution, outdir, config_settings)
 
 
-def _build_in_current_env(builder, outdir, distributions, skip_dependencies=False):
-    # type: (ProjectBuilder, str, List[str], bool) -> None
+def _build_in_current_env(builder, outdir, distributions, config_settings, skip_dependencies=False):
+    # type: (ProjectBuilder, str, List[str], ConfigSettings, bool) -> None
     for dist in distributions:
         if not skip_dependencies:
             missing = builder.check_dependencies(dist)
@@ -69,7 +69,7 @@ def _build_in_current_env(builder, outdir, distributions, skip_dependencies=Fals
                     + ''.join('\n\t' + dep for deps in missing for dep in (deps[0], _format_dep_chain(deps[1:])) if dep)
                 )
 
-        builder.build(dist, outdir)
+        builder.build(dist, outdir, config_settings)
 
 
 def build_package(srcdir, outdir, distributions, config_settings=None, isolation=True, skip_dependencies=False):
@@ -88,11 +88,11 @@ def build_package(srcdir, outdir, distributions, config_settings=None, isolation
         config_settings = {}
 
     try:
-        builder = ProjectBuilder(srcdir, config_settings)
+        builder = ProjectBuilder(srcdir)
         if isolation:
-            _build_in_isolated_env(builder, outdir, distributions)
+            _build_in_isolated_env(builder, outdir, distributions, config_settings)
         else:
-            _build_in_current_env(builder, outdir, distributions, skip_dependencies)
+            _build_in_current_env(builder, outdir, distributions, config_settings, skip_dependencies)
     except BuildException as e:
         _error(str(e))
     except BuildBackendException as e:


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
index 445a2aa..840ec30 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -118,7 +118,7 @@ def test_build_isolated(mocker, test_flit_path):
     required_cmd.assert_called_with('sdist')
     install.assert_any_call(['dep1', 'dep2'])
 
-    build_cmd.assert_called_with('sdist', '.')
+    build_cmd.assert_called_with('sdist', '.', {})
 
 
 def test_build_no_isolation_check_deps_empty(mocker, test_flit_path):
@@ -128,7 +128,7 @@ def test_build_no_isolation_check_deps_empty(mocker, test_flit_path):
 
     build.__main__.build_package(test_flit_path, '.', ['sdist'], isolation=False)
 
-    build_cmd.assert_called_with('sdist', '.')
+    build_cmd.assert_called_with('sdist', '.', {})
 
 
 @pytest.mark.parametrize(
@@ -145,7 +145,7 @@ def test_build_no_isolation_with_check_deps(mocker, test_flit_path, missing_deps
 
     build.__main__.build_package(test_flit_path, '.', ['sdist'], isolation=False)
 
-    build_cmd.assert_called_with('sdist', '.')
+    build_cmd.assert_called_with('sdist', '.', {})
     error.assert_called_with('Missing dependencies:' + output)
 
 
diff --git a/tests/test_projectbuilder.py b/tests/test_projectbuilder.py
index f6c6bf4..43c6349 100644
--- a/tests/test_projectbuilder.py
+++ b/tests/test_projectbuilder.py
@@ -256,11 +256,11 @@ def test_build(mocker, test_flit_path, tmp_dir):
     builder._hook.build_wheel.side_effect = ['dist.whl', Exception]
 
     assert builder.build('sdist', tmp_dir) == os.path.join(tmp_dir, 'dist.tar.gz')
-    builder._hook.build_sdist.assert_called_with(tmp_dir, {})
+    builder._hook.build_sdist.assert_called_with(tmp_dir, None)
     build._working_directory.assert_called_with(test_flit_path)
 
     assert builder.build('wheel', tmp_dir) == os.path.join(tmp_dir, 'dist.whl')
-    builder._hook.build_wheel.assert_called_with(tmp_dir, {})
+    builder._hook.build_wheel.assert_called_with(tmp_dir, None)
     build._working_directory.assert_called_with(test_flit_path)
 
     with pytest.raises(build.BuildBackendException):
@@ -322,7 +322,7 @@ def test_relative_outdir(mocker, tmp_dir, test_flit_path):
 
     builder.build('sdist', '.')
 
-    builder._hook.build_sdist.assert_called_with(os.path.abspath('.'), {})
+    builder._hook.build_sdist.assert_called_with(os.path.abspath('.'), None)
 
 
 def test_not_dir_outdir(mocker, tmp_dir, test_flit_path):