import osimport refrom functools import partialimport yamlhas_regex_module = FalseENV_VAR_MATCHER = re.compile(    r"""        \$\{       # match characters `${` literally        ([^}:\s]+) # 1st group: matches any character except `}` or `:`        :?         # matches the literal `:` character zero or one times        ([^}]+)?   # 2nd group: matches any character except `}`        \}         # match character `}` literally    """, re.VERBOSE)IMPLICIT_ENV_VAR_MATCHER = re.compile(    r"""        .*          # matches any number of any characters        \$\{.*\}    # matches any number of any characters                    # between `${` and `}` literally        .*          # matches any number of any characters    """, re.VERBOSE)RECURSIVE_ENV_VAR_MATCHER = re.compile(    r"""        \$\{       # match characters `${` literally        ([^}]+)?   # matches any character except `}`        \}         # match character `}` literally        ([^$}]+)?  # matches any character except `}` or `$`        \}         # match character `}` literally    """,    re.VERBOSE,)def _replace_env_var(match):    env_var, default = match.groups()    value = os.environ.get(env_var, None)    if value is None:        # expand default using other vars        if default is None:            # regex module return None instead of            #  '' if engine didn't entered default capture group            default = ''        value = default        while IMPLICIT_ENV_VAR_MATCHER.match(value):  # pragma: no cover            value = ENV_VAR_MATCHER.sub(_replace_env_var, value)    return valuedef env_var_constructor(loader, node, raw=False):    raw_value = loader.construct_scalar(node)    # detect and error on recursive environment variables    if not has_regex_module and RECURSIVE_ENV_VAR_MATCHER.match(        raw_value    ):  # pragma: no cover        raise Exception(            "Nested environment variable lookup requires the `regex` module"        )    value = ENV_VAR_MATCHER.sub(_replace_env_var, raw_value)    if value == raw_value:        return value  # avoid recursion    return value if raw else yaml.safe_load(value)def setup_yaml_parser():    yaml.add_constructor('!env_var', env_var_constructor, yaml.SafeLoader)    yaml.add_constructor(        '!raw_env_var',        partial(env_var_constructor, raw=True),        yaml.SafeLoader    )    yaml.add_implicit_resolver(        '!env_var', IMPLICIT_ENV_VAR_MATCHER, Loader=yaml.SafeLoader    )# copy from nameko https://github.com/nameko/nameko/blob/v2.14.1/nameko/cli/main.py

写个单元测试

# coding=utf-8import csvimport jsonimport osimport timeimport unittestfrom collections import namedtuplefrom concurrent.futures import ThreadPoolExecutorfrom pathlib import Pathfrom uuid import uuid4# from elasticsearch import Elasticsearchfrom loguru import loggerimport settingsfrom mark import BASE_DIRfrom pathlib import Pathimport yamlTESTING_BASE_DIR = Path(__file__).resolve().parentclass TestEnvParse(unittest.TestCase):    def test_parse_yaml_with_env(self):        """        测试 es 是否联通        python -m unittest testing.test_env.TestEnvParse.test_parse_yaml_with_env        """        from pon.events import EventletEventRunner        config_filepath = TESTING_BASE_DIR/'config.yaml'        with open(config_filepath, 'r', encoding='utf-8') as f:            config: dict[str, dict] = yaml.safe_load(f)        logger.debug(config)

筹备的 yaml 文件

AMQP_URI: amqp://${RABBIT_USER:guest}:${RABBIT_PASSWORD:guest}@${RABBIT_HOST:localhost}:${RABBIT_PORT:5672}/${RABBIT_VHOST:/}prd:    database:        mysql:            host: ${MYSQL_HOST:110.110.100.110}            port: 3306            username: you            password: you            database_name: haha        elasticsearch:            host: 1110.110.110.100            port: 9200            username: hah            password: hah            index_name: hehe

运行命令:MYSQL_HOST=192.168.31.245 python -m unittest testing.test_env.TestEnvParse.test_parse_yaml_with_env

2022-09-27 13:54:29.565 | DEBUG    | testing.test_env:test_parse_yaml_with_env:33 - {'AMQP_URI': 'amqp://pon:[email protected]:5672//', 'prd': {'database': {'mysql': {'host': '192.168.31.245', 'port': 3306, 'username': 'you', 'password': 'you', 'database_name': 'haha'}, 'elasticsearch': {'host': '1110.110.110.100', 'port': 9200, 'username': 'hah', 'password': 'hah', 'index_name': 'hehe'}}}}

运行后果,能够看到 mysql 的 host 曾经被环境变量替换了