Repository: theskumar/python-dotenv
Pull request #78 (2017-12-09 18:01:56)

Allows `parse_dotenv(stream=StringIO())`
Backwards-compatible API for `parse_dotenv()` that allows a stream having a dotfile to be parsed.

The other option would be to save my already-existing stream in disk and providing the filepath, but this could have security and performance considerations.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/14580763/badge)](https://coveralls.io/builds/14580763)

Coverage decreased (-0.6%) to 84.653% when pulling **ec6f70d3e6401a6a1cad742ec8f98bfd3c67059b on alanjds:parse_stream** into **ef2ee9d7e2204fe16de0a535faaee00897e2a210 on theskumar:master**.

theskumar: This looks good. `parse_dotenv` is not considered a public api as of yet, but I think it's high time we do so.

Some tests are failing due to installation flake8 issues on travis for python 3.4, that I'll fix up.

Would it be possible for you to add some test cases and related documentation as well, for this so others can followup and use this functionality. 
alanjds: Sure. But not "right now".

Probable after the new year I can get back to work in tests/docs.

Thanks for reviewing it.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/15056792/badge)](https://coveralls.io/builds/15056792)

Coverage increased (+0.4%) to 85.644% when pulling **6f2fb0b5965c2959c3f8e66be153474c91549688 on alanjds:parse_stream** into **6eb169800718c9ce86c11871027327490742eb04 on theskumar:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/15056945/badge)](https://coveralls.io/builds/15056945)

Coverage increased (+0.4%) to 85.644% when pulling **6c5c83af5816ec1d1999d53dd4ce1f550c2b226b on alanjds:parse_stream** into **6eb169800718c9ce86c11871027327490742eb04 on theskumar:master**.

alanjds: Docs and Tests included.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/15057020/badge)](https://coveralls.io/builds/15057020)

Coverage increased (+0.4%) to 85.644% when pulling **4283f126650b51a2b9c8b1dae2ff85f7a0aa8ff0 on alanjds:parse_stream** into **6eb169800718c9ce86c11871027327490742eb04 on theskumar:master**.


Patch:
diff --git a/.pyup.yml b/.pyup.yml
new file mode 100644
index 0000000..604f69f
--- /dev/null
+++ b/.pyup.yml
@@ -0,0 +1,4 @@
+# autogenerated pyup.io config file 
+# see https://pyup.io/docs/configuration/ for all available options
+
+update: insecure
diff --git a/README.rst b/README.rst
index 125fb1c..52a52fe 100644
--- a/README.rst
+++ b/README.rst
@@ -91,6 +91,7 @@ ignored.
     SOME_VAR=someval
     # I am a comment and that is OK
     FOO="BAR"
+    MULTILINE_VAR="hello\nworld"
 
 ``.env`` can interpolate variables using POSIX variable expansion, variables
 are replaced from the environment first or from other values in the ``.env``
@@ -110,6 +111,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
 ============
 
diff --git a/dotenv/__init__.py b/dotenv/__init__.py
index fa06a0b..e766351 100644
--- a/dotenv/__init__.py
+++ b/dotenv/__init__.py
@@ -1,8 +1,10 @@
 from .cli import get_cli_string
 from .main import load_dotenv, get_key, set_key, unset_key, find_dotenv
-try:
-    from .ipython import load_ipython_extension
-except ImportError:
-    pass
+
 
 __all__ = ['get_cli_string', 'load_dotenv', 'get_key', 'set_key', 'unset_key', 'find_dotenv', 'load_ipython_extension']
+
+
+def load_ipython_extension(ipython):
+    from .ipython import load_ipython_extension
+    load_ipython_extension(ipython)
diff --git a/dotenv/main.py b/dotenv/main.py
index 37a3c05..9b1efe9 100644
--- a/dotenv/main.py
+++ b/dotenv/main.py
@@ -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):


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index 4e9adda..2b0215c 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -4,8 +4,13 @@ import pytest
 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
 
 
@@ -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'