Repository: pypa/setuptools

Issue:
no way to support setuptools plugin option in setup.cfg metadata
while experimenting with setuptools_scm metadata i discovered that there doesn't seem to be a way to add setup.cfg metadata support to setuptools plugins


Discussion:
I think I generally get what you mean, but @RonnyPfannschmidt can you provide a minimal example of what you have in mind?
@pganssle setuptools_scm has a mode of operation where a mapping with different keys and types is passed as a argument to the setup call, currently its impossible to transfer that

see https://github.com/pypa/setuptools_scm for examples of invocations

i haven't yet made up a minimal example, but something similar to `find:` might help
Hm.. Would it make sense to put it under a tool heading, like so?

```ini
[tool:setuptools_scm]
use_scm_version=True
```

I'm not clear on whether it's possible to achieve your aims already by just having `setuptools_scm` parse the config file and extract its sub-heading (`pytest` already does this, for example, though it's mostly unrelated to setuptools).
@pganssle that would be a failure - setuptools provides a extension mechanism for setup calls, but denies it for setup.cfg

also if a custom config parser was needed, then the only sane option those days would be pyproject.toml

but even then setuptools should provide the config content in a parsed form
@RonnyPfannschmidt Well, plugins can put their configuration wherever they want, but there's standard library support for `setup.cfg`, but not for `pyproject.toml`, and you're asking for more stuff to go into `setup.cfg` in the first place...

In any case, I'm just saying there's already a well-established place for tools to put their configuration in `setup.cfg`, and usually when options are added to `setup.cfg`, they go under a heading *not* present in `setup.py` - note that `setup()` doesn't look like this:

```python
setup(metadata=dict(
    name='mypkg'
    version='0.0.1'))
```

The fact that `setup()` takes a bunch of loose keys for the configuration doesn't mean that `setup.cfg` can't expect those keys to be namespaced or categorized. To me the natural place to namespace such keys would be under tool-specific configuration headers.

Now, I don't know how `pytest` implements this `[tool:pytest]` parser. If it's super inconvenient it makes sense for `setuptools` to provide some hooks for it, but I think that that's the rough shape that the solution should take, and it's *already available today*.
> setuptools provides a extension mechanism for setup calls

Right - that's the `distutils.setup_keywords` entry point, that allows arbitrary third-party packages to solicit parameters when `setup()` is called and invoke hooks in those packages to supply behavior for those keywords. `setuptools_scm` defines `use_scm_version` as a distutils setup_keyword.
What setuptools_scm needs is not a place to define configuration, but a way for declarative config to trigger the hook that would import setuptools_scm and allow it to customize the Distribution.
@jaraco Ah, these things are registered at install-time? For some reason I thought you manually imported your plugins in the install script.

That changes things pretty significantly, and I'm much closer to agreeing that this should be done. Maybe something like this:

```ini
[setup_plugins]
use_scm_version=True
```

I'm pretty much 90% sold on this. The only reservation I have is that it makes it so that the "declarative" configuration file is basically invoking arbitrary scripts, which sorta moves the needle away from it being declarative. Still, I think there's a case to be made that, used sanely, this is no different from having more features in `setuptools` itself, but allows for those features to be broken out into extensions to allow those features to be "opt in".
@pganssle note that pytests `[tool:pytest]` is fundamentally broken and scheduled for removal as the default parser for `setup.cfg` and pytests own config parser fundamentally differ

i'm asking for a mechanism similar to the `distutils.setup_keywords` mechanism - it should be some kind of `distutils.config_section`, `setuptools.metadata.key` and `setuptools.options.key` to allow extensions to get their own data in shape

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -89,6 +89,13 @@ def pypi_link(pkg_filename):
             "%(cmd)s = setuptools.command.%(cmd)s:%(cmd)s" % locals()
             for cmd in read_commands()
         ],
+        "setuptools.finalize_distribution_options": [
+            "parent_finalize = setuptools.dist:_Distribution.finalize_options",
+            "features = setuptools.dist:Distribution._finalize_feature_opts",
+            "keywords = setuptools.dist:Distribution._finalize_setup_keywords",
+            "2to3_doctests = "
+            "setuptools.dist:Distribution._finalize_2to3_doctests",
+        ],
         "distutils.setup_keywords": [
             "eager_resources        = setuptools.dist:assert_string_list",
             "namespace_packages     = setuptools.dist:check_nsp",
diff --git a/setuptools/dist.py b/setuptools/dist.py
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -724,15 +724,28 @@ def fetch_build_eggs(self, requires):
         return resolved_dists
 
     def finalize_options(self):
-        _Distribution.finalize_options(self)
-        if self.features:
-            self._set_global_opts_from_features()
+        """
+        Allow plugins to apply arbitrary operations to the
+        distribution. Each hook may optionally define a 'order'
+        to influence the order of execution. Smaller numbers
+        go first and the default is 0.
+        """
+        hook_key = 'setuptools.finalize_distribution_options'
+
+        def by_order(hook):
+            return getattr(hook, 'order', 0)
+        eps = pkg_resources.iter_entry_points(hook_key)
+        for ep in sorted(eps, key=by_order):
+            ep.load()(self)
 
+    def _finalize_setup_keywords(self):
         for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'):
             value = getattr(self, ep.name, None)
             if value is not None:
                 ep.require(installer=self.fetch_build_egg)
                 ep.load()(self, ep.name, value)
+
+    def _finalize_2to3_doctests(self):
         if getattr(self, 'convert_2to3_doctests', None):
             # XXX may convert to set here when we can rely on set being builtin
             self.convert_2to3_doctests = [
@@ -786,9 +799,12 @@ def fetch_build_egg(self, req):
         cmd.ensure_finalized()
         return cmd.easy_install(req)
 
-    def _set_global_opts_from_features(self):
+    def _finalize_feature_opts(self):
         """Add --with-X/--without-X options based on optional features"""
 
+        if not self.features:
+            return
+
         go = []
         no = self.negative_opt.copy()
 


Test patch: