Search code examples
pythonmetadatasetuptoolspypidistutils

How to specify requirements in Python packages metadata


Core metadata specification documents the metadata field Requires-External which seems to be for specifying system (non-python) dependencies.

How do you actually specify this field though? This is what I've tried:

.
├── mypackage
│   └── __init__.py
└── setup.py

Contents of setup.py

from setuptools import setup

setup(
    name="mypackage",
    description="blah blah",
    url='https://example.org',
    version="0.1",
    packages=["mypackage"],
    requires_external=[
        "C",
        "libpng (>=1.5)",
        'make; sys_platform != "win32"',
    ],
)

When I build this package, that metadata was not included

Metadata-Version: 2.1
Name: mypackage
Version: 0.1
Summary: blah blah
Home-page: https://example.org
License: UNKNOWN
Platform: UNKNOWN

UNKNOWN

So what is the syntax to pass Requires-External to setuptools/distutils? Note: this question is not asking about Requires-Dist metadata.


Solution

  • So what is the syntax to pass Requires-External to setuptools/distutils?

    There is none by default, as neither distutils nor setuptools support the field. Also, requires_external keyword arg is not supported as well - it is silently ignored, just as any other unknown keyword arg.

    To add local support for requires_external kwarg, you need to provide your own distribution implementation. Minimal example (requires Python 3.9):

    from setuptools.dist import Distribution as DistributionOrig
    
    class Distribution(DistributionOrig):
        _DISTUTILS_UNSUPPORTED_METADATA = DistributionOrig._DISTUTILS_UNSUPPORTED_METADATA | {'requires_external': dict}
    

    You can now pass requires_external to setup() when paired with distclass:

    setup(
        ...,
        requires_external=[
            'C',
            'libpng (>=1.5)',
            'make; sys_platform != "win32"',
        ],
        distclass=Distribution,
    )
    

    The next goal is to actually write the contents of requires_external to PKG-INFO. This is quite tricky to do via distribution metadata itself because setuptools patches the relevant methods (DistributionMetadata.read_pkg_file() and DistributionMetadata.write_pkg_file()) with own monolithic impls. The easiest way for this IMO is to modify PKG-INFO post factum via a custom egg_info impl:

    import email
    from pathlib import Path
    from setuptools.command.egg_info import egg_info as egg_info_orig
    
    
    class egg_info(egg_info_orig):
        def run(self):
            super().run()
            # PKG-INFO is now guaranteed to exist
            pkg_info_file = Path(self.egg_info, 'PKG-INFO')
            pkg_info = email.message_from_bytes(pkg_info_file.read_bytes())
            for req in self.distribution.metadata.requires_external:
                pkg_info.add_header('Requires-External', req)
            pkg_info_file.write_bytes(pkg_info.as_bytes())
    

    Pass your own egg_info impl via cmdclass argument in setup():

    setup(
        ...,
        requires_external=[
            'C',
            'libpng (>=1.5)',
            'make; sys_platform != "win32"',
        ],
        distclass=Distribution,
        cmdclass={'egg_info': egg_info},
    )