带有 env 变量的 ConfigParser 和字符串插值

Posted

技术标签:

【中文标题】带有 env 变量的 ConfigParser 和字符串插值【英文标题】:ConfigParser and String interpolation with env variable 【发布时间】:2022-01-20 08:05:23 【问题描述】:

这有点我没有使用 python 语法,我在读取带有插值的 .ini 文件时遇到问题。

这是我的ini文件:

[DEFAULT]
home=$HOME
test_home=$home

[test]
test_1=$test_home/foo.csv
test_2=$test_home/bar.csv

那几行

from ConfigParser import SafeConfigParser

parser = SafeConfigParser()
parser.read('config.ini')

print parser.get('test', 'test_1')

输出

$test_home/foo.csv

在我期待的时候

/Users/nkint/foo.csv

编辑:

我认为$ 语法隐含在所谓的字符串插值中(参考manual):

在核心功能之上,SafeConfigParser 支持 插值。这意味着值可以包含格式字符串 引用同一节中的其他值,或特殊的值 默认部分。

但我错了。这种情况如何处理?

【问题讨论】:

【参考方案1】:

基于@alex-markov 答案(和代码)和@srand9 评论,以下解决方案适用于环境变量和横截面引用。

请注意,插值现在基于ExtendedInterpolation 以允许横截面引用,并且基于before_read 而不是before_get

#!/usr/bin/env python3
import configparser
import os


class EnvInterpolation(configparser.ExtendedInterpolation):
    """Interpolation which expands environment variables in values."""

    def before_read(self, parser, section, option, value):
        value = super().before_read(parser, section, option, value)
        return os.path.expandvars(value)


cfg = """
[paths]
foo : $HOME
[section1]
key = value
my_path = $paths:foo/path
"""

config = configparser.ConfigParser(interpolation=EnvInterpolation())
config.read_string(cfg)
print(config['section1']['my_path'])

【讨论】:

【参考方案2】:

如果是 Python 3,您可以编写自定义插值:

import configparser
import os


class EnvInterpolation(configparser.BasicInterpolation):
    """Interpolation which expands environment variables in values."""

    def before_get(self, parser, section, option, value, defaults):
        value = super().before_get(parser, section, option, value, defaults)
        return os.path.expandvars(value)


cfg = """
[section1]
key = value
my_path = $PATH
"""

config = configparser.ConfigParser(interpolation=EnvInterpolation())
config.read_string(cfg)
print(config['section1']['my_path'])

【讨论】:

嘿@Alex,我也想从其他部分获取环境变量和值。我怎样才能做到这一点?扩展插值看起来并不直接。 请将缺少的呼叫添加到super().before_get(...) @srand9,我已经修复了示例,现在您可以使用 %-notation 来引用来自同一配置的其他部分的值。请参阅docs 了解更多详情 我有点危及您的答案,以发布我自己的答案来解决@srand9 的问题(这也是我的问题)。我希望你不介意:)【参考方案3】:

ConfigParser.get 值是字符串,即使您将值设置为整数或 True。但是 ConfigParser 有 getint、getfloat 和 getboolean。

settings.ini

[default]
home=/home/user/app
tmp=%(home)s/tmp
log=%(home)s/log
sleep=10
debug=True

配置阅读器

>>> from ConfigParser import SafeConfigParser
>>> parser = SafeConfigParser()
>>> parser.read('/home/user/app/settings.ini')
>>> parser.get('defaut', 'home')
'/home/user/app'
>>> parser.get('defaut', 'tmp')
'/home/user/app/tmp'
>>> parser.getint('defaut', 'sleep')
10
>>> parser.getboolean('defaut', 'debug')
True

编辑

确实,如果您使用os.environ 初始化SafeConfigParser,您可以将名称值作为environ var。感谢Michele's 的回答。

【讨论】:

我无法修改 ini 语法,因为其他软件已经在使用它并且它是 $ 语法【参考方案4】:

很晚了,但也许它可以帮助其他人寻找我最近得到的相同答案。此外,其中一个 cmets 是如何从其他部分获取环境变量和值。这是我在从 INI 文件读取时处理转换环境变量和多节标签的方法。

INI 文件:

[PKG]
# <VARIABLE_NAME>=<VAR/PATH>
PKG_TAG = Q1_RC1

[DELIVERY_DIRS]
# <DIR_VARIABLE>=<PATH>
NEW_DELIVERY_DIR=$DEL_PATH\ProjectName_$PKG:PKG_TAG_DELIVERY

使用 ExtendedInterpolation 的 Python 类,以便您可以使用 $PKG:PKG_TAG 类型格式。当我使用内置的os.path.expandvars() 函数(例如上面的$DEL_PATH)将INI 读入字符串时,我添加了将Windows 环境变量转换为字符串的功能。

import os
from configparser import ConfigParser, ExtendedInterpolation

class ConfigParser(object):

    def __init__(self):
        """
        initialize the file parser with
        ExtendedInterpolation to use $Section:option format
        [Section]
        option=variable
        """
        self.config_parser = ConfigParser(interpolation=ExtendedInterpolation())

    def read_ini_file(self, file='./config.ini'):
        """
        Parses in the passed in INI file and converts any Windows environ vars.

        :param file: INI file to parse
        :return: void
        """
        # Expands Windows environment variable paths
        with open(file, 'r') as cfg_file:
            cfg_txt = os.path.expandvars(cfg_file.read())

        # Parses the expanded config string
        self.config_parser.read_string(cfg_txt)

    def get_config_items_by_section(self, section):
        """
        Retrieves the configurations for a particular section

        :param section: INI file section
        :return: a list of name, value pairs for the options in the section
        """
        return self.config_parser.items(section)

    def get_config_val(self, section, option):
        """
        Get an option value for the named section.

        :param section: INI section
        :param option: option tag for desired value
        :return: Value of option tag
        """
        return self.config_parser.get(section, option)

    @staticmethod
    def get_date():
        """
        Sets up a date formatted string.

        :return: Date string
        """
        return datetime.now().strftime("%Y%b%d")

    def prepend_date_to_var(self, sect, option):
        """
        Function that allows the ability to prepend a
        date to a section variable.

        :param sect: INI section to look for variable
        :param option: INI search variable under INI section
        :return: Void - Date is prepended to variable string in INI
        """
        if self.config_parser.get(sect, option):
            var = self.config_parser.get(sect, option)
            var_with_date = var + '_' + self.get_date()
            self.config_parser.set(sect, option, var_with_date)

【讨论】:

【参考方案5】:

似乎在上一个版本3.5.0 中,ConfigParser 没有读取环境变量,所以我最终提供了一个基于BasicInterpolation 的自定义插值。

class EnvInterpolation(BasicInterpolation):
    """Interpolation as implemented in the classic ConfigParser,
    plus it checks if the variable is provided as an environment one in uppercase.
    """

    def _interpolate_some(self, parser, option, accum, rest, section, map,
                          depth):
        rawval = parser.get(section, option, raw=True, fallback=rest)
        if depth > MAX_INTERPOLATION_DEPTH:
            raise InterpolationDepthError(option, section, rawval)
        while rest:
            p = rest.find("%")
            if p < 0:
                accum.append(rest)
                return
            if p > 0:
                accum.append(rest[:p])
                rest = rest[p:]
            # p is no longer used
            c = rest[1:2]
            if c == "%":
                accum.append("%")
                rest = rest[2:]
            elif c == "(":
                m = self._KEYCRE.match(rest)
                if m is None:
                    raise InterpolationSyntaxError(option, section,
                                                   "bad interpolation variable reference %r" % rest)
                var = parser.optionxform(m.group(1))
                rest = rest[m.end():]
                try:
                    v = os.environ.get(var.upper())
                    if v is None:
                        v = map[var]
                except KeyError:
                    raise InterpolationMissingOptionError(option, section, rawval, var) from None
                if "%" in v:
                    self._interpolate_some(parser, option, accum, v,
                                           section, map, depth + 1)
                else:
                    accum.append(v)
            else:
                raise InterpolationSyntaxError(
                    option, section,
                    "'%%' must be followed by '%%' or '(', "
                    "found: %r" % (rest,))

BasicInterpolationEnvInterpolation 的区别在于:

   v = os.environ.get(var.upper())
   if v is None:
       v = map[var]

在签入map 之前,我试图在环境中找到var

【讨论】:

【参考方案6】:

从环境中正确替换变量的技巧是对环境变量使用 $ 语法:

[DEFAULT]
test_home=$HOME

[test]
test_1=%(test_home)s/foo.csv
test_2=%(test_home)s/bar.csv

【讨论】:

【参考方案7】:

如果你想扩展一些环境变量,你可以在解析StringIO流之前使用os.path.expandvars

import ConfigParser
import os
import StringIO

with open('config.ini', 'r') as cfg_file:
    cfg_txt = os.path.expandvars(cfg_file.read())

config = ConfigParser.ConfigParser()
config.readfp(StringIO.StringIO(cfg_txt))

【讨论】:

【参考方案8】:

首先根据文档,您应该使用%(test_home)s 插入test_home。此外,密钥不区分大小写,您不能同时使用 HOMEhome 密钥。最后,您可以使用SafeConfigParser(os.environ) 来考虑您的环境。

from ConfigParser import SafeConfigParser
import os


parser = SafeConfigParser(os.environ)
parser.read('config.ini')

config.ini 在哪里

[DEFAULT]
test_home=%(HOME)s

[test]
test_1=%(test_home)s/foo.csv
test_2=%(test_home)s/bar.csv

【讨论】:

我无法修改 ini 语法,因为其他软件已经在使用它并且它是 $ 语法 您应该在使用 SafeConfigParse() 之前对文件进行预处理。但是您可以将 $ 语法替换为 %()s 语法。真正的问题将是递归的home=$(HOME)。您可以假设大写单词来自 environ 并在更改 sys.environ dict 后替换为 __ENV__KEY 之类的内容。我现在做不到......但你可以自己托盘 这个实现让我印象深刻的一件事是,如果环境变量本身有 '%' 标记,那么解析器会抛出错误。我必须编写一个函数来过滤掉这些键/值。否则,我喜欢这个 impl。 如果您正在使用其他类型的变量以及路径变量,则使用 ConfigParser 而不是 SafeConfigParser 否则会报错 %%' 必须后跟 '%%' 或 '(',找到:%r" % (rest,)) 我喜欢这个答案的简单性,但担心它如何随意导入所有环境变量而不仅仅是相关变量,所以我只将所需的子集传递给ConfigParser:k: v for k, v in os.environ.items() if k in ('MY_ENV_1', 'MY_ENV_2')

以上是关于带有 env 变量的 ConfigParser 和字符串插值的主要内容,如果未能解决你的问题,请参考以下文章

使用 Docker 和 PHP 从 env 文件加载环境变量

Rack 中间件中的“env”变量是啥?

使用configparser读取带有中文的配置文件出现UnicodeDecodeError错误

使用 tox 和 Github Action 访问环境变量

python读取配置文件 变量 ConfigParser模块

.env 文件中的变量的 nestjs 问题