Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'