一种方便配置Python应用程序的方法,使遵循最佳实践变得容易和自然,并解决了在使用例如“configparser”库时遇到的各种常见问题。
项目描述
PyConfig
- TL;DR
PyConfig可以帮助你轻松编写可配置的应用程序,并在加载时处理配置验证。它允许最终用户选择他们的配置语言,以及是否使用文件或环境变量或两者都使用。该库旨在使最佳实践成为做事的自然方式,并消除编写和维护配置选项文档的需求。
- STL;INRAOT (Still Too Long; I’m Not Reading All Of That)
像configparser一样,但更酷。更安全。还有点号自动完成。
通过示例介绍
你可以在下面找到该库的完整指南,但对于简单的用例,可能只需查看一个示例就足够了,所以这就是我们的起点。
在这个例子中,我们假设构建一个问候用户并退出的应用程序。用户可以通过 --name 参数提供名字。问候语还包括根据是否将要下雨给出出门或待在家中的建议。
$ python -m demo
Hello, world! It's a beautiful day outside. Have fun!
$ python -m demo --name Dave
Hello, Dave! You should probably stay home today...
安装 PyConfig(包名为 nx_config)
使用 pip
$ pip install nx_config
或使用 poetry
$ poetry add nx_config
创建配置类及其部分类
首先向您的应用程序中添加一个新文件,例如 config.py。在文件中,您将定义一些“部分类”(它们是 ConfigSection 的子类)和一个“配置类”(它是 Config 的子类),然后初始化它的全局实例(为什么这样做是正确的,请见下文)
# demo/config.py
from datetime import timedelta
from typing import Optional
from nx_config import Config, ConfigSection, URL, SecretString, validate
class GreetingSection(ConfigSection):
num_exclamation_marks: int = 1
all_caps: bool = False
@validate
def positive_exclamation_marks(self):
if self.num_exclamation_marks <= 0:
raise ValueError("Number of exclamation marks must be positive!")
class WeatherSection(ConfigSection):
service_url: URL
username: Optional[str] = None
password: Optional[SecretString] = None
timeout_s: float = 70.0
@validate
def username_and_password_go_together(self):
if (self.username is None) ^ (self.password is None):
raise ValueError("Must either provide both username and password or neither of them!")
def timeout(self) -> timedelta:
return timedelta(seconds=self.timeout_s)
class DemoConfig(Config):
greet: GreetingSection
weather: WeatherSection
config = DemoConfig()
这里我们做了以下可配置的设置
在“world”或用户的名字后面添加多少个感叹号。
整个问候语是否全部打印为大写字母。
将用于获取天气数据(降雨概率)的哪个网络服务。
天气服务的用户凭据。
对天气服务请求的客户端超时。
注意,username 和 password 是可选类型,即可以是 None(某些天气服务可能是免费的)。此外,每个部分中的某些条目有默认值,而其他条目没有(这意味着用户必须通过配置文件或环境变量提供值)。
这里我们看到 URL 和 SecretString 类型。这些条目的值只是普通的 Python 字符串。这些类型提示用于向用户传达意图,并允许 PyConfig 执行验证和其他特殊行为。例如,类型为 SecretString 的条目不允许有默认值(除非它是可选的,并且默认值是 None)。此外,当打印配置或仅打印部分时,类型为 SecretString 的条目将被替换为星号 "*****"。
带有 @validate() 注解的方法将在配置加载后自动调用(理想情况下在应用程序启动时)。每个方法都用于验证单个部分,并且部分可以有多个验证器。
条目 timeout_s 和上面的方法 timeout 的组合帮助我们避免用户混淆,同时能够使用无单位的类型:实际的配置字段名称 timeout_s 明确告诉用户他们必须提供 以秒为单位 的值,但在我们的代码中,我们使用 timeout 方法,因此只与 timedelta 对象一起工作,永远不必担心测量单位。
最后,使用全局配置对象可能看起来很危险(尤其是在 Python 中),但 Config 和 ConfigSection 对象始终*是不可变的,因此没有必要担心全局 状态。
*: 配置内容可以通过两种方式被修改。一种是在使用 fill_config() 或 fill_config_from_path() 加载时。另一种是使用 test_utils.update_section()。你可以在你的代码库中快速找到这些函数的所有用法。加载函数理想情况下只应该使用一次,且仅限于启动时。在生产代码中使用 test_utils 模块应完全禁止!
在代码中使用配置
我们的应用程序的核心将在 greet.py 模块中实现,我们在其中多次使用全局配置。
# demo/greet.py
from datetime import timedelta
from random import random
from typing import Mapping
from demo.config import config
def _get_rain_probability(
url: str, params: Mapping[str, str], timeout: timedelta
) -> float:
return random() # Just as reliable as a weather service...
def greet(name: str):
msg = f"Hello, {name}" + ("!" * config.greet.num_exclamation_marks) # <= config used here
if config.greet.all_caps: # <= and here
msg = msg.upper()
if config.weather.username is None: # <= here too
params = {}
else:
params = {
"username": config.weather.username, # <= and here
"password": config.weather.password, # <= and again
}
rain_prob = _get_rain_probability(
url=config.weather.service_url, # <= once more
params=params,
timeout=config.weather.timeout(), # <= last time
)
if rain_prob > 0.5:
msg += " You should probably stay home today..."
else:
msg += " It's a beautiful day outside. Have fun!"
print(msg)
你的 IDE 可能会提供对节名称和节内条目进行自动补全的功能。与字典(例如,使用 configparser)的常规方法相比,你不太可能通过这种方式犯输入错误。即使你犯了错误,你也会尝试获取一个不存在的属性,而在 PyConfig 中,配置和节段的属性由类声明确定(它们不依赖于用户在运行时提供的配置文件)。这意味着如果你测试了你的代码并且没有收到 AttributeError,你可以确信在生产环境中也不会收到 AttributeError,无论你的用户在他们的配置文件中输入了什么。
在启动时加载配置
# demo/__main__.py
from argparse import ArgumentParser
from demo.config import config
from demo.greet import greet
from nx_config import add_cli_options, resolve_config_path, fill_config_from_path
parser = ArgumentParser()
parser.add_argument("--name")
add_cli_options(parser, config_t=type(config))
args = parser.parse_args()
fill_config_from_path(config, path=resolve_config_path(cli_args=args))
greet(name=args.name or "world")
这里的魔法发生在 fill_config_from_path()。此函数将读取配置文件并将配置对象的条目填充为相应的值。路径可以是硬编码的(不推荐),或者你可以使用 resolve_config_path() 不带参数,在这种情况下,路径通过 CONFIG_PATH 环境变量提供(更好),或者你可以使用上面提到的 argparse.ArgumentParser 允许用户通过 CLI 参数提供配置路径(最佳)。辅助函数 add_cli_options() 将添加 --config-path 选项(以及其他选项),resolve_config_path() 将尝试读取它。如果用户在命令行上没有提供路径,resolve_config_path() 仍然会使用 CONFIG_PATH 环境变量作为后备。
配置文件的格式将由路径的扩展名确定(例如,使用 .yaml 作为 YAML)。请注意,不提供配置文件(既不通过 --config-path 也不通过 CONFIG_PATH)是可以的(并且是常见的做法)。在这种情况下,配置值将从一个名为 SECTIONNAME__ENTRYNAME(双下划线!)的环境变量中读取。即使提供了配置文件,值也可以通过下面的方法通过这些环境变量覆盖。
编写配置文件
上面的 add_cli_options() 函数还添加了一个 --generate-config 选项,它会打印出一个模板配置文件并退出。它旨在如下使用
$ python -m demo --generate-config=yaml > demo/config.yaml
在本例中生成以下文件
# demo/config.yaml
greet:
#num_exclamation_marks:
#all_caps:
weather:
service_url:
#username:
#password:
#timeout_s:
所有条目和所有部分都存在,但是具有默认值的条目已被注释掉,这样您可以确切地知道需要填写什么才能使程序运行。我们可以在该文件中填写service_url,例如
service_url: www.weatherservice24.com/rain
并使用它来运行我们的应用程序。我们仍然可以使用规范命名的环境变量(如GREET__NUM_EXCLAMATION_MARKS)更改其他条目(甚至覆盖此文件中的值)
$ export GREET__NUM_EXCLAMATION_MARKS=5
$ python -m demo --name Dave --config-path demo/config.yaml
Hello, Dave!!!!! It's a beautiful day outside. Have fun!
为什么?
PyConfig有什么好处?为什么您要费力学习使用另一个库,当configparser已经做得相当不错时?此外:已经有很多Python配置库了!PyConfig有什么不同之处?
避免硬编码路径
configparser.ConfigParser.read()方法接受一个字符串或PathLike(或多个)作为参数。我见过并参与过许多项目,其中这个参数被硬编码为一个版本控制的字符串。这当然在大多数情况下是一个坏主意。这使得在本地尝试代码或自动部署到多个服务器变得困难,可能会导致与使用相同路径的不同应用程序发生冲突(因此使它们无法独立配置),由于权限缺失等问题而引起头痛,等等。它还使得在不同的应用程序运行中使用不同的配置变得麻烦和缓慢。
大多数在这些项目上工作的开发者都知道这是一个坏主意,也知道如何避免它(例如,从命令行参数或环境变量中获取路径),但(a)这些解决方案需要一些额外的工作,并且(b)它们需要教用户如何为每个应用程序提供配置路径...
PyConfig提供了两个非常简单的解决方案来解决这个问题,使最佳实践几乎是最容易的事情之一。首先,您可以使用不带参数的函数resolve_config_path()。这将返回一个pathlib.Path,如果已定义CONFIG_PATH环境变量,否则返回None。通过一些额外的工作,使用argparse.ArgumentParser和add_cli_options(),您可以允许最终用户通过--config-path命令行选项或CONFIG_PATH环境变量提供配置路径
parser = ArgumentParser()
add_cli_options(parser, config_t=DemoConfig)
args = parser.parse_args()
path = resolve_config_path(cli_args=args)
如果您有多个应用程序共享环境变量,或者您为单个应用程序使用多个配置类(很少需要),则可以在命令行选项和路径环境变量中添加前缀
parser = ArgumentParser()
add_cli_options(parser, prefix="demo", config_t=DemoConfig)
args = parser.parse_args()
path = resolve_config_path("demo", cli_args=args)
现在将使用命令行选项--demo-config-path和环境变量DEMO_CONFIG_PATH。
最重要的是,这种解决方案为用户提供了一种标准化的方法来提供配置文件,通过遵循简单命名约定的参数,为所有使用PyConfig的应用程序。
不可变性
有些人可能会争论,在上述示例中,我们不应该创建一个全局的config对象,而是在启动时只是加载它,而是应该创建并加载一个在__main__.py中创建并加载的config对象,然后将其注入到greet调用中。在大多数情况下,我会同意这个建议。但它是为了避免全局状态,即可以在代码的任何地方读取和修改的全局变量,通常会引起麻烦。
在Config实例的情况下,我们无需担心。配置对象及其各个部分和条目都是不可变的,因此实例只是一个常量的命名空间。支持的部分条目类型也都是不可变的,包括支持的集合类型tuple和frozenset。
许多配置库允许在任何时候自由修改配置对象,这在长期运行的服务中尤其成问题。如果发生关键错误甚至崩溃,您无法保证启动时提供的配置仍然是正在使用的配置。当前配置可能与您在配置文件中看到的值完全不同。这使得理解和复制错误变得困难。使用PyConfig,通过在项目中搜索fill_config()和fill_config_from_path()的使用,可以非常容易地检查配置是否可以更改。理想情况下,它将在启动时加载一次,但即使您的应用程序允许在运行时更新配置,协调此操作的逻辑至少是易于找到的。此外,请参阅下面的“日志”部分,它可以帮助使您的应用程序易于调试。
为了便于使用不同的配置进行测试,我们添加了test_utils.update_section()函数(只能通过test_utils模块导入,而不能直接从nx_config导入)
# tests/test_greeting.py
from unittest import TestCase
from nx_config.test_utils import update_section
from demo.config import config
class DemoTests(TestCase):
def setUp(self):
... # load your base config values for testing
def test_something(self):
update_section(config.greet, num_exclamation_marks=7)
... # call code that uses config
同样,您可以轻松扫描项目中的test_utils使用情况。显然,它只能用于测试,决不能用于生产代码。就是这样!fill_config()、fill_config_from_path()和test_utils.update_section()是修改配置实例的唯一方法。
*, ** 和 ***:当然……这是Python……总有通过修改配置和部分内部属性来欺骗的黑暗方式。让我们假设所有为您的项目做出贡献的人都是善良的成年人。
配置文件格式
与许多配置库不同,PyConfig完全将您的代码(以及您的配置选项的建模)与最终用户允许选择的输入格式分开。您只需编写Python代码,而无需考虑YAML、INI、JSON、.ENV或其他任何东西。您的代码与配置格式无关。
PyConfig目前支持YAML、INI和环境变量。然而,它被设计成易于扩展,我们将倾听社区的反馈,看看哪些其他格式会是好的候选者。当添加新格式时,作为开发人员,您只需要安装最新版本,您的最终用户就可以开始享受额外的灵活性,即使您的代码保持不变。
这种选择自由对于使用不同编程语言的团队的公司来说也很有趣。他们可以选择定义一个单一的、公司范围内的“配置语言”,用于所有项目。这对每个人来说都很方便,并允许使用集中的配置文件在生产环境中使用(例如,使用不同服务的凭据、公共URL等)。同时,个别程序员仍然可以选择不同的“配置语言”进行本地测试,如果他们想的话。
记录配置选项
使用PyConfig最大的优势之一是配置模型的内容(即它应该有哪个部分,每个部分应该有哪些条目,它们的类型应该是什么等)仅定义在代码中。
以 configparser 为例,通常有3个独立的配置选项“定义”。一个是源代码中配置映射的用法,它遍布整个代码库,并不总是容易找到。第二个是为最终用户编写的文档,通常以PDF或markdown格式,列出所有部分、条目、类型以及如何使用每个条目。第三个有时是一个模板 INI文件,最终用户可以复制该文件并用他们选择的值填写。这3个“定义”需要维护并保持同步,但这种情况很少见。开发者经常会不小心删除使用配置值的代码,或者添加使用全新的配置条目的代码,或者更改条目的默认值……并忘记更新文档或INI模板。即使你非常小心,投入大量精力保持文档更新,经验丰富的最终用户仍然不会信任你的文档,因为他们已经多次陷入过这个陷阱。
进入:PyConfig!代码,即你的类定义,是配置选项的唯一定义。这是最终的真实情况,始终是最新的,并详细记录了配置的每个细节,包括类型、默认值以及有效性标准。如果你为配置类和部分类添加文档字符串,它们更有可能保持更新,因为它们紧挨着它们引用的代码。一些工具甚至直接支持在类属性下添加文档字符串,所以你可以放心尝试。
如果你对一个 argparse.ArgumentParser 应用了 add_cli_options() 函数,你的最终用户将免费获得 --generate-config CLI选项,他们可以使用该选项为任何支持的文件格式生成配置模板,例如。
$ python -m demo --generate-config=yaml
greet:
#num_exclamation_marks:
#all_caps:
weather:
service_url:
#username:
#password:
#timeout_s:
使用 add_cli_options() 还会添加 --config-help CLI选项。它显示一个特定于文档应用程序的配置模型的消息,之后是关于如何使用PyConfig进行配置的速查表式、一般说明(面向最终用户)。
这意味着你应用程序需要的所有文档(就配置选项而言)都很容易、自动地从你的类定义中生成,并且始终是最新的!即使你希望在网站或GitHub上直接访问文档,你也可以设置管道在每次发布后重新生成它。无需维护。
你的项目的贡献者甚至更高兴:他们只需要查看Python代码,只需查看一个模块(通常称为config.py),无需任何额外的PDF文件、markdown文件或网页,并且他们可以保证在那里找到所有相关、最新的信息。
自动验证和在启动时失败
PyConfig始终将配置输入与在ConfigSection子类声明中使用的类型提示进行验证。在环境变量或INI文件的情况下,值最初被解释为字符串,因此“检查类型”意味着检查提供的字符串是否可以转换为预期的类型(即字符串 "3.14" 对浮点数是可行的,但对 UUID 就不行)。在YAML或JSON文件的情况下,例如,已经有标准库可以将它们解析为不同类型的Python对象,因此只需要进行较小的转换(例如,str 到 Path 或 list 到 frozenset)取决于提供的类型提示。
另外两个开箱即用的自动检查是
用户必须为每个没有默认值的字段提供值。
密钥不能有默认值。它们必须始终由最终用户提供。(但 Optional[SecretString] 可以有默认 None,tuple[SecretString, ...] 可以有默认 () 等。)
在此基础上,您可以通过 @validate() 注解将验证方法(单参数 self,无返回值)添加到您的部分类中。这些方法将在 fill_config() 或 fill_config_from_path() (见上面的示例)中填充部分的值之后立即被调用。
如果您使用 PyConfig 并遵循在应用启动时(并且仅在此之后)加载所有配置的最佳实践,那么您将永远不必担心无效的配置值会在您长时间运行的服务启动后造成麻烦,无论是深夜还是您的即将结束的假期中。您能用其他配置库做到同样的事情吗?当然可以。PyConfig 只是友好且方便。
日志记录(以及密钥)
Config 和 ConfigSection 子类都可以非常方便地轻松打印。`__str__` 方法生成内联描述,而 `__repr__` 方法提供多行和缩进版本。此外,密钥(即作为 SecretString 类型注解的部分)会自动用星号屏蔽,包括可选的密钥和密钥集合。
以下是使用上面提到的 DemoConfig 类的示例输出
>>> print(str(config)) DemoConfig(greet=GreetingSection(num_exclamation_marks=1, all_caps=False), weather=WeatherSection(service_url='www.weatherservice24.dummy', username='Dave', password='*****', timeout_s=70.0)) >>> print(str(config.greet)) GreetingSection(num_exclamation_marks=1, all_caps=False) >>> print(repr(config)) DemoConfig( greet=GreetingSection( num_exclamation_marks=1, all_caps=False, ), weather=WeatherSection( service_url='www.weatherservice24.dummy', username='Dave', password='*****', timeout_s=70.0, ), ) >>> print(repr(config.greet)) GreetingSection( num_exclamation_marks=1, all_caps=False, )
在编写日志消息时,同时具有这两种格式非常方便,您确实应该利用这一点并在某些情况下记录应用程序的配置。一个好的主意是在启动时加载配置后立即记录配置。另一种方法是在发生严重错误时记录配置(这对于调试来说更方便,因为所有重要信息都捆绑在错误消息中)。仅仅始终记录整个配置,而不是尝试猜测足够调试所需值的子集,也很方便。如果您始终记录整个配置(或者至少整个部分),那么您不必担心意外暴露最终用户的密钥。
选择哪种方法得到哪种格式时,考虑到了调试。在 REPL 中,如果您只输入您要检查的对象,则结果将使用 `__repr__` 打印。
>>> config.weather WeatherSection( service_url='www.weatherservice24.dummy', username='Dave', password='*****', timeout_s=70.0, )
如果您使用 PyCharm,则控制台和调试器的“变量”视图将使用 `__str__` 在变量名称旁边显示值,在这种情况下,单行描述更合适。
**: 仅当您使用 Config 和 ConfigSection 的 `__str__` 和 `__repr__` 方法时,密钥才会被屏蔽。请记住,`my_config.my_section.my_secret` 的实际值只是一个普通的内置 str,所以如果您在日志中打印它,它将 不会 被屏蔽!
使用属性而不是字符串
使用属性(例如 cfg.a_section.an_entry)来定义部分和部分条目,而不是像许多配置库中使用字符串映射方式(例如 cfg["a_section"]["an_entry"]),不仅更简洁、更美观,而且更容易输入。
你的集成开发环境(IDE)可以通过点自动补全帮助你(a)展示可用的部分和部分条目,以及(b)避免输入错误。这尤为重要,因为即使你的配置在启动时得到了彻底的验证,但在使用配置时出现的输入错误可能只会导致麻烦,而且是在没有人在场和准备采取行动的时候。 (当然,这种情况在你公司永远不会发生,因为你们每个项目都有100%的代码覆盖率…)
理论上,IDE还能做更多的事情。如果你在这样属性中输入错误(因为你没有使用自动补全),静态分析器可以突出显示它们并发出警告。如果你决定更改部分或部分条目的名称,IDE可以帮助进行自动重构。不幸的是,我们还没有成功让它们与PyConfig部分和条目一起工作。我们知道这是由于IDE的限制以及PyConfig在幕后使用了大量魔法,但我们仍在努力理解为什么它不起作用。
尽管如此,自动补全+更简洁+更美观已经足够成为喜欢属性胜过映射的原因。
通过环境变量进行便捷的配置
有些情况下使用文件来配置应用可能会很麻烦,比如在本地终端上进行快速测试和实验时,总是需要更改一个或两个配置选项。
使用PyConfig,你可以 总是 使用环境变量覆盖来自文件的任何配置。标准的命名约定是 SECTIONNAME__ENTRYNAME(是的,双下划线,这使得当部分名称或条目名称也包含下划线时,分离更加清晰)。在上面的示例中,我们已经看到如何通过设置环境变量 GREET__NUM_EXCLAMATION_MARKS 来覆盖 config.greet.num_exclamation_marks 条目。
如果你在单个应用中有多个配置,或者多个应用共享一些环境变量,你也可以使用前缀来使变量名称更具体。例如,你可以使用环境变量 FOO__GREET__NUM_EXCLAMATION_MARKS,并在加载配置时传递 env_prefix 参数到 fill_config_from_path(),就像 fill_config_from_path(config, path=..., env_prefix="FOO") 一样。
最后,配置文件的路径也可以通过环境变量提供,即 CONFIG_PATH。同样,可以使用前缀来使这个名称更具体。例如,你可以使用变量 BAR_CONFIG_PATH,然后使用 resolve_config_path("bar", cli_args=...) 来获取路径。注意:如果你在这种情况下使用 cli_args 参数,resolve_config_path() 将寻找选项 --bar-config-path 而不是 --config-path,因此请确保在调用 argparse.ArgumentParser 时使用相同的前缀,通过传递一个带有 prefix 参数的 add_cli_options(parser, prefix="bar", config_t=type(config))。
对最有用的类型提供支持
在加载配置值之后,你应该能够直接使用它们,而无需首先将它们转换为其他类型。大多数用例都应该由PyConfig(以及可能还有更多)已支持的类型来覆盖。
基础支持的类型包括 int、float、bool、str、datetime.datetime、uuid.UUID、pathlib.Path、nx_config.SecretString 和 nx_config.URL。
集合支持的类型包括所有 Python 版本的 typing.Tuple[base, ...] 和 typing.FrozenSet[base],以及 Python 3.9 及更高版本的 tuple[base, ...] 和 frozenset[base](其中 base 是上述基础支持类型之一)。请注意,元组类型中的省略号(...)在此处是字面意义,即表示任意长度且所有元素类型相同的元组。
可选支持的类型是 typing.Optional[base_or_coll](其中 base_or_coll 是上述基础或集合支持类型之一)。请注意,“可选”必须是最外层,即您不能有可选元素的集合,例如 tuple[Optional[int], ...]。
但是,如果您想使用自己的自定义类型,您可能需要付出更多的努力。例如,如果您想使用无单位的 Temperature 类型,最终用户必须提供一个有单位的值(例如 surface_temp_celsius: float),然后您必须自行转换它(例如,在同一节中的方法 def surface_temp(self) -> Temperature)。
关于导入的注意事项
您从 PyConfig 需要用于生产代码的所有内容都可以(并且应该)直接从 nx_config 模块导入
from nx_config import Config, ConfigSection, SecretString, fill_config, ...
您从 PyConfig 需要用于测试的所有内容都可以(并且应该)直接从 nx_config.test_utils 模块导入
from nx_config.test_utils import update_section
这就是全部内容。 如果您发现自己从其他子模块导入内容:这可能不是为您设计的。我已经努力将所有其他内容保护在下划线后面,但可能有一些东西已经或可能在未来滑过去了。
关于配置库与配置应用的注意事项
通常没有太多意义直接从文件和环境变量中直接将配置用于库。配置应由应用请求并接收,然后应用可以将任何必要的值注入到库类和函数中。库至少应该给应用提供将所有相关值作为输入参数注入的可能性。这使得编写测试更加容易和方便,甚至可能对性能很重要。
我见过提供初始化时解析配置文件(使用默认、硬编码的路径)的类的库。非常了解的用户很少在应用程序中初始化此类对象,并尽可能长时间地保留它们。但大多数用户只是假设初始化的成本几乎为零,并在需要时创建新对象,不知不觉地重复解析文件和丢弃信息。
应用程序编写者应该对如何以及何时读取和解析文件拥有最终控制权。
将一个 Config 子类添加到库中是一个非常糟糕的想法。这会强迫应用程序编写者使用该类来处理特定库,然后使用不同的类来处理他们的配置选项。将一个 ConfigSection 子类添加到库中 可以是应用程序编写者的友好功能,他们可以在自己的 Config 类中使用这些部分。但这也可能带来一些僵化:应用程序编写者可能只想让用户对库的配置拥有 一些 控制权,但库提供的 ConfigSection 很可能给予他们完全控制权。
保持简单:在应用程序中使用 PyConfig。在库中使用注入。
关于 pydantic 的说明
如果您不熟悉 pydantic:它是一个通用的“建模”Python库,几乎提供了 PyConfig 所做的一切,并且 更多(真的)。它功能更强大、更灵活,功能丰富,非常适合配置。它也比 PyConfig 更成熟、更老练。
当我第一次遇到 pydantic 时,我实际上对它与 PyConfig 部分的一些相似之处感到非常惊讶,例如他们提供的 @validator() 注解、NamedTuple 风格的类声明,甚至还有 SecretStr 类型!在最后这个例子中,nx_config.SecretString 类型在运行时变成了普通的 str,而 pydantic.SecretStr 类型是 str 的包装器,您需要调用 get_secret_value() 方法来使用包装的字符串。但这甚至更有趣,因为这正是我在 PyConfig 第一版中使用的方法,只是我的方法被命名为 get_value_at_own_peril(),它返回受保护的成员 _dont_you_dare_use_me。然后我的同事中说他们发现秘密字符串的使用很烦人,并让我改变了主意。
我对 pydantic 没有任何批评,并且我真诚地不认为其他库是“竞争”。我们都在同一条船上。但我确实认为有时候应该使用 pydantic,有时候应该使用 PyConfig。如果您已经在项目中使用 pydantic,或者您已经非常熟悉它,或者您实际上需要它来建模除配置之外的事物,请务必使用它。
然而,如果您只是想找到一种更好、更安全的方法来添加配置到您的应用程序中,那么也许您应该检查一下 PyConfig。它是最小化、单用途和简单的。几乎没有学习曲线,并且该包相当小巧,没有不必要的功能。它还强制执行不可变性,这是在 pydantic 中可选的。在我看来,您需要了解自己在做什么,并且在使用 pydantic(特别是在应用程序配置方面)时要自律,而 PyConfig 则自然引导您走向最佳实践。但嘿,我确实有偏见……
详细文档
PyConfig 的完整文档仍然非常 处于建设中,可以在 这里 找到。
常见问题解答
- 为什么我不能将部分嵌套到其他部分中?
这并不是最容易的设计选择。编写PyConfig时最重要的要求之一是它应该支持INI文件,而且只有INI文件(实际上)支持1级嵌套。最终,尽管这个问题经常被问到,但在配置中几乎没有更深层嵌套的用例。在少数这样的用例中,问题可以通过在应用程序中使用多个Config子类优雅地解决。
- 为什么我不能在Config子类中直接添加条目?为什么所有条目都必须在节中?
首先,这会给实现添加更多复杂性。其次,INI不允许没有节的部分。第三,这实际上不是什么大问题。你总是可以只添加一个通用节到你的配置中。
- 为什么不支持字典作为节条目的类型?
INI。答案几乎总是INI。我选择支持可迭代类型tuple和frozenset,因为将逗号分隔的值解释为序列是如此常见和自然,而这些类型在配置中非常有用。此外,我已经看到了几个项目,配置值通过逗号分隔转换为序列,但开发人员不得不自己解析字符串。
对于字典,没有这样简单、优雅且常见的表示。幸运的是,对于作为节条目的字典的需求也很少。
- 关于环境变量的标准命名:如果有一个名为foo__bar的节和名为baz的条目,还有一个名为foo的节和名为bar__baz的条目,会发生什么?
老实说,我没有想过。可能是一些不好的事情。
- 所有这些问题真的是经常被问到的,还是你在一边编造?
是的。
项目详情
nx_config_swarfield-1.1.tar.gz的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 9583739ac50eff7e061be64bbb050944f08c248f736d881f531dbc09d7a36b65 |
|
MD5 | 0c0ec154ab635a9b641ed7173812c717 |
|
BLAKE2b-256 | 8526054c64ab4f748e38b2b89f3dbeec97f4294426a6a7080a7645b9a005ea2f |
nx_config_swarfield-1.1-py3-none-any.whl的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | e1c29e322f9ea3a50fcbaecdfb52228e88162fe29e0863b8a8fb56efdd71bbd3 |
|
MD5 | 7bb9018d7379c2c81954811a42c63edf |
|
BLAKE2b-256 | e4c6a775eb2f65c17331f111ab2fb3e906b76f3867e02ba48c045f7462acfa25 |