英文:
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
文件不是软件包安装的一部分。(我应该意识到这一点。)
那么,提供元数据(如软件包版本)的正确/最佳方式是什么?我有以下要求:
- 我只想在
pyproject.toml
中提供信息一次。(我知道如果我需要重复一个值,最终会出现不匹配的问题。) - 我希望最终用户可以访问这些信息,这样安装软件包的人可以从她的交互式Python会话中执行类似
mypackage.VERSION
的操作。 - 我只想使用
pyproject.toml
、Poetry或PDM。(实际上我使用PDM,但我知道Poetry更受欢迎。关键是我不想使用setup.py
或setup.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:
- 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.) - 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. - 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 asetup.py
orsetup.cfg
hack. I want to purely use the new way.)
答案1
得分: 3
如您所见,来自原始源树的pyproject.toml
文件通常无法在已安装的包内读取。这并不是新问题,同样的情况也适用于传统源树中的setup.py
文件,在实际安装中会缺失。但包的元数据可以在与您的包安装相邻的.dist-info子目录中找到。
为了演示使用一个真实的已安装包和相应的元数据文件的示例,其中包含了从setup.py
或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 '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 '^Version:' .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.:
>>> from importlib.metadata import metadata
>>> metadata("six")["Summary"]
'Python 2 and 3 compatibility utilities'
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 == "VERSION":
# consider adding a deprecation warning here advising caller to use
# importlib.metadata.version directly
return importlib.metadata.version("mypackage")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论