From 5f651a2e1390988afd72dad476ff32653936e17a Mon Sep 17 00:00:00 2001 From: Vasiliy Polyakov Date: Mon, 9 Jan 2023 02:36:45 +0500 Subject: [PATCH] Resolve environment variable values Fix #421: add support for embedded variables. --- environ/environ.py | 49 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/environ/environ.py b/environ/environ.py index fc5eebbe..9fce96e4 100644 --- a/environ/environ.py +++ b/environ/environ.py @@ -17,8 +17,10 @@ import os import re import sys +import threading import urllib.parse as urlparselib import warnings +from os.path import expandvars from urllib.parse import ( parse_qs, ParseResult, @@ -187,7 +189,10 @@ class Env: } CLOUDSQL = 'cloudsql' + VAR = re.compile(r'(?[A-Z_][0-9A-Z_]*)}?', re.IGNORECASE) + def __init__(self, **scheme): + self._local = threading.local() self.smart_cast = True self.escape_proxy = False self.prefix = "" @@ -358,9 +363,12 @@ def path(self, var, default=NOTSET, **kwargs): """ return Path(self.get_value(var, default=default), **kwargs) - def get_value(self, var, cast=None, default=NOTSET, parse_default=False): + def get_value(self, var, cast=None, default=NOTSET, parse_default=False, add_prefix=True): """Return value for given environment variable. + - Substitute environment variable values. + - Detect infinite recursion in values (self-reference). + :param str var: Name of variable. :param collections.abc.Callable or None cast: @@ -369,15 +377,30 @@ def get_value(self, var, cast=None, default=NOTSET, parse_default=False): If var not present in environ, return this instead. :param bool parse_default: Force to parse default. + :param bool add_prefix: + Whether to add prefix to variable name. :returns: Value from environment or default (if set). :rtype: typing.IO[typing.Any] """ + var_name = "{}{}".format(self.prefix, var) if add_prefix else var + if not hasattr(self._local, 'vars'): + self._local.vars = set() + if var_name in self._local.vars: + error_msg = "Environment variable '{}' recursively references itself (eventually)".format(var_name) + raise ImproperlyConfigured(error_msg) + + self._local.vars.add(var_name) + try: + return self._get_value(var_name, cast=cast, default=default, parse_default=parse_default) + finally: + self._local.vars.remove(var_name) + + def _get_value(self, var_name, cast=None, default=NOTSET, parse_default=False): logger.debug("get '{}' casted as '{}' with default '{}'".format( - var, cast, default + var_name, cast, default )) - var_name = "{}{}".format(self.prefix, var) if var_name in self.scheme: var_info = self.scheme[var_name] @@ -403,26 +426,28 @@ def get_value(self, var, cast=None, default=NOTSET, parse_default=False): value = self.ENVIRON[var_name] except KeyError as exc: if default is self.NOTSET: - error_msg = "Set the {} environment variable".format(var) + error_msg = "Set the {} environment variable".format(var_name) raise ImproperlyConfigured(error_msg) from exc value = default + # Substitute environment variables + if isinstance(value, (str, bytes)) and var_name != 'DJANGO_SECRET_KEY': + def repl(m): + return self.get_value(m['name'], cast=cast, default=default, + parse_default=parse_default, add_prefix=False) + value = self.VAR.sub(repl, value) + value = expandvars(value) + # Resolve any proxied values prefix = b'$' if isinstance(value, bytes) else '$' escape = rb'\$' if isinstance(value, bytes) else r'\$' - if hasattr(value, 'startswith') and value.startswith(prefix): - value = value.lstrip(prefix) - value = self.get_value(value, cast=cast, default=default) - if self.escape_proxy and hasattr(value, 'replace'): value = value.replace(escape, prefix) # Smart casting - if self.smart_cast: - if cast is None and default is not None and \ - not isinstance(default, NoValue): - cast = type(default) + if self.smart_cast and cast is None and default is not None and not isinstance(default, NoValue): + cast = type(default) value = None if default is None and value == '' else value