Replacement for ./setup.py --version?

Hi,

I’ve been reading Why you shouldn't invoke setup.py directly and I’m willing to replace all calls of setup.py (thanks to the very helpful table at the end of the article, it shouldn’t be too difficult) but I’m using setuptools-scm to define the version of my tool (rdiff-backup) and using hence ./setup.py --version to get the version of my tool (not of setup.py itself, obviously). I had a look at pyproject-build but didn’t see anything useful and my search skills failed on me. Or is this one of the few cases where calling setup.py is still correct?

I hate when this happens: writing down the question brought a new idea how to find a solution, and I found it… python -m setuptools_scm is the simple answer… Sorry for the noise, perhaps it’ll help someone else at least.

1 Like

There are no cases where invoking setup.py directly is still correct.

That is a good question. One that I have seen asked more than once. And the “correct” solution is not that simple I am afraid (see below):

In the case of setuptools-scm, there is indeed an easy way, python -m setuptools_scm:

Personally I think it is fine to use python setup.py --version, as long as you are fully aware that it is not supported, that it might break, and that you should really avoid it. But if there is no other solution, then maybe it is okay-ish:

Then there is of course the cases where the metadata (here the version string) is clearly specified in some easy to parse file such as pyproject.toml or setup.cfg. In such cases it should be straightfoward to extract the value with tomllib or configparser


For an “over-kill” solution that allows getting any meta-data field from any project, with the help of the build project:

#!/usr/bin/env python

import argparse
import pathlib

import build.util

def _main():
    args_parser = argparse.ArgumentParser()
    args_parser.add_argument('path')
    args = args_parser.parse_args()
    path_name = getattr(args, 'path')
    path = pathlib.Path(path_name)
    #
    metadata = build.util.project_wheel_metadata(path)
    print(metadata)

if __name__ == '__main__':
    _main()

This will actually call the build back-end (setuptools, poetry, flit, pdm, or watever), so this might take some seconds.

The build.util API is documented here (on “latest”) and it was added in “0.7.0 (16-09-2021)”, in this change.

1 Like

Are you only trying to find out what setuptools_scm thinks is the current version number? Or is this intended to be part of a larger automated task or something? The default versioning scheme that setuptools_scm uses is explained in the documentation. If you just want to see the repository tags (which setuptools_scm will use to compute a version number; otherwise the only thing it can go off of is the number of commits), and you aren’t trying to do it programmatically, it might make more sense to use the CLI for your version control directly.

If you need to take into account anywhere that the metadata could be or how it could be specified (in particularly if it’s something that a tool like setuptools_scm is computing on the fly, and the tool doesn’t offer its own interface), then yes, the natural approach is to invoke the build system and see what it generates for the corresponding core metadata.

However, for something that’s known to be in pyproject.toml, or setup.cfg etc. (and it seems like OP in that case was specifically concerned with setup.cfg), it would make more sense to just parse that file directly. In the case of pyproject.toml in particular, one can simply install toml or tomli (or use tomllib since 3.11) and interpret the file just as one would with a JSON file and json.

1 Like

Hi, @sinoroc thanks for the extensive answer, I think, I’ll stick to the setuptools_scm approach, the “overkill” approach would also work, but it’s definitely… overkill (and it takes 17s where setuptools_scm takes 1s).

1 Like

It is over-kill for that specific situation for sure : D

It is because we know what the build backend is and what its plugins are. But this solution should work for any project, any build backend, any plugin. It might be helpful to whoever read this thread in search for a generic solution.

We can simplify that a bit to just…

import sys
import build.util

path = sys.argv[1] if len(sys.argv) > 1 else "."
print(build.util.project_wheel_metadata(path))

This also gets the first in the current dir by default, without having to pass an arg.

The great majority of this time is due to needing to create an isolated environment and install the build dependencies, which is what gaurentees everything will work for any arbitrary project. To skip this when the build dependencies are already available in the local environment (which running python -m setuptools_scm also requires them to be) and perform similarly to the same, but automatically fall back to isolated mode otherwise, you can do the following:

import sys

import build
import build.util

path = sys.argv[1] if len(sys.argv) > 1 else "."
try:
    metadata = build.util.project_wheel_metadata(path, isolated=False)
except build.BuildBackendException:
    metadata = build.util.project_wheel_metadata(path, isolated=True)
print(metadata)

This is essentially what we do in Pyroma to retrieve the project metadata, taking the fast path if it works and otherwise falling back to the slower but more reliable path otherwise.

1 Like

OK, I ended up mixing both of your suggestions into the following script. That allows me to output only a specific metadata parameter, e.g. the “Version”:

#!/usr/bin/env python
# Alternative using the build front-end to `python -m setuptools_scm`

import argparse
import pathlib

import build.util


def get_args():
    """Parse the command line arguments"""
    args_parser = argparse.ArgumentParser(
        description="Output some or all metadata of a given project")
    args_parser.add_argument('--path', '-p', default='.',
                             help="Path where to find the project (default: current path)")
    args_parser.add_argument('keys', nargs='*',
                             help="Metadata keys to output (else output all metadata)")
    args = args_parser.parse_args()
    return args


def get_metadata(path='.'):
    """Get project metadata from the given path"""
    path = pathlib.Path(path)
    try:
        metadata = build.util.project_wheel_metadata(path, isolated=False)
    except build.BuildBackendException:
        metadata = build.util.project_wheel_metadata(path, isolated=True)
    return metadata


def main():
    args = get_args()

    metadata = get_metadata(args.path)

    if args.keys:
        for key in args.keys:
            print(metadata[key])
    else:
        print(metadata)


if __name__ == '__main__':
    main()

Thanks for the help, that was great!

1 Like