Repository: pypa/setuptools

Issue:
[BUG] PEP621: Warnings/errors when handling a `long_description` (`readme`) dynamically supplied from within `setup.py`
### setuptools version

setuptools===61.3.1

### Python version

Python 3.9.12

### OS

Debian Linux bullseye (Debian 11)

### Additional environment information

_No response_

### Description

This is downstream of #1688 & the recent experimental PEP621 implementation.

For my Python projects on PyPI, I use a `setuptools` packaging workflow where I dynamically modify the contents of `README.md` (alternatively `README.rst`) as part of `setup.py`, before passing those contents into the `long_description` argument to `setup()`. One example of this is [here](https://github.com/bskinn/sphobjinv/blob/505c7afc656f20b9e105f4ead9c6eb570eef971a/setup.py#L14-L40), where I'm changing some documentation links -- as README sits in the repo, they point to the `latest` docs; when packaged for PyPI, though, I want the links point to a specific historical `vx.y` docs version.

I'm attempting to convert this workflow to PEP621. In doing so, it appears that it's currently impossible to configure `setuptools` to properly accept this state of affairs.

1. If I don't declare `readme` in `project.dynamic`, I get a `_WouldIgnoreField` warning.
2. If I *do* declare `readme` in `project.dynamic`, I get a `KeyError` from an attempted access to `dynamic_cfg["readme"]`
3. If I attempt to declare `project.dynamic = ["version", "long_description"]`, instead of `["version", "readme"]`, I get `ValueError: invalid pyproject.toml config: project.dynamic[1]`


### Expected behavior

I would expect it to be necessary to declare my `long_description` as `dynamic`, since I am generating it dynamically within `setup.py`. Thus, (1.) above (and the future behavior of completely rejecting a dynamic `readme` when it is not declared as `dynamic`) is expected behavior.

I would expect (2.) to result in a successful build. I would guess that the PEP621 `project.readme` field, and in particular the `readme` element within `dynamic_cfg`, is not being correctly linked to/populated with the contents of `long_description` coming from `setup.py`.

Given that the `pyproject.toml` table key for `project.dynamic` expects to see `readme` and not `long_description`, the error in (3.) is expected.

### How to Reproduce

**Setup**
1. Clone and enter the demonstration repo at https://github.com/bskinn/setuptools-pep621-readme.
2. Create a virtual environment for the repo and `pip install build`; or, just use an externally-installed version of `build` (e.g., via `pipx`).

**Repro**
1. `$ git checkout readme-static && python -m build`
    - `readme` is ***not*** included in `project.dynamic`; (1.) above
2. `$ git checkout readme-dynamic && python -m build`
    - `readme` **is** included in `project.dynamic`; (2.) above
3. `$ git checkout long-desc-in-dynamic && python -m build`
    - `long_description` is included in `project.dynamic`, `readme` is omitted; (3.) above

### Output

```console

# skipping setup steps since they should be trivial


# Repro Step (1.)
$ git checkout readme-static && pm build
HEAD is now at 127ba90 Initial commit

* Creating venv isolated environment...
* Installing packages in isolated environment... (setuptools>=61, trove-classifiers, wheel)
* Getting dependencies for sdist...
/tmp/build-env-lntkqmmu/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py:102: _ExperimentalProjectMetadata: Support for project metadata in `pyproject.toml` is still experimental and may be removed (or change) in future releases.
  warnings.warn(msg, _ExperimentalProjectMetadata)
/tmp/build-env-lntkqmmu/lib/python3.9/site-packages/setuptools/config/_apply_pyprojecttoml.py:93: _WouldIgnoreField: !!


    ##########################################################################
    # configuration would be ignored/result in error due to `pyproject.toml` #
    ##########################################################################

    The following seems to be defined outside of `pyproject.toml`:

    `readme = 'This is a test readme.'`

    According to the spec (see the link bellow), however, setuptools CANNOT
    consider this value unless 'readme' is listed as `dynamic`.

    https://packaging.python.org/en/latest/specifications/declaring-project-metadata/

    For the time being, `setuptools` will still consider the given value (as a
    **transitional** measure), but please note that future releases of setuptools will
    follow strictly the standard.

    To prevent this warning, you can list 'readme' under `dynamic` or alternatively
    remove the `[project]` table from your file and rely entirely on other means of
    configuration.
    

!!
    
  warnings.warn(msg, _WouldIgnoreField)
running egg_info
writing setuptools_pep621_readme.egg-info/PKG-INFO
writing dependency_links to setuptools_pep621_readme.egg-info/dependency_links.txt
writing top-level names to setuptools_pep621_readme.egg-info/top_level.txt
reading manifest file 'setuptools_pep621_readme.egg-info/SOURCES.txt'
adding license file 'LICENSE.txt'
writing manifest file 'setuptools_pep621_readme.egg-info/SOURCES.txt'
* Building sdist...
/tmp/build-env-lntkqmmu/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py:102: _ExperimentalProjectMetadata: Support for project metadata in `pyproject.toml` is still experimental and may be removed (or change) in future releases.
  warnings.warn(msg, _ExperimentalProjectMetadata)
/tmp/build-env-lntkqmmu/lib/python3.9/site-packages/setuptools/config/_apply_pyprojecttoml.py:93: _WouldIgnoreField: !!


    ##########################################################################
    # configuration would be ignored/result in error due to `pyproject.toml` #
    ##########################################################################

    The following seems to be defined outside of `pyproject.toml`:

    `readme = 'This is a test readme.'`

    According to the spec (see the link bellow), however, setuptools CANNOT
    consider this value unless 'readme' is listed as `dynamic`.

    https://packaging.python.org/en/latest/specifications/declaring-project-metadata/

    For the time being, `setuptools` will still consider the given value (as a
    **transitional** measure), but please note that future releases of setuptools will
    follow strictly the standard.

    To prevent this warning, you can list 'readme' under `dynamic` or alternatively
    remove the `[project]` table from your file and rely entirely on other means of
    configuration.
    

!!
    
  warnings.warn(msg, _WouldIgnoreField)
running sdist
running egg_info
writing setuptools_pep621_readme.egg-info/PKG-INFO
writing dependency_links to setuptools_pep621_readme.egg-info/dependency_links.txt
writing top-level names to setuptools_pep621_readme.egg-info/top_level.txt
reading manifest file 'setuptools_pep621_readme.egg-info/SOURCES.txt'
adding license file 'LICENSE.txt'
writing manifest file 'setuptools_pep621_readme.egg-info/SOURCES.txt'
running check
creating setuptools-pep621-readme-0.1.dev1
creating setuptools-pep621-readme-0.1.dev1/setuptools_pep621_readme.egg-info
copying files to setuptools-pep621-readme-0.1.dev1...
copying LICENSE.txt -> setuptools-pep621-readme-0.1.dev1
copying README.md -> setuptools-pep621-readme-0.1.dev1
copying pyproject.toml -> setuptools-pep621-readme-0.1.dev1
copying setup.py -> setuptools-pep621-readme-0.1.dev1
copying setuptools_pep621_readme.egg-info/PKG-INFO -> setuptools-pep621-readme-0.1.dev1/setuptools_pep621_readme.egg-info
copying setuptools_pep621_readme.egg-info/SOURCES.txt -> setuptools-pep621-readme-0.1.dev1/setuptools_pep621_readme.egg-info
copying setuptools_pep621_readme.egg-info/dependency_links.txt -> setuptools-pep621-readme-0.1.dev1/setuptools_pep621_readme.egg-info
copying setuptools_pep621_readme.egg-info/top_level.txt -> setuptools-pep621-readme-0.1.dev1/setuptools_pep621_readme.egg-info
Writing setuptools-pep621-readme-0.1.dev1/setup.cfg
Creating tar archive
removing 'setuptools-pep621-readme-0.1.dev1' (and everything under it)
* Building wheel from sdist
* Creating venv isolated environment...
* Installing packages in isolated environment... (setuptools>=61, trove-classifiers, wheel)
* Getting dependencies for wheel...
/tmp/build-env-n_v08h44/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py:102: _ExperimentalProjectMetadata: Support for project metadata in `pyproject.toml` is still experimental and may be removed (or change) in future releases.
  warnings.warn(msg, _ExperimentalProjectMetadata)
/tmp/build-env-n_v08h44/lib/python3.9/site-packages/setuptools/config/_apply_pyprojecttoml.py:93: _WouldIgnoreField: !!


    ##########################################################################
    # configuration would be ignored/result in error due to `pyproject.toml` #
    ##########################################################################

    The following seems to be defined outside of `pyproject.toml`:

    `readme = 'This is a test readme.'`

    According to the spec (see the link bellow), however, setuptools CANNOT
    consider this value unless 'readme' is listed as `dynamic`.

    https://packaging.python.org/en/latest/specifications/declaring-project-metadata/

    For the time being, `setuptools` will still consider the given value (as a
    **transitional** measure), but please note that future releases of setuptools will
    follow strictly the standard.

    To prevent this warning, you can list 'readme' under `dynamic` or alternatively
    remove the `[project]` table from your file and rely entirely on other means of
    configuration.
    

!!
    
  warnings.warn(msg, _WouldIgnoreField)
running egg_info
writing setuptools_pep621_readme.egg-info/PKG-INFO
writing dependency_links to setuptools_pep621_readme.egg-info/dependency_links.txt
writing top-level names to setuptools_pep621_readme.egg-info/top_level.txt
reading manifest file 'setuptools_pep621_readme.egg-info/SOURCES.txt'
adding license file 'LICENSE.txt'
writing manifest file 'setuptools_pep621_readme.egg-info/SOURCES.txt'
* Installing packages in isolated environment... (wheel)
* Building wheel...
/tmp/build-env-n_v08h44/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py:102: _ExperimentalProjectMetadata: Support for project metadata in `pyproject.toml` is still experimental and may be removed (or change) in future releases.
  warnings.warn(msg, _ExperimentalProjectMetadata)
/tmp/build-env-n_v08h44/lib/python3.9/site-packages/setuptools/config/_apply_pyprojecttoml.py:93: _WouldIgnoreField: !!


    ##########################################################################
    # configuration would be ignored/result in error due to `pyproject.toml` #
    ##########################################################################

    The following seems to be defined outside of `pyproject.toml`:

    `readme = 'This is a test readme.'`

    According to the spec (see the link bellow), however, setuptools CANNOT
    consider this value unless 'readme' is listed as `dynamic`.

    https://packaging.python.org/en/latest/specifications/declaring-project-metadata/

    For the time being, `setuptools` will still consider the given value (as a
    **transitional** measure), but please note that future releases of setuptools will
    follow strictly the standard.

    To prevent this warning, you can list 'readme' under `dynamic` or alternatively
    remove the `[project]` table from your file and rely entirely on other means of
    configuration.
    

!!
    
  warnings.warn(msg, _WouldIgnoreField)
running bdist_wheel
running build
installing to build/bdist.linux-x86_64/wheel
running install
running install_egg_info
running egg_info
writing setuptools_pep621_readme.egg-info/PKG-INFO
writing dependency_links to setuptools_pep621_readme.egg-info/dependency_links.txt
writing top-level names to setuptools_pep621_readme.egg-info/top_level.txt
reading manifest file 'setuptools_pep621_readme.egg-info/SOURCES.txt'
adding license file 'LICENSE.txt'
writing manifest file 'setuptools_pep621_readme.egg-info/SOURCES.txt'
Copying setuptools_pep621_readme.egg-info to build/bdist.linux-x86_64/wheel/setuptools_pep621_readme-0.1.dev1-py3.9.egg-info
running install_scripts
adding license file "LICENSE.txt" (matched pattern "LICEN[CS]E*")
creating build/bdist.linux-x86_64/wheel/setuptools_pep621_readme-0.1.dev1.dist-info/WHEEL
creating '/.../setuptools-pep621-readme/dist/tmp8qw1jvw5/setuptools_pep621_readme-0.1.dev1-py3-none-any.whl' and adding 'build/bdist.linux-x86_64/wheel' to it
adding 'setuptools_pep621_readme-0.1.dev1.dist-info/LICENSE.txt'
adding 'setuptools_pep621_readme-0.1.dev1.dist-info/METADATA'
adding 'setuptools_pep621_readme-0.1.dev1.dist-info/WHEEL'
adding 'setuptools_pep621_readme-0.1.dev1.dist-info/top_level.txt'
adding 'setuptools_pep621_readme-0.1.dev1.dist-info/RECORD'
removing build/bdist.linux-x86_64/wheel
Successfully built setuptools-pep621-readme-0.1.dev1.tar.gz and setuptools_pep621_readme-0.1.dev1-py3-none-any.whl



# Repro Step (2.)
$ git checkout readme-dynamic && python -m build
Previous HEAD position was 127ba90 Initial commit
HEAD is now at 84b342f Change to dynamic `readme`

HEAD is now at 84b342f Change to dynamic `readme`
* Creating venv isolated environment...
* Installing packages in isolated environment... (setuptools>=61, trove-classifiers, wheel)
* Getting dependencies for sdist...
/tmp/build-env-uvtxu5az/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py:102: _ExperimentalProjectMetadata: Support for project metadata in `pyproject.toml` is still experimental and may be removed (or change) in future releases.
  warnings.warn(msg, _ExperimentalProjectMetadata)
Traceback (most recent call last):
  File "/.../setuptools-pep621-readme/env/lib/python3.9/site-packages/pep517/in_process/_in_process.py", line 363, in <module>
    main()
  File "/.../setuptools-pep621-readme/env/lib/python3.9/site-packages/pep517/in_process/_in_process.py", line 345, in main
    json_out['return_val'] = hook(**hook_input['kwargs'])
  File "/.../setuptools-pep621-readme/env/lib/python3.9/site-packages/pep517/in_process/_in_process.py", line 297, in get_requires_for_build_sdist
    return hook(config_settings)
  File "/tmp/build-env-uvtxu5az/lib/python3.9/site-packages/setuptools/build_meta.py", line 181, in get_requires_for_build_sdist
    return self._get_build_requires(config_settings, requirements=[])
  File "/tmp/build-env-uvtxu5az/lib/python3.9/site-packages/setuptools/build_meta.py", line 159, in _get_build_requires
    self.run_setup()
  File "/tmp/build-env-uvtxu5az/lib/python3.9/site-packages/setuptools/build_meta.py", line 174, in run_setup
    exec(compile(code, __file__, 'exec'), locals())
  File "setup.py", line 12, in <module>
    setup(
  File "/tmp/build-env-uvtxu5az/lib/python3.9/site-packages/setuptools/__init__.py", line 87, in setup
    return distutils.core.setup(**attrs)
  File "/tmp/build-env-uvtxu5az/lib/python3.9/site-packages/setuptools/_distutils/core.py", line 122, in setup
    dist.parse_config_files()
  File "/tmp/build-env-uvtxu5az/lib/python3.9/site-packages/setuptools/dist.py", line 854, in parse_config_files
    pyprojecttoml.apply_configuration(self, filename, ignore_option_errors)
  File "/tmp/build-env-uvtxu5az/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py", line 54, in apply_configuration
    config = read_configuration(filepath, True, ignore_option_errors, dist)
  File "/tmp/build-env-uvtxu5az/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py", line 134, in read_configuration
    return expand_configuration(asdict, root_dir, ignore_option_errors, dist)
  File "/tmp/build-env-uvtxu5az/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py", line 189, in expand_configuration
    return _ConfigExpander(config, root_dir, ignore_option_errors, dist).expand()
  File "/tmp/build-env-uvtxu5az/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py", line 236, in expand
    self._expand_all_dynamic(dist, package_dir)
  File "/tmp/build-env-uvtxu5az/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py", line 279, in _expand_all_dynamic
    readme=self._obtain_readme(dist),
  File "/tmp/build-env-uvtxu5az/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py", line 323, in _obtain_readme
    "content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst"),
KeyError: 'readme'



# Repro Step (3.)
$ git checkout long-desc-in-dynamic && python -m build
Previous HEAD position was 127ba90 Initial commit
HEAD is now at c5b9d79 Switch to `long_description` in `dynamic`
* Creating venv isolated environment...
* Installing packages in isolated environment... (setuptools>=61, trove-classifiers, wheel)
* Getting dependencies for sdist...
/tmp/build-env-yhivke4e/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py:102: _ExperimentalProjectMetadata: Support for project metadata in `pyproject.toml` is still experimental and may be removed (or change) in future releases.
  warnings.warn(msg, _ExperimentalProjectMetadata)
configuration error: `project.dynamic[1]` must be one of ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']
GIVEN VALUE:
    "long_description"

OFFENDING RULE: 'enum'

DEFINITION:
    {
        "enum": [
            "version",
            "description",
            "readme",
            "requires-python",
            "license",
            "authors",
            "maintainers",
            "keywords",
            "classifiers",
            "urls",
            "scripts",
            "gui-scripts",
            "entry-points",
            "dependencies",
            "optional-dependencies"
        ]
    }
Traceback (most recent call last):
  File "/.../setuptools-pep621-readme/env/lib/python3.9/site-packages/pep517/in_process/_in_process.py", line 363, in <module>
    main()
  File "/.../setuptools-pep621-readme/env/lib/python3.9/site-packages/pep517/in_process/_in_process.py", line 345, in main
    json_out['return_val'] = hook(**hook_input['kwargs'])
  File "/.../setuptools-pep621-readme/env/lib/python3.9/site-packages/pep517/in_process/_in_process.py", line 297, in get_requires_for_build_sdist
    return hook(config_settings)
  File "/tmp/build-env-yhivke4e/lib/python3.9/site-packages/setuptools/build_meta.py", line 181, in get_requires_for_build_sdist
    return self._get_build_requires(config_settings, requirements=[])
  File "/tmp/build-env-yhivke4e/lib/python3.9/site-packages/setuptools/build_meta.py", line 159, in _get_build_requires
    self.run_setup()
  File "/tmp/build-env-yhivke4e/lib/python3.9/site-packages/setuptools/build_meta.py", line 174, in run_setup
    exec(compile(code, __file__, 'exec'), locals())
  File "setup.py", line 12, in <module>
    setup(
  File "/tmp/build-env-yhivke4e/lib/python3.9/site-packages/setuptools/__init__.py", line 87, in setup
    return distutils.core.setup(**attrs)
  File "/tmp/build-env-yhivke4e/lib/python3.9/site-packages/setuptools/_distutils/core.py", line 122, in setup
    dist.parse_config_files()
  File "/tmp/build-env-yhivke4e/lib/python3.9/site-packages/setuptools/dist.py", line 854, in parse_config_files
    pyprojecttoml.apply_configuration(self, filename, ignore_option_errors)
  File "/tmp/build-env-yhivke4e/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py", line 54, in apply_configuration
    config = read_configuration(filepath, True, ignore_option_errors, dist)
  File "/tmp/build-env-yhivke4e/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py", line 120, in read_configuration
    validate(subset, filepath)
  File "/tmp/build-env-yhivke4e/lib/python3.9/site-packages/setuptools/config/pyprojecttoml.py", line 43, in validate
    raise error from None
ValueError: invalid pyproject.toml config: `project.dynamic[1]`

ERROR Backend subproccess exited when trying to invoke get_requires_for_build_sdist

```



Discussion:
cc @abravalheri 
Hi @bskinn, thank you very much for reporting this, the option in (2) should succeed. I think that is the compliant/preferred way forward. I will have a look on this (probably tomorrow evening). 

Resolving patch:
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -316,12 +316,17 @@ def _obtain_version(self, dist: "Distribution", package_dir: Mapping[str, str]):
         return None
 
     def _obtain_readme(self, dist: "Distribution") -> Optional[Dict[str, str]]:
-        if "readme" in self.dynamic:
-            dynamic_cfg = self.dynamic_cfg
+        if "readme" not in self.dynamic:
+            return None
+
+        dynamic_cfg = self.dynamic_cfg
+        if "readme" in dynamic_cfg:
             return {
                 "text": self._obtain(dist, "readme", {}),
                 "content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst"),
             }
+
+        self._ensure_previously_set(dist, "readme")
         return None
 
     def _obtain_entry_points(


Test patch:
diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -253,6 +253,20 @@ def test_dynamic_without_config(self, tmp_path):
         with pytest.raises(OptionError, match="No configuration .* .classifiers."):
             read_configuration(pyproject)
 
+    def test_dynamic_readme_from_setup_script_args(self, tmp_path):
+        config = """
+        [project]
+        name = "myproj"
+        version = '42'
+        dynamic = ["readme"]
+        """
+        pyproject = tmp_path / "pyproject.toml"
+        pyproject.write_text(cleandoc(config))
+        dist = Distribution(attrs={"long_description": "42"})
+        # No error should occur because of missing `readme`
+        dist = apply_configuration(dist, pyproject)
+        assert dist.metadata.long_description == "42"
+
     def test_dynamic_without_file(self, tmp_path):
         config = """
         [project]