如何在使用pyproject.toml构建时,正确地捕获库/包版本到我的包中。

huangapple go评论80阅读模式
英文:

How to properly capture library / package version in my package when using pyproject.toml to build

问题

我已经从使用setup.py文件构建我的软件包/库转移到完全使用pyproject.toml。总体上我更喜欢这种方式,但似乎在pyproject.toml中放置的版本号并没有以任何方式传递到构建中。因此,我无法弄清楚如何将软件包版本或在pyproject.toml中提供的任何其他元数据注入到我的软件包中。

谷歌搜索引导我进入了这个帖子,其中提出了一些建议。它们都似乎是一种不太正规的方法,但我尝试了这个,因为它似乎是最佳选择:

from pip._vendor import tomli ## 我需要向后兼容 Python 3.8

with open("pyproject.toml", "rb") as proj_file:
    _METADATA = tomli.load(proj_file)

DESCRIPTION = _METADATA["project"]["description"]
NAME = _METADATA["project"]["name"]
VERSION = _METADATA["project"]["version"]

在测试时它可以正常工作,但我没有进行足够稳健的测试:一旦我尝试在一个新的位置/机器上安装它,它失败了,因为pyproject.toml文件不是软件包安装的一部分。(我应该意识到这一点。)


那么,提供元数据(如软件包版本)的正确/最佳方式是什么?我有以下要求:

  1. 我只想在pyproject.toml中提供信息一次。(我知道如果我需要重复一个值,最终会出现不匹配的问题。)
  2. 我希望最终用户可以访问这些信息,这样安装软件包的人可以从她的交互式Python会话中执行类似mypackage.VERSION的操作。
  3. 我只想使用pyproject.toml、Poetry或PDM。(实际上我使用PDM,但我知道Poetry更受欢迎。关键是我不想使用setup.pysetup.cfg的不正规方法。我想纯粹使用新的方式。)
英文:

I have moved away from a setup.py file to build my packages / libraries to fully using pyproject.toml. I prefer it overall, but it seems that the version placed in the pyproject.toml does not propagate through the build in any way. So I cannot figure out how to inject the package version -- or any other metadata provided in the pyproject.toml -- into my package.

A google search led me to this thread, which had some suggestions. They all seemed like hacks, but I tried this one because it seemed best:

from pip._vendor import tomli ## I need to be backwards compatible to Python 3.8

with open("pyproject.toml", "rb") as proj_file:
    _METADATA = tomli.load(proj_file)

DESCRIPTION = _METADATA["project"]["description"]
NAME = _METADATA["project"]["name"]
VERSION = _METADATA["project"]["version"]

It worked fine upon testing, but I did not test robustly enough: once I tried to install this in a fresh location / machine, it failed because the pyproject.toml file is not part of the package installation. (I should have realized this.)


So, what is the right / best way to provide metadata, like the package version, to my built package? I need the following requirements:

  1. I only want to provide the information once, in the pyproject.toml. (I know that if I need to repeat a value, at some point there will be a mismatch.)
  2. I want the information to be available to the end user, so that someone who installs the package can do something like mypackage.VERSION from her interactive Python session.
  3. I want to only use pyproject.toml and Poetry / PDM. (I actually use PDM, but I know that Poetry is more popular. The point is that I don't want a setup.py or setup.cfg hack. I want to purely use the new way.)

答案1

得分: 3

如您所见,来自原始源树pyproject.toml文件通常无法在已安装的包内读取。这并不是新问题,同样的情况也适用于传统源树中的setup.py文件,在实际安装中会缺失。但包的元数据可以在与您的包安装相邻的.dist-info子目录中找到。

为了演示使用一个真实的已安装包和相应的元数据文件的示例,其中包含了从setup.pypyproject.toml中获取的版本信息和其他内容:

$ pip install -q six
$ pip show --files six
Name: six
Version: 1.16.0
Summary: Python 2 and 3 compatibility utilities
Home-page: https://github.com/benjaminp/six
Author: Benjamin Peterson
Author-email: benjamin@python.org
License: MIT
Location: /tmp/eg/.venv/lib/python3.11/site-packages
Requires: 
Required-by: 
Files:
  __pycache__/six.cpython-311.pyc
  six-1.16.0.dist-info/INSTALLER
  six-1.16.0.dist-info/LICENSE
  six-1.16.0.dist-info/METADATA
  six-1.16.0.dist-info/RECORD
  six-1.16.0.dist-info/REQUESTED
  six-1.16.0.dist-info/WHEEL
  six-1.16.0.dist-info/top_level.txt
  six.py
$ grep 'Version:' .venv/lib/python3.11/site-packages/six-1.16.0.dist-info/METADATA
Version: 1.16.0

这个.dist-info/METADATA文件包含版本信息和其他元数据。用户可以在必要时,使用stdlib的importlib.metadata来访问包元数据。版本字符串是已安装包中必须存在的,因为它是元数据规范中的必填字段,并且有一个专用的函数用于获取它。其他可选的键可以使用metadata头找到,例如:

>>> from importlib.metadata import metadata
>>> metadata("six")["Summary"]
'Python 2 and 3 compatibility utilities'

所有需要访问元数据的Python版本都已经到达了终止支持(EOL),但如果您需要维护对Python <= 3.7的支持,那么可以使用PyPI上的importlib-metadata,方式与stdlib相同。

当您只发布一个包含__init__.py文件的顶级目录,并且它与pyproject.toml中的项目名称相同时,您可以使用__package__属性,以便您不必在源代码中硬编码包名称:

# 在包内的任何位置都可以访问
import importlib.metadata

the_version_str = importlib.metadata.version(__package__)

如果名称不匹配(例如:分发包 "python-dateutil" 提供导入包 "dateutil"),或者您有多个顶级名称(例如:分发包 "setuptools" 提供导入包 "setuptools" 和 "pkg_resources"),那么您可能希望硬编码包名称,或者尝试从已安装的packages_distributions()映射中发现它。

这满足了点1和点3。

对于点2:

我希望信息对最终用户可用,以便安装包的人可以在其交互式Python会话中执行像mypackage.VERSION这样的操作。

类似的方法可以工作:

# 在your_package/__init__.py中
import importlib.metadata

VERSION = importlib.metadata.version(__package__)

然而,我建议根本不提供版本属性。这样做有几个不利之处,原因我在这里这里已经描述过。如果您以前提供了版本属性,并且出于向后兼容性考虑需要保留它,您可以考虑使用一个后备模块__getattr__来维护支持,当通常的方式找不到属性时将调用该模块:

def __getattr__(name):
    if name == "VERSION":
        # 考虑添加一个弃用警告,建议调用者直接使用importlib.metadata.version
        return importlib.metadata.version("mypackage")
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
英文:

As you've seen, the pyproject.toml file from the original source tree is generally not available to read from within an installed package. This isn't new, it's also the case that the setup.py file from a legacy source tree would be missing from an actual installation. The package metadata, however, is available in a .dist-info subdirectory alongside your package installation.

To demonstrate using a real example of an installed package and the corresponding metadata files, which contain the version info and other stuff coming from the setup.py or pyproject.toml:

$ pip install -q six
$ pip show --files six
Name: six
Version: 1.16.0
Summary: Python 2 and 3 compatibility utilities
Home-page: https://github.com/benjaminp/six
Author: Benjamin Peterson
Author-email: benjamin@python.org
License: MIT
Location: /tmp/eg/.venv/lib/python3.11/site-packages
Requires: 
Required-by: 
Files:
  __pycache__/six.cpython-311.pyc
  six-1.16.0.dist-info/INSTALLER
  six-1.16.0.dist-info/LICENSE
  six-1.16.0.dist-info/METADATA
  six-1.16.0.dist-info/RECORD
  six-1.16.0.dist-info/REQUESTED
  six-1.16.0.dist-info/WHEEL
  six-1.16.0.dist-info/top_level.txt
  six.py
$ grep &#39;^Version:&#39; .venv/lib/python3.11/site-packages/six-1.16.0.dist-info/METADATA
Version: 1.16.0

This .dist-info/METADATA file is the location containing the version info and other metadata. Users of your package, and the package itself, may access package metadata if/when necessary by using stdlib importlib.metadata. The version string is guaranteed to be there for an installed package, because it's a required field in the metadata specification, and there is a dedicated function for it. Other keys, which are optional, can be found using the metadata headers, e.g.:

&gt;&gt;&gt; from importlib.metadata import metadata
&gt;&gt;&gt; metadata(&quot;six&quot;)[&quot;Summary&quot;]
&#39;Python 2 and 3 compatibility utilities&#39;

All versions of Python where accessing the metadata was non-trivial are now EOL, but if you need to maintain support for Python <= 3.7 then importlib-metadata from PyPI can be used the same way as stdlib.

When you only publish one top-level directory containing an __init__.py file and it has the same name as the project name in pyproject.toml, then you may use the __package__ attribute so that you don't have to hardcode the package name in source code:

# can be accessed anywhere within the package
import importlib.metadata

the_version_str = importlib.metadata.version(__package__)

If the names are mismatching (example: the distribution package "python-dateutil" provides import package "dateutil"), or you have multiple top-level names (example: the distribution package "setuptools" provides import packages "setuptools" and "pkg_resources") then you may want to just hardcode the package name, or try and discover it from the installed packages_distributions() mapping.

This satisfies point 1. and point 3.

For point 2.:

> I want the information to be available to the end user, so that someone who installs the package can do something like mypackage.VERSION from her interactive Python session.

The similar recipe works:

# in your_package/__init__.py
import importlib.metadata

VERSION = importlib.metadata.version(__package__)

However, I would recommend not to provide a version attribute at all. There are several disadvantages to doing that, for reasons I've described here and here. If you have historically provided a version attribute, and need to keep it for backwards compatibility concerns, you may consider maintaining support using a fallback module __getattr__ which will be invoked when an attribute is not found by the usual means:

def __getattr__(name):
    if name == &quot;VERSION&quot;:
        # consider adding a deprecation warning here advising caller to use
        # importlib.metadata.version directly
        return importlib.metadata.version(&quot;mypackage&quot;)
    raise AttributeError(f&quot;module {__name__!r} has no attribute {name!r}&quot;)

huangapple
  • 本文由 发表于 2023年7月20日 19:29:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/76729393.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定