简体   繁体   中英

Getting Python package distribution version from within a package

You can get the version of a python distribution using

import pkg_resources
pkg_resources.get_distribution("distro").version

This is great if you know the distribution name, however I need to dynamically figure out my distribution name at runtime.

# Common framework base app class, extended by each app
class App(object):
    def get_app_version(self) -> str:
        package_name = self.__class__.__module__.split('.')[0]
        try:
            return pkg_resources.get_distribution(package_name).version
        except Exception:
            return "development"

This works for cases where the app's package name is the same as the distribution name (eg requests ). However this fails once they don't match (eg my-app containing package my_app ).

So what I need is a mapping between distributions and their packages, which I'm sure must exist somewhere since pip seems to know what to delete when you call uninstall:

$ pip uninstall requests
Uninstalling requests-2.21.0:
  Would remove:
    /home/user/.virtualenvs/app/lib/python3.6/site-packages/requests-2.21.0.dist-info/*
    /home/user/.virtualenvs/app/lib/python3.6/site-packages/requests/*

How do I programatically access this mapping?

If you're looking for a solution that works both from your development—not installed, or just locally called—version, and an installed version, then try this solution.

Imports:

import ast
import csv
import inspect
from os import listdir, path

import pkg_resources

Utility function:

def get_first_setup_py(cur_dir):
    if 'setup.py' in listdir(cur_dir):
        return path.join(cur_dir, 'setup.py')
    prev_dir = cur_dir
    cur_dir = path.realpath(path.dirname(cur_dir))
    if prev_dir == cur_dir:
        raise StopIteration()
    return get_first_setup_py(cur_dir)

Now using Python's ast library:

def parse_package_name_from_setup_py(setup_py_file_name):
    with open(setup_py_file_name, 'rt') as f:
        parsed_setup_py = ast.parse(f.read(), 'setup.py')

    # Assumes you have an `if __name__ == '__main__':`, and that it's at the end:
    main_body = next(sym for sym in parsed_setup_py.body[::-1]
                     if isinstance(sym, ast.If)).body

    setup_call = next(sym.value
                      for sym in main_body[::-1]
                      if isinstance(sym, ast.Expr) and
                      isinstance(sym.value, ast.Call) and
                      sym.value.func.id in frozenset(('setup',
                                                      'distutils.core.setup',
                                                      'setuptools.setup')))

    package_version = next(keyword
                           for keyword in setup_call.keywords
                           if keyword.arg == 'version'
                           and isinstance(keyword.value, ast.Name))

    # Return the raw string if it is one
    if isinstance(package_version.value, ast.Str):
        return package_version.s

    # Otherwise it's a variable at the top of the `if __name__ == '__main__'` block
    elif isinstance(package_version.value, ast.Name):
        return next(sym.value.s
                    for sym in main_body
                    if isinstance(sym, ast.Assign)
                    and isinstance(sym.value, ast.Str)
                    and any(target.id == package_version.value.id
                            for target in sym.targets)
                    )

    else:
        raise NotImplemented('Package version extraction only built for raw strings and '
                             'variables in the same function that setup() is called')

Finally replace the function in @Gricey's answer by changing return "development" to:

return parse_package_name_from_setup_py(get_first_setup_py(path.dirname(__file__)))

Taken from my answer https://stackoverflow.com/a/60352386

I believe the project's name should be hard-coded if possible. If not then some function like the following could help figuring out the metadata for the installed distribution containing the current file ( __file__ ):

import pathlib
import importlib_metadata

def get_project_distribution():
    for dist in importlib_metadata.distributions():
        try:
            relative = pathlib.Path(__file__).relative_to(dist.locate_file(''))
        except ValueError:
            pass
        else:
            if relative in dist.files:
                return dist
    return None

project_distribution = get_project_distribution()
if project_distribution:
    project_name = project_distribution.metadata['Name']
    version = project_distribution.metadata['Version']

Update (February 2021):

Looks like this could become easier thanks to the newly added packages_distributions() function in importlib_metadata :

After a couple of hours of exploring pkg_resources and reading the source for pip's uninstall I've got the following working:

import inspect
import pkg_resources
import csv

class App(object):
    def get_app_version(self) -> str:
        # Iterate through all installed packages and try to find one that has the app's file in it
        app_def_path = inspect.getfile(self.__class__)
        for dist in pkg_resources.working_set:
            try:
                filenames = [
                    os.path.normpath(os.path.join(dist.location, r[0]))
                    for r in csv.reader(dist.get_metadata_lines("RECORD"))
                ]
                if app_def_path in filenames:
                    return dist.version
            except FileNotFoundError:
                # Not pip installed or something
                pass
        return "development"

This iterates through all installed packages and for each of those iterates through its list of files and tries to match that to the current file, this matches the package to the distribution. It's not really ideal, and I'm still open to better answers.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM