#94 Derive distribution options, without installing Setuptools endpoints
Closed: Fixed by bignose. Opened by mgorny.

The version 3.0.2 installs the following entry points:

[setuptools.finalize_distribution_options]
description_fields = util.packaging:derive_dist_description
maintainer = util.packaging:derive_maintainer
version = util.packaging:derive_version

However, the util package is not installed to the system (and shouldn't be). As a result, once python-daemon is installed, it is no longer possible to build anything using setuptools, as it is attempting to load the plugins:

# python3.12 -m build --no-iso
* Getting build dependencies for sdist...
Traceback (most recent call last):
  File "/usr/lib/python3.12/site-packages/pyproject_hooks/_in_process/_in_process.py", line 373, in <module>
    main()
  File "/usr/lib/python3.12/site-packages/pyproject_hooks/_in_process/_in_process.py", line 357, in main
    json_out["return_val"] = hook(**hook_input["kwargs"])
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/pyproject_hooks/_in_process/_in_process.py", line 308, in get_requires_for_build_sdist
    return hook(config_settings)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/setuptools/build_meta.py", line 330, in get_requires_for_build_sdist
    return self._get_build_requires(config_settings, requirements=[])
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/setuptools/build_meta.py", line 297, in _get_build_requires
    self.run_setup()
  File "/usr/lib/python3.12/site-packages/setuptools/build_meta.py", line 313, in run_setup
    exec(code, locals())
  File "<string>", line 1, in <module>
  File "/usr/lib/python3.12/site-packages/setuptools/__init__.py", line 108, in setup
    return distutils.core.setup(**attrs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/setuptools/_distutils/core.py", line 146, in setup
    _setup_distribution = dist = klass(attrs)
                                 ^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/setuptools/dist.py", line 289, in __init__
    _Distribution.__init__(self, dist_attrs)
  File "/usr/lib/python3.12/site-packages/setuptools/_distutils/dist.py", line 286, in __init__
    self.finalize_options()
  File "/usr/lib/python3.12/site-packages/setuptools/dist.py", line 640, in finalize_options
    for ep in sorted(loaded, key=by_order):
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/setuptools/dist.py", line 639, in <lambda>
    loaded = map(lambda e: e.load(), filtered)
                           ^^^^^^^^
  File "/usr/lib/python3.12/importlib/metadata/__init__.py", line 205, in load
    module = import_module(match.group('module'))
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1310, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1324, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'util'
ERROR Backend subprocess exited when trying to invoke get_requires_for_build_sdist

Please yank the release because it completely breaks the environment after upgrading, and it is non-trivial to figure out what the cause is.


I can confirm that this breaks a lot of things. python-daemon is a dependency of ansible-runner, and when building Ansible Execution Environments, ansible-builder installs ansible-runner without allowing to specify any constraints, so basically every Ansible Execution Environment build gets python-daemon 3.0.2. And then once pip installs something that uses setup.py / setup.cfg, this bug stops the build. (Which in my case is only making CI red and not breaking anything in production: https://github.com/ansible-collections/community.routeros/actions/runs/10121136568)

Yanking 3.0.2 or fixing this ASAP would be very much appreciated :)

As a result, once python-daemon is installed, it is no longer possible to build anything using setuptools, as it is attempting to load the plugins

:cry: "Stop Setuptools from ever becoming so weird and fragile" is getting higher on my list of important uses for a time machine.

I have now yanked from PyPI the ‘python-daemon’ version “3.0.2” release.

I will keep this bug report open as we figure out a way to achieve the goals of these plug-ins, without breaking everyone's Setuptools.

Metadata Update from @bignose:
- Issue assigned to bignose
- Issue tagged with: confirmed, help-wanted

I have discussed ways to achieve the "derive distribution options at build time" goal, at discuss.python.org: Dynamic metadata populated by entry-points

but the implementation I came up with got us into this mess. I'll appreciate help to move to a better implementation that meets my goals for these functions.

Hi there, I would recommend moving all static metadata into pyproject.toml and let setup.py contain just the dynamic parts (which is also what the setuptools maintainer recommended here)

It would look like this:
https://github.com/branchvincent/python-daemon/commit/23833185b9d0cd307b13d9ce5a774113cdc0bd53. Feel free to take as much or as little as you want from that diff or lmk if anything is unclear and I'm happy to explain in further detail (I would have made a pull request but for some reason pagure is rejecting my git push attempts)

Thank you @branchv. It's unfortunate you can't get your changes into Pagure for a merge request here? I hope you can get some success with that.

I would recommend moving all static metadata into pyproject.toml and let setup.py contain just the dynamic parts

Yes, that is the intention of the branch wip/maintenance/pyproject, addressing the requirements in this issue.

lmk if anything is unclear and I'm happy to explain in further detail

I see that your change implements many of the changes in the branch mentioned above; this is a good sign we're pulling in the same direction.

Unfortunately, I see you have modified some of the functions that deliberately get the ChangeLog file from the Distribution object, changing them to instead expect a static filesystem path for the ChangeLog file.

So, that doesn't satisfy the need to get this information from the Distribution object.

The changes you've implemented, expect the ChangeLog to be at a known static filesystem path. But the ChangeLog location is, at run time, only to be known by interrogating the Distribution object, because that's the object holding information for the specific environment where this distribution is installed.

Admittedly, that (getting this interrogation to work both at build time and at run time) is one aspect holding up implementing a fix for this issue.

I see that your change implements many of the changes in the branch mentioned above; this is a good sign we're pulling in the same direction.

Ah that's great, agreed!

So, that doesn't satisfy the need to get this information from the Distribution object.

I see, can you clarify this need and the motivation behind it? By runtime, do you mean at runtime for your end users or just during development? From my initial look at this issue, I must have misread the question as "How can I tell setuptools at build time about my dynamically-derived distribution metadata".

The changes you've implemented, expect the ChangeLog to be at a known static filesystem path. But the ChangeLog location is, at run time, only to be known by interrogating the Distribution object

Yes, the expectation is that your sdist and wheel are immutable after they are built and uploaded to PyPI. An sdist may have dynamic metadata but once built into a wheel and installed, the fully resolved metadata is written to disk in a METADATA file. For example, for the yanked release:

$ uv venv && uv pip install --no-binary=python-daemon python-daemon==3.0.2
$ grep ^Version .venv/lib/python3.*/site-packages/python_daemon-3.0.2.dist-info/METADATA
Version: 3.0.2
$ cat .venv/lib/python3.*/site-packages/python_daemon-3.0.2.dist-info/RECORD
daemon/__init__.py,sha256=IiCxBMbNH3knrtWxmazfZUtrVwl6L2qnbWtYbdkEzsw,1665
daemon/_metadata.py,sha256=_IvsY6S8_tMD8l5Smffwr__LfMvA5vScgQbZgtdQh2I,1037
daemon/daemon.py,sha256=t9qiNXgrKI1Pm2LE0yTPI57YP4MGNaqTiqbWBOim3Bw,35324
daemon/pidfile.py,sha256=ab9Ja4x2gEVWLGx82sCcJ18GSik-lHWmSguXduJixLg,2186
python_daemon-3.0.2.dist-info/COPYING,sha256=cSNJE6wsokJsnzOFJMxnKyH7aiIAmRYG18_yWT-og98,1657
python_daemon-3.0.2.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
python_daemon-3.0.2.dist-info/LICENSE.ASF-2,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
python_daemon-3.0.2.dist-info/LICENSE.GPL-3,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
python_daemon-3.0.2.dist-info/METADATA,sha256=w2jzuEXXSNQyvN91AmeRYSgjJnbTJzSnmrVLnlcn6mw,3131
python_daemon-3.0.2.dist-info/RECORD,,
python_daemon-3.0.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
python_daemon-3.0.2.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
python_daemon-3.0.2.dist-info/entry_points.txt,sha256=im5-ox-L06PgBTwRZV6eNQtH9EQbmScaMUg9FXUf7cA,189
python_daemon-3.0.2.dist-info/top_level.txt,sha256=7tSe9oVFyd_dCLrotHDHVIQ6OAM26I6rF6ggOsDL0uk,7

So although your sdist intended for the version to be dynamic at runtime, it becomes resolved once installed (you can see the same is true for the description and maintainer fields here). Also note that the Changelog file is no longer present at runtime and has no need to be. But perhaps I'm missing something here.

@branchv wrote:

By ["getting this interrogation to work both at build time and at run time"], do you mean at runtime for your end users or just during development?

Yes, at build time and at run time. The intention is:

  • Some package metadata is canonically stored in the Change Log.
  • This means any update to that information in the Change Log should be automatically reflected when interrogating the package for that metadata.
  • Therefore the interface to get those metadata should work whether at build time (examining the source when building) or at run time (examining the distribution already installed).

Yes, the expectation is that your sdist and wheel are immutable after they are built and uploaded to PyPI.

Agreed.

To my understanding, the supported interface to interrogate the installed package from within a Python session, is to interrogate the Distribution object (of type setuptools.dist.Distribution) and read from there.

So although your sdist intended for the version to be dynamic at runtime

The intention is to interrogate the package to discover its metadata, for those specific fields that are canonically recorded in ChangeLog. That is, the pyproject.toml is not the canonical record of this information, because that would be superfluous with the ChangeLog interface and risk one or the other being out of date.

To my understanding, the way to make this happen is to tell the build system this is a "dynamic" metadata, because it's determined by an API call.

With the migration to 'changelog-chug' for interrogating the Change Log at build time, merged to the 'main' branch, this issue is now resolved.

Metadata Update from @bignose:
- Issue close_status updated to: Fixed
- Issue status updated to: Closed (was: Open)

Metadata Update from @bignose:
- Issue untagged with: help-wanted

Metadata