about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_apply_pyprojecttoml.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/setuptools/tests/config/test_apply_pyprojecttoml.py')
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/config/test_apply_pyprojecttoml.py772
1 files changed, 772 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_apply_pyprojecttoml.py b/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_apply_pyprojecttoml.py
new file mode 100644
index 00000000..489fd98e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -0,0 +1,772 @@
+"""Make sure that applying the configuration from pyproject.toml is equivalent to
+applying a similar configuration from setup.cfg
+
+To run these tests offline, please have a look on ``./downloads/preload.py``
+"""
+
+from __future__ import annotations
+
+import io
+import re
+import tarfile
+from inspect import cleandoc
+from pathlib import Path
+from unittest.mock import Mock
+
+import pytest
+from ini2toml.api import LiteTranslator
+from packaging.metadata import Metadata
+
+import setuptools  # noqa: F401 # ensure monkey patch to metadata
+from setuptools._static import is_static
+from setuptools.command.egg_info import write_requirements
+from setuptools.config import expand, pyprojecttoml, setupcfg
+from setuptools.config._apply_pyprojecttoml import _MissingDynamic, _some_attrgetter
+from setuptools.dist import Distribution
+from setuptools.errors import InvalidConfigError, RemovedConfigError
+from setuptools.warnings import InformationOnly, SetuptoolsDeprecationWarning
+
+from .downloads import retrieve_file, urls_from_file
+
+HERE = Path(__file__).parent
+EXAMPLES_FILE = "setupcfg_examples.txt"
+
+
+def makedist(path, **attrs):
+    return Distribution({"src_root": path, **attrs})
+
+
+def _mock_expand_patterns(patterns, *_, **__):
+    """
+    Allow comparing the given patterns for 2 dist objects.
+    We need to strip special chars to avoid errors when validating.
+    """
+    return [re.sub("[^a-z0-9]+", "", p, flags=re.I) or "empty" for p in patterns]
+
+
+@pytest.mark.parametrize("url", urls_from_file(HERE / EXAMPLES_FILE))
+@pytest.mark.filterwarnings("ignore")
+@pytest.mark.uses_network
+def test_apply_pyproject_equivalent_to_setupcfg(url, monkeypatch, tmp_path):
+    monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.0.1"))
+    monkeypatch.setattr(
+        Distribution, "_expand_patterns", Mock(side_effect=_mock_expand_patterns)
+    )
+    setupcfg_example = retrieve_file(url)
+    pyproject_example = Path(tmp_path, "pyproject.toml")
+    setupcfg_text = setupcfg_example.read_text(encoding="utf-8")
+    toml_config = LiteTranslator().translate(setupcfg_text, "setup.cfg")
+    pyproject_example.write_text(toml_config, encoding="utf-8")
+
+    dist_toml = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject_example)
+    dist_cfg = setupcfg.apply_configuration(makedist(tmp_path), setupcfg_example)
+
+    pkg_info_toml = core_metadata(dist_toml)
+    pkg_info_cfg = core_metadata(dist_cfg)
+    assert pkg_info_toml == pkg_info_cfg
+
+    if any(getattr(d, "license_files", None) for d in (dist_toml, dist_cfg)):
+        assert set(dist_toml.license_files) == set(dist_cfg.license_files)
+
+    if any(getattr(d, "entry_points", None) for d in (dist_toml, dist_cfg)):
+        print(dist_cfg.entry_points)
+        ep_toml = {
+            (k, *sorted(i.replace(" ", "") for i in v))
+            for k, v in dist_toml.entry_points.items()
+        }
+        ep_cfg = {
+            (k, *sorted(i.replace(" ", "") for i in v))
+            for k, v in dist_cfg.entry_points.items()
+        }
+        assert ep_toml == ep_cfg
+
+    if any(getattr(d, "package_data", None) for d in (dist_toml, dist_cfg)):
+        pkg_data_toml = {(k, *sorted(v)) for k, v in dist_toml.package_data.items()}
+        pkg_data_cfg = {(k, *sorted(v)) for k, v in dist_cfg.package_data.items()}
+        assert pkg_data_toml == pkg_data_cfg
+
+    if any(getattr(d, "data_files", None) for d in (dist_toml, dist_cfg)):
+        data_files_toml = {(k, *sorted(v)) for k, v in dist_toml.data_files}
+        data_files_cfg = {(k, *sorted(v)) for k, v in dist_cfg.data_files}
+        assert data_files_toml == data_files_cfg
+
+    assert set(dist_toml.install_requires) == set(dist_cfg.install_requires)
+    if any(getattr(d, "extras_require", None) for d in (dist_toml, dist_cfg)):
+        extra_req_toml = {(k, *sorted(v)) for k, v in dist_toml.extras_require.items()}
+        extra_req_cfg = {(k, *sorted(v)) for k, v in dist_cfg.extras_require.items()}
+        assert extra_req_toml == extra_req_cfg
+
+
+PEP621_EXAMPLE = """\
+[project]
+name = "spam"
+version = "2020.0.0"
+description = "Lovely Spam! Wonderful Spam!"
+readme = "README.rst"
+requires-python = ">=3.8"
+license-files = ["LICENSE.txt"]  # Updated to be PEP 639 compliant
+keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"]
+authors = [
+  {email = "hi@pradyunsg.me"},
+  {name = "Tzu-Ping Chung"}
+]
+maintainers = [
+  {name = "Brett Cannon", email = "brett@python.org"},
+  {name = "John X. Ãørçeč", email = "john@utf8.org"},
+  {name = "Γαμα קּ 東", email = "gama@utf8.org"},
+]
+classifiers = [
+  "Development Status :: 4 - Beta",
+  "Programming Language :: Python"
+]
+
+dependencies = [
+  "httpx",
+  "gidgethub[httpx]>4.0.0",
+  "django>2.1; os_name != 'nt'",
+  "django>2.0; os_name == 'nt'"
+]
+
+[project.optional-dependencies]
+test = [
+  "pytest < 5.0.0",
+  "pytest-cov[all]"
+]
+
+[project.urls]
+homepage = "http://example.com"
+documentation = "http://readthedocs.org"
+repository = "http://github.com"
+changelog = "http://github.com/me/spam/blob/master/CHANGELOG.md"
+
+[project.scripts]
+spam-cli = "spam:main_cli"
+
+[project.gui-scripts]
+spam-gui = "spam:main_gui"
+
+[project.entry-points."spam.magical"]
+tomatoes = "spam:main_tomatoes"
+"""
+
+PEP621_INTERNATIONAL_EMAIL_EXAMPLE = """\
+[project]
+name = "spam"
+version = "2020.0.0"
+authors = [
+  {email = "hi@pradyunsg.me"},
+  {name = "Tzu-Ping Chung"}
+]
+maintainers = [
+  {name = "Степан Бандера", email = "криївка@оун-упа.укр"},
+]
+"""
+
+PEP621_EXAMPLE_SCRIPT = """
+def main_cli(): pass
+def main_gui(): pass
+def main_tomatoes(): pass
+"""
+
+PEP639_LICENSE_TEXT = """\
+[project]
+name = "spam"
+version = "2020.0.0"
+authors = [
+  {email = "hi@pradyunsg.me"},
+  {name = "Tzu-Ping Chung"}
+]
+license = {text = "MIT"}
+"""
+
+PEP639_LICENSE_EXPRESSION = """\
+[project]
+name = "spam"
+version = "2020.0.0"
+authors = [
+  {email = "hi@pradyunsg.me"},
+  {name = "Tzu-Ping Chung"}
+]
+license = "mit or apache-2.0"  # should be normalized in metadata
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Programming Language :: Python",
+]
+"""
+
+
+def _pep621_example_project(
+    tmp_path,
+    readme="README.rst",
+    pyproject_text=PEP621_EXAMPLE,
+):
+    pyproject = tmp_path / "pyproject.toml"
+    text = pyproject_text
+    replacements = {'readme = "README.rst"': f'readme = "{readme}"'}
+    for orig, subst in replacements.items():
+        text = text.replace(orig, subst)
+    pyproject.write_text(text, encoding="utf-8")
+
+    (tmp_path / readme).write_text("hello world", encoding="utf-8")
+    (tmp_path / "LICENSE.txt").write_text("--- LICENSE stub ---", encoding="utf-8")
+    (tmp_path / "spam.py").write_text(PEP621_EXAMPLE_SCRIPT, encoding="utf-8")
+    return pyproject
+
+
+def test_pep621_example(tmp_path):
+    """Make sure the example in PEP 621 works"""
+    pyproject = _pep621_example_project(tmp_path)
+    dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+    assert set(dist.metadata.license_files) == {"LICENSE.txt"}
+
+
+@pytest.mark.parametrize(
+    ("readme", "ctype"),
+    [
+        ("Readme.txt", "text/plain"),
+        ("readme.md", "text/markdown"),
+        ("text.rst", "text/x-rst"),
+    ],
+)
+def test_readme_content_type(tmp_path, readme, ctype):
+    pyproject = _pep621_example_project(tmp_path, readme)
+    dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+    assert dist.metadata.long_description_content_type == ctype
+
+
+def test_undefined_content_type(tmp_path):
+    pyproject = _pep621_example_project(tmp_path, "README.tex")
+    with pytest.raises(ValueError, match="Undefined content type for README.tex"):
+        pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+
+def test_no_explicit_content_type_for_missing_extension(tmp_path):
+    pyproject = _pep621_example_project(tmp_path, "README")
+    dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+    assert dist.metadata.long_description_content_type is None
+
+
+@pytest.mark.parametrize(
+    ("pyproject_text", "expected_maintainers_meta_value"),
+    (
+        pytest.param(
+            PEP621_EXAMPLE,
+            (
+                'Brett Cannon <brett@python.org>, "John X. Ãørçeč" <john@utf8.org>, '
+                'Γαμα קּ 東 <gama@utf8.org>'
+            ),
+            id='non-international-emails',
+        ),
+        pytest.param(
+            PEP621_INTERNATIONAL_EMAIL_EXAMPLE,
+            'Степан Бандера <криївка@оун-упа.укр>',
+            marks=pytest.mark.xfail(
+                reason="CPython's `email.headerregistry.Address` only supports "
+                'RFC 5322, as of Nov 10, 2022 and latest Python 3.11.0',
+                strict=True,
+            ),
+            id='international-email',
+        ),
+    ),
+)
+def test_utf8_maintainer_in_metadata(  # issue-3663
+    expected_maintainers_meta_value,
+    pyproject_text,
+    tmp_path,
+):
+    pyproject = _pep621_example_project(
+        tmp_path,
+        "README",
+        pyproject_text=pyproject_text,
+    )
+    dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+    assert dist.metadata.maintainer_email == expected_maintainers_meta_value
+    pkg_file = tmp_path / "PKG-FILE"
+    with open(pkg_file, "w", encoding="utf-8") as fh:
+        dist.metadata.write_pkg_file(fh)
+    content = pkg_file.read_text(encoding="utf-8")
+    assert f"Maintainer-email: {expected_maintainers_meta_value}" in content
+
+
+@pytest.mark.parametrize(
+    (
+        'pyproject_text',
+        'license',
+        'license_expression',
+        'content_str',
+        'not_content_str',
+    ),
+    (
+        pytest.param(
+            PEP639_LICENSE_TEXT,
+            'MIT',
+            None,
+            'License: MIT',
+            'License-Expression: ',
+            id='license-text',
+            marks=[
+                pytest.mark.filterwarnings(
+                    "ignore:.project.license. as a TOML table is deprecated",
+                )
+            ],
+        ),
+        pytest.param(
+            PEP639_LICENSE_EXPRESSION,
+            None,
+            'MIT OR Apache-2.0',
+            'License-Expression: MIT OR Apache-2.0',
+            'License: ',
+            id='license-expression',
+        ),
+    ),
+)
+def test_license_in_metadata(
+    license,
+    license_expression,
+    content_str,
+    not_content_str,
+    pyproject_text,
+    tmp_path,
+):
+    pyproject = _pep621_example_project(
+        tmp_path,
+        "README",
+        pyproject_text=pyproject_text,
+    )
+    dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+    assert dist.metadata.license == license
+    assert dist.metadata.license_expression == license_expression
+    pkg_file = tmp_path / "PKG-FILE"
+    with open(pkg_file, "w", encoding="utf-8") as fh:
+        dist.metadata.write_pkg_file(fh)
+    content = pkg_file.read_text(encoding="utf-8")
+    assert "Metadata-Version: 2.4" in content
+    assert content_str in content
+    assert not_content_str not in content
+
+
+def test_license_classifier_with_license_expression(tmp_path):
+    text = PEP639_LICENSE_EXPRESSION.rsplit("\n", 2)[0]
+    pyproject = _pep621_example_project(
+        tmp_path,
+        "README",
+        f"{text}\n    \"License :: OSI Approved :: MIT License\"\n]",
+    )
+    msg = "License classifiers have been superseded by license expressions"
+    with pytest.raises(InvalidConfigError, match=msg) as exc:
+        pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+    assert "License :: OSI Approved :: MIT License" in str(exc.value)
+
+
+def test_license_classifier_without_license_expression(tmp_path):
+    text = """\
+    [project]
+    name = "spam"
+    version = "2020.0.0"
+    license = {text = "mit or apache-2.0"}
+    classifiers = ["License :: OSI Approved :: MIT License"]
+    """
+    pyproject = _pep621_example_project(tmp_path, "README", text)
+
+    msg1 = "License classifiers are deprecated(?:.|\n)*MIT License"
+    msg2 = ".project.license. as a TOML table is deprecated"
+    with (
+        pytest.warns(SetuptoolsDeprecationWarning, match=msg1),
+        pytest.warns(SetuptoolsDeprecationWarning, match=msg2),
+    ):
+        dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+    # Check license classifier is still included
+    assert dist.metadata.get_classifiers() == ["License :: OSI Approved :: MIT License"]
+
+
+class TestLicenseFiles:
+    def base_pyproject(
+        self,
+        tmp_path,
+        additional_text="",
+        license_toml='license = {file = "LICENSE.txt"}\n',
+    ):
+        text = PEP639_LICENSE_EXPRESSION
+
+        # Sanity-check
+        assert 'license = "mit or apache-2.0"' in text
+        assert 'license-files' not in text
+        assert "[tool.setuptools]" not in text
+
+        text = re.sub(
+            r"(license = .*)\n",
+            license_toml,
+            text,
+            count=1,
+        )
+        assert license_toml in text  # sanity check
+        text = f"{text}\n{additional_text}\n"
+        pyproject = _pep621_example_project(tmp_path, "README", pyproject_text=text)
+        return pyproject
+
+    def base_pyproject_license_pep639(self, tmp_path, additional_text=""):
+        return self.base_pyproject(
+            tmp_path,
+            additional_text=additional_text,
+            license_toml='license = "licenseref-Proprietary"'
+            '\nlicense-files = ["_FILE*"]\n',
+        )
+
+    def test_both_license_and_license_files_defined(self, tmp_path):
+        setuptools_config = '[tool.setuptools]\nlicense-files = ["_FILE*"]'
+        pyproject = self.base_pyproject(tmp_path, setuptools_config)
+
+        (tmp_path / "_FILE.txt").touch()
+        (tmp_path / "_FILE.rst").touch()
+
+        # Would normally match the `license_files` patterns, but we want to exclude it
+        # by being explicit. On the other hand, contents should be added to `license`
+        license = tmp_path / "LICENSE.txt"
+        license.write_text("LicenseRef-Proprietary\n", encoding="utf-8")
+
+        msg1 = "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'"
+        msg2 = ".project.license. as a TOML table is deprecated"
+        with (
+            pytest.warns(SetuptoolsDeprecationWarning, match=msg1),
+            pytest.warns(SetuptoolsDeprecationWarning, match=msg2),
+        ):
+            dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+        assert set(dist.metadata.license_files) == {"_FILE.rst", "_FILE.txt"}
+        assert dist.metadata.license == "LicenseRef-Proprietary\n"
+
+    def test_both_license_and_license_files_defined_pep639(self, tmp_path):
+        # Set license and license-files
+        pyproject = self.base_pyproject_license_pep639(tmp_path)
+
+        (tmp_path / "_FILE.txt").touch()
+        (tmp_path / "_FILE.rst").touch()
+
+        msg = "Normalizing.*LicenseRef"
+        with pytest.warns(InformationOnly, match=msg):
+            dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+        assert set(dist.metadata.license_files) == {"_FILE.rst", "_FILE.txt"}
+        assert dist.metadata.license is None
+        assert dist.metadata.license_expression == "LicenseRef-Proprietary"
+
+    def test_license_files_defined_twice(self, tmp_path):
+        # Set project.license-files and tools.setuptools.license-files
+        setuptools_config = '[tool.setuptools]\nlicense-files = ["_FILE*"]'
+        pyproject = self.base_pyproject_license_pep639(tmp_path, setuptools_config)
+
+        msg = "'project.license-files' is defined already. Remove 'tool.setuptools.license-files'"
+        with pytest.raises(InvalidConfigError, match=msg):
+            pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+    def test_default_patterns(self, tmp_path):
+        setuptools_config = '[tool.setuptools]\nzip-safe = false'
+        # ^ used just to trigger section validation
+        pyproject = self.base_pyproject(tmp_path, setuptools_config, license_toml="")
+
+        license_files = "LICENCE-a.html COPYING-abc.txt AUTHORS-xyz NOTICE,def".split()
+
+        for fname in license_files:
+            (tmp_path / fname).write_text(f"{fname}\n", encoding="utf-8")
+
+        dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+        assert (tmp_path / "LICENSE.txt").exists()  # from base example
+        assert set(dist.metadata.license_files) == {*license_files, "LICENSE.txt"}
+
+    def test_missing_patterns(self, tmp_path):
+        pyproject = self.base_pyproject_license_pep639(tmp_path)
+        assert list(tmp_path.glob("_FILE*")) == []  # sanity check
+
+        msg1 = "Cannot find any files for the given pattern.*"
+        msg2 = "Normalizing 'licenseref-Proprietary' to 'LicenseRef-Proprietary'"
+        with (
+            pytest.warns(SetuptoolsDeprecationWarning, match=msg1),
+            pytest.warns(InformationOnly, match=msg2),
+        ):
+            pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+    def test_deprecated_file_expands_to_text(self, tmp_path):
+        """Make sure the old example with ``license = {text = ...}`` works"""
+
+        assert 'license-files = ["LICENSE.txt"]' in PEP621_EXAMPLE  # sanity check
+        text = PEP621_EXAMPLE.replace(
+            'license-files = ["LICENSE.txt"]',
+            'license = {file = "LICENSE.txt"}',
+        )
+        pyproject = _pep621_example_project(tmp_path, pyproject_text=text)
+
+        msg = ".project.license. as a TOML table is deprecated"
+        with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
+            dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+        assert dist.metadata.license == "--- LICENSE stub ---"
+        assert set(dist.metadata.license_files) == {"LICENSE.txt"}  # auto-filled
+
+
+class TestPyModules:
+    # https://github.com/pypa/setuptools/issues/4316
+
+    def dist(self, name):
+        toml_config = f"""
+        [project]
+        name = "test"
+        version = "42.0"
+        [tool.setuptools]
+        py-modules = [{name!r}]
+        """
+        pyproject = Path("pyproject.toml")
+        pyproject.write_text(cleandoc(toml_config), encoding="utf-8")
+        return pyprojecttoml.apply_configuration(Distribution({}), pyproject)
+
+    @pytest.mark.parametrize("module", ["pip-run", "abc-d.λ-xyz-e"])
+    def test_valid_module_name(self, tmp_path, monkeypatch, module):
+        monkeypatch.chdir(tmp_path)
+        assert module in self.dist(module).py_modules
+
+    @pytest.mark.parametrize("module", ["pip run", "-pip-run", "pip-run-stubs"])
+    def test_invalid_module_name(self, tmp_path, monkeypatch, module):
+        monkeypatch.chdir(tmp_path)
+        with pytest.raises(ValueError, match="py-modules"):
+            self.dist(module).py_modules
+
+
+class TestExtModules:
+    def test_pyproject_sets_attribute(self, tmp_path, monkeypatch):
+        monkeypatch.chdir(tmp_path)
+        pyproject = Path("pyproject.toml")
+        toml_config = """
+        [project]
+        name = "test"
+        version = "42.0"
+        [tool.setuptools]
+        ext-modules = [
+          {name = "my.ext", sources = ["hello.c", "world.c"]}
+        ]
+        """
+        pyproject.write_text(cleandoc(toml_config), encoding="utf-8")
+        with pytest.warns(pyprojecttoml._ExperimentalConfiguration):
+            dist = pyprojecttoml.apply_configuration(Distribution({}), pyproject)
+        assert len(dist.ext_modules) == 1
+        assert dist.ext_modules[0].name == "my.ext"
+        assert set(dist.ext_modules[0].sources) == {"hello.c", "world.c"}
+
+
+class TestDeprecatedFields:
+    def test_namespace_packages(self, tmp_path):
+        pyproject = tmp_path / "pyproject.toml"
+        config = """
+        [project]
+        name = "myproj"
+        version = "42"
+        [tool.setuptools]
+        namespace-packages = ["myproj.pkg"]
+        """
+        pyproject.write_text(cleandoc(config), encoding="utf-8")
+        with pytest.raises(RemovedConfigError, match="namespace-packages"):
+            pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+
+class TestPresetField:
+    def pyproject(self, tmp_path, dynamic, extra_content=""):
+        content = f"[project]\nname = 'proj'\ndynamic = {dynamic!r}\n"
+        if "version" not in dynamic:
+            content += "version = '42'\n"
+        file = tmp_path / "pyproject.toml"
+        file.write_text(content + extra_content, encoding="utf-8")
+        return file
+
+    @pytest.mark.parametrize(
+        ("attr", "field", "value"),
+        [
+            ("license_expression", "license", "MIT"),
+            pytest.param(
+                *("license", "license", "Not SPDX"),
+                marks=[pytest.mark.filterwarnings("ignore:.*license. overwritten")],
+            ),
+            ("classifiers", "classifiers", ["Private :: Classifier"]),
+            ("entry_points", "scripts", {"console_scripts": ["foobar=foobar:main"]}),
+            ("entry_points", "gui-scripts", {"gui_scripts": ["bazquux=bazquux:main"]}),
+            pytest.param(
+                *("install_requires", "dependencies", ["six"]),
+                marks=[
+                    pytest.mark.filterwarnings("ignore:.*install_requires. overwritten")
+                ],
+            ),
+        ],
+    )
+    def test_not_listed_in_dynamic(self, tmp_path, attr, field, value):
+        """Setuptools cannot set a field if not listed in ``dynamic``"""
+        pyproject = self.pyproject(tmp_path, [])
+        dist = makedist(tmp_path, **{attr: value})
+        msg = re.compile(f"defined outside of `pyproject.toml`:.*{field}", re.S)
+        with pytest.warns(_MissingDynamic, match=msg):
+            dist = pyprojecttoml.apply_configuration(dist, pyproject)
+
+        dist_value = _some_attrgetter(f"metadata.{attr}", attr)(dist)
+        assert not dist_value
+
+    @pytest.mark.parametrize(
+        ("attr", "field", "value"),
+        [
+            ("license_expression", "license", "MIT"),
+            ("install_requires", "dependencies", []),
+            ("extras_require", "optional-dependencies", {}),
+            ("install_requires", "dependencies", ["six"]),
+            ("classifiers", "classifiers", ["Private :: Classifier"]),
+        ],
+    )
+    def test_listed_in_dynamic(self, tmp_path, attr, field, value):
+        pyproject = self.pyproject(tmp_path, [field])
+        dist = makedist(tmp_path, **{attr: value})
+        dist = pyprojecttoml.apply_configuration(dist, pyproject)
+        dist_value = _some_attrgetter(f"metadata.{attr}", attr)(dist)
+        assert dist_value == value
+
+    def test_license_files_exempt_from_dynamic(self, monkeypatch, tmp_path):
+        """
+        license-file is currently not considered in the context of dynamic.
+        As per 2025-02-19, https://packaging.python.org/en/latest/specifications/pyproject-toml/#license-files
+        allows setuptools to fill-in `license-files` the way it sees fit:
+
+        > If the license-files key is not defined, tools can decide how to handle license files.
+        > For example they can choose not to include any files or use their own
+        > logic to discover the appropriate files in the distribution.
+
+        Using license_files from setup.py to fill-in the value is in accordance
+        with this rule.
+        """
+        monkeypatch.chdir(tmp_path)
+        pyproject = self.pyproject(tmp_path, [])
+        dist = makedist(tmp_path, license_files=["LIC*"])
+        (tmp_path / "LIC1").write_text("42", encoding="utf-8")
+        dist = pyprojecttoml.apply_configuration(dist, pyproject)
+        assert dist.metadata.license_files == ["LIC1"]
+
+    def test_warning_overwritten_dependencies(self, tmp_path):
+        src = "[project]\nname='pkg'\nversion='0.1'\ndependencies=['click']\n"
+        pyproject = tmp_path / "pyproject.toml"
+        pyproject.write_text(src, encoding="utf-8")
+        dist = makedist(tmp_path, install_requires=["wheel"])
+        with pytest.warns(match="`install_requires` overwritten"):
+            dist = pyprojecttoml.apply_configuration(dist, pyproject)
+        assert "wheel" not in dist.install_requires
+
+    def test_optional_dependencies_dont_remove_env_markers(self, tmp_path):
+        """
+        Internally setuptools converts dependencies with markers to "extras".
+        If ``install_requires`` is given by ``setup.py``, we have to ensure that
+        applying ``optional-dependencies`` does not overwrite the mandatory
+        dependencies with markers (see #3204).
+        """
+        # If setuptools replace its internal mechanism that uses `requires.txt`
+        # this test has to be rewritten to adapt accordingly
+        extra = "\n[project.optional-dependencies]\nfoo = ['bar>1']\n"
+        pyproject = self.pyproject(tmp_path, ["dependencies"], extra)
+        install_req = ['importlib-resources (>=3.0.0) ; python_version < "3.7"']
+        dist = makedist(tmp_path, install_requires=install_req)
+        dist = pyprojecttoml.apply_configuration(dist, pyproject)
+        assert "foo" in dist.extras_require
+        egg_info = dist.get_command_obj("egg_info")
+        write_requirements(egg_info, tmp_path, tmp_path / "requires.txt")
+        reqs = (tmp_path / "requires.txt").read_text(encoding="utf-8")
+        assert "importlib-resources" in reqs
+        assert "bar" in reqs
+        assert ':python_version < "3.7"' in reqs
+
+    @pytest.mark.parametrize(
+        ("field", "group"),
+        [("scripts", "console_scripts"), ("gui-scripts", "gui_scripts")],
+    )
+    @pytest.mark.filterwarnings("error")
+    def test_scripts_dont_require_dynamic_entry_points(self, tmp_path, field, group):
+        # Issue 3862
+        pyproject = self.pyproject(tmp_path, [field])
+        dist = makedist(tmp_path, entry_points={group: ["foobar=foobar:main"]})
+        dist = pyprojecttoml.apply_configuration(dist, pyproject)
+        assert group in dist.entry_points
+
+
+class TestMeta:
+    def test_example_file_in_sdist(self, setuptools_sdist):
+        """Meta test to ensure tests can run from sdist"""
+        with tarfile.open(setuptools_sdist) as tar:
+            assert any(name.endswith(EXAMPLES_FILE) for name in tar.getnames())
+
+
+class TestInteropCommandLineParsing:
+    def test_version(self, tmp_path, monkeypatch, capsys):
+        # See pypa/setuptools#4047
+        # This test can be removed once the CLI interface of setup.py is removed
+        monkeypatch.chdir(tmp_path)
+        toml_config = """
+        [project]
+        name = "test"
+        version = "42.0"
+        """
+        pyproject = Path(tmp_path, "pyproject.toml")
+        pyproject.write_text(cleandoc(toml_config), encoding="utf-8")
+        opts = {"script_args": ["--version"]}
+        dist = pyprojecttoml.apply_configuration(Distribution(opts), pyproject)
+        dist.parse_command_line()  # <-- there should be no exception here.
+        captured = capsys.readouterr()
+        assert "42.0" in captured.out
+
+
+class TestStaticConfig:
+    def test_mark_static_fields(self, tmp_path, monkeypatch):
+        monkeypatch.chdir(tmp_path)
+        toml_config = """
+        [project]
+        name = "test"
+        version = "42.0"
+        dependencies = ["hello"]
+        keywords = ["world"]
+        classifiers = ["private :: hello world"]
+        [tool.setuptools]
+        obsoletes = ["abcd"]
+        provides = ["abcd"]
+        platforms = ["abcd"]
+        """
+        pyproject = Path(tmp_path, "pyproject.toml")
+        pyproject.write_text(cleandoc(toml_config), encoding="utf-8")
+        dist = pyprojecttoml.apply_configuration(Distribution({}), pyproject)
+        assert is_static(dist.install_requires)
+        assert is_static(dist.metadata.keywords)
+        assert is_static(dist.metadata.classifiers)
+        assert is_static(dist.metadata.obsoletes)
+        assert is_static(dist.metadata.provides)
+        assert is_static(dist.metadata.platforms)
+
+
+# --- Auxiliary Functions ---
+
+
+def core_metadata(dist) -> str:
+    with io.StringIO() as buffer:
+        dist.metadata.write_pkg_file(buffer)
+        pkg_file_txt = buffer.getvalue()
+
+    # Make sure core metadata is valid
+    Metadata.from_email(pkg_file_txt, validate=True)  # can raise exceptions
+
+    skip_prefixes: tuple[str, ...] = ()
+    skip_lines = set()
+    # ---- DIFF NORMALISATION ----
+    # PEP 621 is very particular about author/maintainer metadata conversion, so skip
+    skip_prefixes += ("Author:", "Author-email:", "Maintainer:", "Maintainer-email:")
+    # May be redundant with Home-page
+    skip_prefixes += ("Project-URL: Homepage,", "Home-page:")
+    # May be missing in original (relying on default) but backfilled in the TOML
+    skip_prefixes += ("Description-Content-Type:",)
+    # Remove empty lines
+    skip_lines.add("")
+
+    result = []
+    for line in pkg_file_txt.splitlines():
+        if line.startswith(skip_prefixes) or line in skip_lines:
+            continue
+        result.append(line + "\n")
+
+    return "".join(result)