简体   繁体   English

从包中获取 Python 包分发版本

[英]Getting Python package distribution version from within a package

You can get the version of a python distribution using您可以使用以下命令获取 python 发行版的版本

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 ).这适用于应用程序包名称与分发名称相同的情况(例如requests )。 However this fails once they don't match (eg my-app containing package my_app ).然而,一旦它们不匹配(例如my-app包含包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 似乎知道在调用卸载时要删除的内容:

$ 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:现在使用 Python 的ast库:

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 "development"更改为:

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

Taken from my answer https://stackoverflow.com/a/60352386摘自我的回答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__ ):如果不是,那么像下面这样的函数可以帮助找出包含当前文件( __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):更新(2021 年 2 月):

Looks like this could become easier thanks to the newly added packages_distributions() function in importlib_metadata :由于importlib_metadata新添加的packages_distributions()函数,看起来这会变得更容易:

After a couple of hours of exploring pkg_resources and reading the source for pip's uninstall I've got the following working:经过几个小时的探索pkg_resources并阅读pip 卸载源代码后,我得到了以下工作:

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.这不是很理想,我仍然愿意接受更好的答案。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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