Search code examples
pythoncross-platformsetup.pypython-wheel

How do you get the filename of a Python wheel when running setup.py?


I have a build process that creates a Python wheel using the following command:

python setup.py bdist_wheel

The build process can be run on many platforms (Windows, Linux, py2, py3 etc.) and I'd like to keep the default output names (e.g. mapscript-7.2-cp27-cp27m-win_amd64.whl) to upload to PyPI.

Is there anyway to get the generated wheel's filename (e.g. mapscript-7.2-cp27-cp27m-win_amd64.whl) and save to a variable so I can then install the wheel later on in the script for testing?

Ideally the solution would be cross platform. My current approach is to try and clear the folder, list all files and select the first (and only) file in the list, however this seems a very hacky solution.


Solution

  • setuptools

    If you are using a setup.py script to build the wheel distribution, you can use the bdist_wheel command to query the wheel file name. The drawback of this method is that it uses bdist_wheel's private API, so the code may break on wheel package update if the authors decide to change it.

    from setuptools.dist import Distribution
    
    
    def wheel_name(**kwargs):
        # create a fake distribution from arguments
        dist = Distribution(attrs=kwargs)
        # finalize bdist_wheel command
        bdist_wheel_cmd = dist.get_command_obj('bdist_wheel')
        bdist_wheel_cmd.ensure_finalized()
        # assemble wheel file name
        distname = bdist_wheel_cmd.wheel_dist_name
        tag = '-'.join(bdist_wheel_cmd.get_tag())
        return f'{distname}-{tag}.whl'
    

    The wheel_name function accepts the same arguments you pass to the setup() function. Example usage:

    >>> wheel_name(name="mydist", version="1.2.3")
    mydist-1.2.3-py3-none-any.whl
    >>> wheel_name(name="mydist", version="1.2.3", ext_modules=[Extension("mylib", ["mysrc.pyx", "native.c"])])
    mydist-1.2.3-cp36-cp36m-linux_x86_64.whl
    

    Notice that the source files for native libs (mysrc.pyx or native.c in the above example) don't have to exist to assemble the wheel name. This is helpful in case the sources for the native lib don't exist yet (e.g. you are generating them later via SWIG, Cython or whatever).

    This makes the wheel_name easily reusable in the setup.py script where you define the distribution metadata:

    # setup.py
    from setuptools import setup, find_packages, Extension
    from setup_helpers import wheel_name
    
    setup_kwargs = dict(
        name='mydist',
        version='1.2.3',
        packages=find_packages(),
        ext_modules=[Extension(...), ...],
        ...
    )
    file = wheel_name(**setup_kwargs)
    ...
    setup(**setup_kwargs)
    

    If you want to use it outside of the setup script, you have to organize the access to setup() args yourself (e.g. reading them from a setup.cfg script or whatever).

    This part is loosely based on my other answer to setuptools, know in advance the wheel filename of a native library

    poetry

    Things can be simplified a lot (it's practically a one-liner) if you use poetry because all the relevant metadata is stored in the pyproject.toml. Again, this uses an undocumented API:

    from clikit.io import NullIO
    
    from poetry.factory import Factory
    from poetry.masonry.builders.wheel import WheelBuilder
    from poetry.utils.env import NullEnv
    
    
    def wheel_name(rootdir='.'):
        builder = WheelBuilder(Factory().create_poetry(rootdir), NullEnv(), NullIO())
        return builder.wheel_filename
    

    The rootdir argument is the directory containing your pyproject.toml script.

    flit

    AFAIK flit can't build wheels with native extensions, so it can give you only the purelib name. Nevertheless, it may be useful if your project uses flit for distribution building. Notice this also uses an undocumented API:

    from flit_core.wheel import WheelBuilder
    from io import BytesIO
    from pathlib import Path
    
    
    def wheel_name(rootdir='.'):
        config = str(Path(rootdir, 'pyproject.toml'))
        builder = WheelBuilder.from_ini_path(config, BytesIO())
        return builder.wheel_filename
    

    Implementing your own solution

    I'm not sure whether it's worth it. Still, if you want to choose this path, consider using packaging.tags before you find some old deprecated stuff or even decide to query the platform yourself. You will still have to fall back to private stuff to assemble the correct wheel name, though.