-
Notifications
You must be signed in to change notification settings - Fork 42
/
dotenv.py
118 lines (92 loc) · 3.58 KB
/
dotenv.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import os
import re
import sys
import warnings
__version__ = '1.4.2'
line_re = re.compile(r"""
^
(?:export\s+)? # optional export
([\w\.]+) # key
(?:\s*=\s*|:\s+?) # separator
( # optional value begin
'(?:\'|[^'])*' # single quoted value
| # or
"(?:\"|[^"])*" # double quoted value
| # or
[^#\n]+ # unquoted value
)? # value end
(?:\s*\#.*)? # optional comment
$
""", re.VERBOSE)
variable_re = re.compile(r"""
(\\)? # is it escaped with a backslash?
(\$) # literal $
( # collect braces with var for sub
\{? # allow brace wrapping
([A-Z0-9_]+) # match the variable
\}? # closing brace
) # braces end
""", re.IGNORECASE | re.VERBOSE)
def read_dotenv(dotenv=None, override=False):
"""
Read a .env file into os.environ.
If not given a path to a dotenv path, does filthy magic stack backtracking
to find manage.py and then find the dotenv.
If tests rely on .env files, setting the overwrite flag to True is a safe
way to ensure tests run consistently across all environments.
:param override: True if values in .env should override system variables.
"""
if dotenv is None:
frame_filename = sys._getframe().f_back.f_code.co_filename
dotenv = os.path.join(os.path.dirname(frame_filename), '.env')
if os.path.isdir(dotenv) and os.path.isfile(os.path.join(dotenv, '.env')):
dotenv = os.path.join(dotenv, '.env')
if os.path.exists(dotenv):
with open(dotenv) as f:
for k, v in parse_dotenv(f.read()).items():
if override:
os.environ[k] = v
else:
os.environ.setdefault(k, v)
else:
warnings.warn("Not reading {0} - it doesn't exist.".format(dotenv),
stacklevel=2)
def parse_dotenv(content):
env = {}
for line in content.splitlines():
m1 = line_re.search(line)
if m1:
key, value = m1.groups()
if value is None:
value = ''
# Remove leading/trailing whitespace
value = value.strip()
# Remove surrounding quotes
m2 = re.match(r'^([\'"])(.*)\1$', value)
if m2:
quotemark, value = m2.groups()
else:
quotemark = None
# Unescape all chars except $ so variables can be escaped properly
if quotemark == '"':
value = re.sub(r'\\([^$])', r'\1', value)
if quotemark != "'":
# Substitute variables in a value
for parts in variable_re.findall(value):
if parts[0] == '\\':
# Variable is escaped, don't replace it
replace = ''.join(parts[1:-1])
else:
# Replace it with the value from the environment
replace = env.get(
parts[-1],
os.environ.get(parts[-1], '')
)
value = value.replace(''.join(parts[0:-1]), replace)
env[key] = value
elif not re.search(r'^\s*(?:#.*)?$', line): # not comment or blank
warnings.warn(
"Line {0} doesn't match format".format(repr(line)),
SyntaxWarning
)
return env