Skip to content

Commit 84d16c4

Browse files
alanjdstheskumar
authored andcommitted
Allows parse_dotenv(stream=StringIO()) (theskumar#78)
* allows `parse_dotenv(stream=StringIO()) * Test: parse_dotenv accepting a filelike stream * Docs on parse_dotenv(stream=...) * Oops. Forgot to commit the import * Py3 / Py2 compatibility * Docs compat on Py2/Py3
1 parent dc3ba36 commit 84d16c4

File tree

3 files changed

+63
-14
lines changed

3 files changed

+63
-14
lines changed

README.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,34 @@ Django
111111
If you are using django you should add the above loader script at the
112112
top of ``wsgi.py`` and ``manage.py``.
113113

114+
115+
In-memory filelikes
116+
-------------------
117+
118+
Is possible to not rely on the filesystem to parse filelikes from other sources
119+
(e.g. from a network storage). ``parse_dotenv`` accepts a filelike `stream`.
120+
Just be sure to rewind it before passing.
121+
122+
.. code:: python
123+
124+
from io import StringIO # Python2: from StringIO import StringIO
125+
from dotenv.main import parse_dotenv
126+
filelike = StringIO('SPAM=EGSS\n')
127+
filelike.seek(0)
128+
parsed = parse_dotenv(stream=filelike)
129+
130+
The returned lazy generator yields ``(key,value)`` tuples.
131+
To ease the consumption, is suggested to unpack it into a dictionary.
132+
133+
.. code:: python
134+
135+
os_env_like = dict(iter(parsed))
136+
assert os_env_like['SPAM'] == 'EGGS'
137+
138+
This could be useful if you need to *consume* the envfile but not *apply* it
139+
directly into the system environment.
140+
141+
114142
Installation
115143
============
116144

dotenv/main.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,24 +95,31 @@ def dotenv_values(dotenv_path):
9595
return values
9696

9797

98-
def parse_dotenv(dotenv_path):
99-
with open(dotenv_path) as f:
100-
for line in f:
101-
line = line.strip()
102-
if not line or line.startswith('#') or '=' not in line:
103-
continue
104-
k, v = line.split('=', 1)
98+
def parse_dotenv(dotenv_path=None, stream=None):
99+
if dotenv_path:
100+
f = open(dotenv_path)
101+
elif stream:
102+
f = stream
105103

106-
# Remove any leading and trailing spaces in key, value
107-
k, v = k.strip(), v.strip().encode('unicode-escape').decode('ascii')
104+
for line in f:
105+
line = line.strip()
106+
if not line or line.startswith('#') or '=' not in line:
107+
continue
108+
k, v = line.split('=', 1)
108109

109-
if len(v) > 0:
110-
quoted = v[0] == v[-1] in ['"', "'"]
110+
# Remove any leading and trailing spaces in key, value
111+
k, v = k.strip(), v.strip().encode('unicode-escape').decode('ascii')
111112

112-
if quoted:
113-
v = decode_escaped(v[1:-1])
113+
if len(v) > 0:
114+
quoted = v[0] == v[-1] in ['"', "'"]
114115

115-
yield k, v
116+
if quoted:
117+
v = decode_escaped(v[1:-1])
118+
119+
yield k, v
120+
121+
if dotenv_path:
122+
f.close()
116123

117124

118125
def resolve_nested_variables(values):

tests/test_core.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44
import tempfile
55
import warnings
66
import sh
7+
try:
8+
from StringIO import StringIO
9+
except ImportError:
10+
from io import StringIO
711

812
from dotenv import load_dotenv, find_dotenv, set_key
13+
from dotenv.main import parse_dotenv
914
from IPython.terminal.embed import InteractiveShellEmbed
1015

1116

@@ -110,3 +115,12 @@ def test_ipython_override():
110115
ipshell.magic("load_ext dotenv")
111116
ipshell.magic("dotenv -o")
112117
assert os.environ["MYNEWVALUE"] == 'q1w2e3'
118+
119+
120+
def test_parse_dotenv_stream():
121+
stream = StringIO('DOTENV=WORKS\n')
122+
stream.seek(0)
123+
parsed_generator = parse_dotenv(stream=stream)
124+
parsed_dict = dict(iter(parsed_generator))
125+
assert 'DOTENV' in parsed_dict
126+
assert parsed_dict['DOTENV'] == 'WORKS'

0 commit comments

Comments
 (0)