编写 setup.cfg 安装密钥以安装其他安装密钥的依赖项

问题描述 投票:0回答:1

问题:

我正在为 Python 包编写一个

setup.cfg
文件,该包根据用途需要不同的依赖项集。出于开发目的,我想向
[options.extras_require]
文件的
setup.cfg
表添加一个键,以同时安装多组依赖项。

目前

setup.cfg
文件的相关部分:

[options.extras_require]
option1 =
    pkg1
    pkg2
    pkg3
    pkg4
    pkg5
option2 =
    pkg1
    pkg2
    pkg6
    pkg7
    pkg8
option3 =
    pkg3
    pkg4
    pkg9
    pkg10
all = 
    pkg1
    pkg2
    pkg3
    pkg4
    pkg5
    pkg6
    pkg7
    pkg8
    pkg9
    pkg10

随附的

setup.py
文件经过了最低程度的修饰,我们所有的元数据目前仍保存在
setup.cfg
文件中:

from __future__ import annotations

import setuptools

if __name__ == "__main__":
    # Do not add any parameters here. Edit setup.cfg instead.
    setuptools.setup()

到目前为止,我已将依赖项手动添加到

all
键中,但这需要我在每次包的依赖项列表更改时手动爬行并向其添加依赖项。

有没有办法编写

all
键,以便安装选项 1、2 和 3 下列出的所有内容,而无需我每次环境更改时手动将它们写入
all

构建: 我目前测试的 Python 版本是 3.11.8,我使用的是

setuptools
68.2.2 和
pip
23.3.1。该包应该在 Python 3.9 及更高版本上工作,因此它需要与
setuptools
pip
的多个版本兼容。

更新:

将修改后的字典输入到

setup()
:

我尝试按照下面的建议修改

setup.py
文件,现在看起来像这样:

from __future__ import annotations

from setuptools import setup
from setuptools.config.setupcfg import read_configuration

# Run code only if file is run as a script
if __name__ == "__main__":
    # Read the configuration from setup.cfg
    config = read_configuration("setup.cfg")

    # Get the dependencies from the other sections
    option1_deps = config["options"]["extras_require"]["option1"]
    option2_deps = config["options"]["extras_require"]["option2"]
    option3_deps = config["options"]["extras_require"]["option3"]

    # Combine dependencies
    all_deps = option1_deps + option2_deps + option3_deps

    # Update the config with the combined dependences
    config["options"]["extras_require"]["all"] = all_deps

    # Pass the udpated configuration to setuptools.setup()
    setup(**config["options"])

但是,我在尝试运行时遇到了以下问题

pip install -e .[all]

  Preparing metadata (setup.py) ... error
  error: subprocess-exited-with-error
  
  × python setup.py egg_info did not run successfully.
  │ exit code: 1
  ╰─> [1 lines of output]
      error in setup command: 'python_requires' must be a string containing valid version specifiers; 'SpecifierSet' object has no attribute 'split'
      [end of output]
  
  note: This error originates from a subprocess, and is likely not a problem with pip.
error: metadata-generation-failed

× Encountered error while generating package metadata.
╰─> See above for output.

note: This is an issue with the package mentioned above, not pip.

单独测试

read_configuration()
setup()

经过一番调查,我发现

read_configuration
的工作原理与宣传的一样,并且能够正确生成
[metadata]
[options]
表的字典结构。然而,当它被输入到
setup()
时,会导致它之后无法正确读取
setup.cfg
。不带任何参数运行
setup()
效果很好。

我应该如何继续?

python configuration
1个回答
0
投票

TL;博士

有多种方法可以实现这一目标。最佳实践是使用

pyproject.toml
来管理 Python 项目,还有一些更简单的解决方法。

解决方法

一个简单的解决方案是将

pip install '.[all]'
替换为
pip install '.[purpose1, purpose2]'
。在这种情况下,两个组的依赖项都会安装。

如果您仍然想通过

pip install '.[all]'
安装所有依赖项,您应该解析
setup.cfg
文件并手动合并它们:

from __future__ import annotations

from setuptools import setup
from setuptools.config.setupcfg import read_configuration

# Run code only if file is run as a script
if __name__ == "__main__":
    # Read the configuration from setup.cfg
    config = read_configuration("setup.cfg")

    # Get the dependencies from the other sections
    option1_deps = config["options"]["extras_require"]["option1"]
    option2_deps = config["options"]["extras_require"]["option2"]
    option3_deps = config["options"]["extras_require"]["option3"]

    # Combine dependencies
    all_deps = option1_deps + option2_deps + option3_deps

    # Update the config with the combined dependences
    config["options"]["extras_require"]["all"] = all_deps

    # Pass the udpated configuration to setuptools.setup()
    setup(**config["options"])

请注意,由于

read_configuration
已被弃用作为公开的主 API,因此应从
read_configuration
而不是
setuptools.config.setupcfg
导入
setuptools.config
。 (根据警告消息,这将来也可能被弃用,因此建议从
setup.cfg
迁移到
pyproject.toml
。)

(虽然我不确定为什么说明符有错误。它看起来很适合我。)

更好(但不完美)的方法

注意:本节中仍然存在一些未解决的问题(请参阅讨论),我正在寻找解决方案。 如果有人对此有任何想法,请随时告诉我!

PEP 517中提出了一种构建Python包的现代方法,其中建议将

setup.cfg
替换为
pyproject.toml
。 在你的情况下,
options.extras_require
options.python_requires
应该相当于
pyproject.toml
:

[project]
requires-python = ">=3.9"

[project.optional-dependencies]
purpose1 = [
    "pkg1",
    ...,
    "pkg5"
]
purpose2 = [
    "pkg1",
    ...,
    "pkg8"
]
all = [
    "__ALL_DEPS__"
]

然后你可以像这样组合两组:

import tomllib                                                                                                                                                                                                        
from copy import deepcopy                                                                                                                                                                                             
from setuptools import setup                                                                                                                                                                                          
                                                                                                                                                                                                                      
# Run code only if file is run as a script                                                                                                                                                                            
if __name__ == "__main__":                                                                                                                                                                                            
    # Read the configuration from pyproject.toml                                                                                                                                                                      
    with open('pyproject.toml', 'rb') as fcfg:                                                                                                                                                                        
        config = tomllib.load(fcfg)                                                                                                                                                                                   
                                                                                                                                                                                                                      
    # Get the dependencies from the other sections                                                                                                                                                                    
    option1_deps = config["project"]["optional-dependencies"]["option1"]                                                                                                                                              
    option2_deps = config["project"]["optional-dependencies"]["option2"]                                                                                                                                              
    option3_deps = config["project"]["optional-dependencies"]["option3"]                                                                                                                                              
                                                                                                                                                                                                                      
    # Combine dependencies                                                                                                                                                                                            
    all_deps = option1_deps + option2_deps + option3_deps                                                                                                                                                             
                                                                                                                                                                                                                      
    # Construct dependencies mapping                                                                                                                                                                                  
    extras_require = deepcopy(config["project"]["optional-dependencies"])                                                                                                                                             
    extras_require["all"] = all_deps                                                                                                                                                                                  
                                                                                                                                                                                                                      
    # Pass the updated configuration to setuptools.setup()                                                                                                                                                            
    setup(extras_require=extras_require)

讨论

这段代码仍然存在一些问题。 即使我将 extras_require 传递给

setup
方法,我也无法弄清楚为什么没有安装依赖项。 也许这是因为
pyproject.toml
中的额外依赖项覆盖了
setup
参数。 如果是这个原因,修改后的配置应该写回
pyproject.toml
:

with open('pyproject.toml', 'r') as fcfg:
    contents = fcfg.read()
deps_string = '\n'.join([f'"{dep}",' for dep in all_deps])
with open('pyproject.toml', 'w') as fcfg:
    fcfg.write(contents.replace('"__ALL_DEPS__"', deps_string)

显然这不是完美的解决方案,但在这种情况下应该可行。

结论

总而言之,实现这一点的主要思想是解析配置文件并手动组合各个部分。 无论使用哪种配置格式,核心都是解析配置文件,合并依赖组,并应用合并的依赖项。

上述解决方案可能仍需要改进,当我找到合适的方法时我会更新这个答案。

© www.soinside.com 2019 - 2024. All rights reserved.