如何在不安装的情况下读取 Python 包元数据?

How to read Python package metadata without installation?

我有一个 Python 程序,它是 pip 的包装器,我用它来协助开发 Python 包。基本上我面临的问题是如何读取包的 NameVersion 等元数据(通常是 '.tar.gz' 和'.whl' 档案)无需安装。 distutils 或其他工具可以做到这一点吗?

只是一些注意事项...代码是为 Python 3 编写的,但我正在使用各种 Python 包,例如 sdist, bdist_wheel 对于 Py2 和 Py3。此外,我只关心我有路径的本地包,而不是 PyPi 上可用的理论包。

我现在做的工作很好,但看起来很乱,我想知道是否有更好的工具可以抽象它。现在我正在读取存档中的元数据文本文件并手动解析出我需要的字段。如果失败,我将从包的文件名中删除名称和版本(真的很糟糕)。有一个更好的方法吗?这是我用来解析包 NameVersion.

的两个函数



更新

Simeon,感谢您建议使用包含在 wheel 档案中的 metadata.json 文件。我不熟悉档案中包含的所有文件,但我希望有一种很好的方法来解析其中的一些文件。 metadata.json 当然符合车轮的标准。我打算把这个问题再悬而未决,看看在接受之前是否还有其他建议。

无论如何,如果以后有人遇到这个问题,我附上了更新后的代码。它可能可以更清晰地说明为 class,但这就是我现在所拥有的。对于边缘情况,它不是超级坚固的,所以买家要小心。

import tarfile, zipfile

def getmetapath(afo):
    """
    Return path to the metadata file within a tarfile or zipfile object.

    tarfile: PKG-INFO
    zipfile: metadata.json
    """

    if isinstance(afo, tarfile.TarFile):
        pkgname = afo.fileobj.name
        for path in afo.getnames():
            if path.endswith('/PKG-INFO'):
                return path
    elif isinstance(afo, zipfile.ZipFile):
        pkgname = afo.filename
        for path in afo.namelist():
            if path.endswith('.dist-info/metadata.json'):
                return path

    try:
        raise AttributeError("Unable to identify metadata file for '{0}'".format(pkgname))
    except NameError:
        raise AttributeError("Unable to identify archive's metadata file")


def getmetafield(pkgpath, field):
    """
    Return the value of a field from package metadata file.
    Whenever possible, version fields are returned as a version object.

    i.e. getmetafield('/path/to/archive-0.3.tar.gz', 'name') ==> 'archive'
    """

    wrapper = str

    if field.casefold() == 'version':
        try:
            # attempt to use version object (able to perform comparisons)
            from distutils.version import LooseVersion as wrapper
        except ImportError:
            pass

    # package is a tar archive
    if pkgpath.endswith('.tar.gz'):

        with tarfile.open(pkgpath) as tfo:
            with tfo.extractfile(getmetapath(tfo)) as mfo:
                metalines = mfo.read().decode().splitlines()

        for line in metalines:
            if line.startswith(field.capitalize() + ': '):
                return wrapper(line.split(': ')[-1])

    # package is a wheel (zip) archive
    elif pkgpath.endswith('.whl'):

        import json

        with zipfile.ZipFile(pkgpath) as zfo:
            metadata = json.loads(zfo.read(getmetapath(zfo)).decode())
            try:
                return wrapper(metadata[field.lower()])
            except KeyError:
                pass

    raise Exception("Unable to extract field '{0}' from package '{1}'". \
                    format(field, pkgpath))

这种情况并不好,这就是创建 wheel 文件的原因。如果您只需要支持 wheel 文件,那么您可以清理代码,但只要您必须支持 *.tar.gz 个源包,您的方法就会有点混乱。

车轮的文件格式在PEP 427中指定,因此您既可以解析文件名获取某些信息,也可以读取其中的<package>-<version>.dist-info目录的内容。特别是 metadata.jsonMETADATA 非常有用。事实上,阅读 metadata.json 就足够了,这将导致干净的代码无需安装即可访问该信息。

我会重构代码以使用 metadata.json 并为 PKG-INFO 的源包实施最大努力的方法。长期计划是将所有 tar.gz 源包转换为 wheels 并删除当时过时的 PKG-INFO 解析代码。

软件包 pkginfo 使这更容易。

from pkginfo import Wheel, SDist

def getmeta(pkgpath):
    if pkgpath.endswith('.tar.gz'):
        dist = SDist
    elif pkgpath.endswith('.whl'): 
        dist = Wheel
    pkg = dist(pkgpath)       
    print(pkg.name, pkg.license)