Skip to content
This repository was archived by the owner on May 4, 2023. It is now read-only.

Commit 9a3da42

Browse files
committed
Resolve environment variable values
Fix joke2k#421: add support for embedded variables.
1 parent 7720a49 commit 9a3da42

File tree

3 files changed

+73
-12
lines changed

3 files changed

+73
-12
lines changed

environ/environ.py

+37-12
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
import os
1818
import re
1919
import sys
20+
import threading
2021
import urllib.parse as urlparselib
2122
import warnings
23+
from os.path import expandvars
2224
from urllib.parse import (
2325
parse_qs,
2426
ParseResult,
@@ -187,7 +189,10 @@ class Env:
187189
}
188190
CLOUDSQL = 'cloudsql'
189191

192+
VAR = re.compile(r'(?<!\\)\$\{?(?P<name>[A-Z_][0-9A-Z_]*)}?', re.IGNORECASE)
193+
190194
def __init__(self, **scheme):
195+
self._local = threading.local()
191196
self.smart_cast = True
192197
self.escape_proxy = False
193198
self.prefix = ""
@@ -358,9 +363,12 @@ def path(self, var, default=NOTSET, **kwargs):
358363
"""
359364
return Path(self.get_value(var, default=default), **kwargs)
360365

361-
def get_value(self, var, cast=None, default=NOTSET, parse_default=False):
366+
def get_value(self, var, cast=None, default=NOTSET, parse_default=False, add_prefix=True):
362367
"""Return value for given environment variable.
363368
369+
- Substitute environment variable values.
370+
- Detect infinite recursion in values (self-reference).
371+
364372
:param str var:
365373
Name of variable.
366374
:param collections.abc.Callable or None cast:
@@ -369,15 +377,30 @@ def get_value(self, var, cast=None, default=NOTSET, parse_default=False):
369377
If var not present in environ, return this instead.
370378
:param bool parse_default:
371379
Force to parse default.
380+
:param bool add_prefix:
381+
Whether to add prefix to variable name.
372382
:returns: Value from environment or default (if set).
373383
:rtype: typing.IO[typing.Any]
374384
"""
375385

386+
var_name = "{}{}".format(self.prefix, var) if add_prefix else var
387+
if not hasattr(self._local, 'vars'):
388+
self._local.vars = set()
389+
if var_name in self._local.vars:
390+
error_msg = "Environment variable '{}' recursively references itself (eventually)".format(var_name)
391+
raise ImproperlyConfigured(error_msg)
392+
393+
self._local.vars.add(var_name)
394+
try:
395+
return self._get_value(var_name, cast=cast, default=default, parse_default=parse_default)
396+
finally:
397+
self._local.vars.remove(var_name)
398+
399+
def _get_value(self, var_name, cast=None, default=NOTSET, parse_default=False):
376400
logger.debug("get '{}' casted as '{}' with default '{}'".format(
377-
var, cast, default
401+
var_name, cast, default
378402
))
379403

380-
var_name = "{}{}".format(self.prefix, var)
381404
if var_name in self.scheme:
382405
var_info = self.scheme[var_name]
383406

@@ -403,26 +426,28 @@ def get_value(self, var, cast=None, default=NOTSET, parse_default=False):
403426
value = self.ENVIRON[var_name]
404427
except KeyError as exc:
405428
if default is self.NOTSET:
406-
error_msg = "Set the {} environment variable".format(var)
429+
error_msg = "Set the {} environment variable".format(var_name)
407430
raise ImproperlyConfigured(error_msg) from exc
408431

409432
value = default
410433

434+
# Substitute environment variables
435+
if isinstance(value, (str, bytes)) and var_name != 'DJANGO_SECRET_KEY':
436+
def repl(m):
437+
return self.get_value(m['name'], cast=cast, default=default,
438+
parse_default=parse_default, add_prefix=False)
439+
value = self.VAR.sub(repl, value)
440+
value = expandvars(value)
441+
411442
# Resolve any proxied values
412443
prefix = b'$' if isinstance(value, bytes) else '$'
413444
escape = rb'\$' if isinstance(value, bytes) else r'\$'
414-
if hasattr(value, 'startswith') and value.startswith(prefix):
415-
value = value.lstrip(prefix)
416-
value = self.get_value(value, cast=cast, default=default)
417-
418445
if self.escape_proxy and hasattr(value, 'replace'):
419446
value = value.replace(escape, prefix)
420447

421448
# Smart casting
422-
if self.smart_cast:
423-
if cast is None and default is not None and \
424-
not isinstance(default, NoValue):
425-
cast = type(default)
449+
if self.smart_cast and cast is None and default is not None and not isinstance(default, NoValue):
450+
cast = type(default)
426451

427452
value = None if default is None and value == '' else value
428453

tests/test_embedded.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import pytest
2+
3+
from environ import Env, Path
4+
from environ.compat import ImproperlyConfigured
5+
6+
7+
class TestEmbedded:
8+
def setup_method(self, method):
9+
Env.ENVIRON = {}
10+
self.env = Env()
11+
self.env.read_env(Path(__file__, is_file=True)('test_embedded.txt'))
12+
13+
def test_substitution(self):
14+
assert self.env('HELLO') == 'Hello, world!'
15+
16+
def test_braces(self):
17+
assert self.env('BRACES') == 'Hello, world!'
18+
19+
def test_recursion(self):
20+
with pytest.raises(ImproperlyConfigured) as excinfo:
21+
self.env('RECURSIVE')
22+
assert str(excinfo.value) == "Environment variable 'RECURSIVE' recursively references itself (eventually)"
23+
24+
def test_transitive(self):
25+
with pytest.raises(ImproperlyConfigured) as excinfo:
26+
self.env('R4')
27+
assert str(excinfo.value) == "Environment variable 'R4' recursively references itself (eventually)"

tests/test_embedded.txt

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
VAR1='Hello'
2+
VAR2='world'
3+
HELLO="$VAR1, $VAR2!"
4+
BRACES="${VAR1}, ${VAR2}!"
5+
RECURSIVE="This variable is $RECURSIVE"
6+
R1="$R2"
7+
R2="$R3"
8+
R3="$R4"
9+
R4="$R1"

0 commit comments

Comments
 (0)