Skip to content
28 changes: 28 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,34 @@ Django
If you are using django you should add the above loader script at the
top of ``wsgi.py`` and ``manage.py``.


In-memory filelikes
-------------------

Is possible to not rely on the filesystem to parse filelikes from other sources
(e.g. from a network storage). ``parse_dotenv`` accepts a filelike `stream`.
Just be sure to rewind it before passing.

.. code:: python

from io import StringIO # Python2: from StringIO import StringIO
from dotenv.main import parse_dotenv
filelike = StringIO('SPAM=EGSS\n')
filelike.seek(0)
parsed = parse_dotenv(stream=filelike)

The returned lazy generator yields ``(key,value)`` tuples.
To ease the consumption, is suggested to unpack it into a dictionary.

.. code:: python

os_env_like = dict(iter(parsed))
assert os_env_like['SPAM'] == 'EGGS'

This could be useful if you need to *consume* the envfile but not *apply* it
directly into the system environment.


Installation
============

Expand Down
35 changes: 21 additions & 14 deletions dotenv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,24 +95,31 @@ def dotenv_values(dotenv_path):
return values


def parse_dotenv(dotenv_path):
with open(dotenv_path) as f:
for line in f:
line = line.strip()
if not line or line.startswith('#') or '=' not in line:
continue
k, v = line.split('=', 1)
def parse_dotenv(dotenv_path=None, stream=None):
if dotenv_path:
f = open(dotenv_path)
elif stream:
f = stream

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

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

if quoted:
v = decode_escaped(v[1:-1])
if len(v) > 0:
quoted = v[0] == v[-1] in ['"', "'"]

yield k, v
if quoted:
v = decode_escaped(v[1:-1])

yield k, v

if dotenv_path:
f.close()


def resolve_nested_variables(values):
Expand Down
14 changes: 14 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
import tempfile
import warnings
import sh
try:
from StringIO import StringIO
except ImportError:
from io import StringIO

from dotenv import load_dotenv, find_dotenv, set_key
from dotenv.main import parse_dotenv
from IPython.terminal.embed import InteractiveShellEmbed


Expand Down Expand Up @@ -110,3 +115,12 @@ def test_ipython_override():
ipshell.magic("load_ext dotenv")
ipshell.magic("dotenv -o")
assert os.environ["MYNEWVALUE"] == 'q1w2e3'


def test_parse_dotenv_stream():
stream = StringIO('DOTENV=WORKS\n')
stream.seek(0)
parsed_generator = parse_dotenv(stream=stream)
parsed_dict = dict(iter(parsed_generator))
assert 'DOTENV' in parsed_dict
assert parsed_dict['DOTENV'] == 'WORKS'