Repository: theskumar/python-dotenv (Python)

Issue:
Add support for variables with no value
In a quest for a `.env` lib for [docker-compose](https://github.com/docker/compose) we would like to integrate `python-dotenv` replacing the home brewed solution. 

But there is a feature missing, which is variables with no value like in [this test](https://github.com/docker/compose/blob/master/tests/fixtures/env/resolve.env) where the variables `ENV_DEF` and `NO_DEF` should be fetched from the environment (if possible).

So as return of `dotenv_values()`, that would be a key with value `None`. With this approach, the decision on what to do is up to the caller. In our case, we look for it in the current environment, fulfilled with the value found or suppressed if not present.


Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index c8f56f51..5588c7ff 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,10 +12,6 @@ matrix:
 
     - python: "2.7"
       env: TOXENV=py27
-    - python: "3.4"
-      env: TOXENV=py34
-    - python: "3.4"
-      env: TOXENV=py34-no-typing
     - python: "3.5"
       env: TOXENV=py35
     - python: "3.6"
diff --git a/src/dotenv/main.py b/src/dotenv/main.py
index 6b7f947d..5a1c3250 100644
--- a/src/dotenv/main.py
+++ b/src/dotenv/main.py
@@ -50,7 +50,7 @@ class DotEnv():
     def __init__(self, dotenv_path, verbose=False, encoding=None):
         # type: (Union[Text, _PathLike, _StringIO], bool, Union[None, Text]) -> None
         self.dotenv_path = dotenv_path  # type: Union[Text,_PathLike, _StringIO]
-        self._dict = None  # type: Optional[Dict[Text, Text]]
+        self._dict = None  # type: Optional[Dict[Text, Optional[Text]]]
         self.verbose = verbose  # type: bool
         self.encoding = encoding  # type: Union[None, Text]
 
@@ -68,7 +68,7 @@ def _get_stream(self):
             yield StringIO('')
 
     def dict(self):
-        # type: () -> Dict[Text, Text]
+        # type: () -> Dict[Text, Optional[Text]]
         """Return dotenv as dict"""
         if self._dict:
             return self._dict
@@ -78,10 +78,10 @@ def dict(self):
         return self._dict
 
     def parse(self):
-        # type: () -> Iterator[Tuple[Text, Text]]
+        # type: () -> Iterator[Tuple[Text, Optional[Text]]]
         with self._get_stream() as stream:
             for mapping in with_warn_for_invalid_lines(parse_stream(stream)):
-                if mapping.key is not None and mapping.value is not None:
+                if mapping.key is not None:
                     yield mapping.key, mapping.value
 
     def set_as_environment_variables(self, override=False):
@@ -92,7 +92,8 @@ def set_as_environment_variables(self, override=False):
         for k, v in self.dict().items():
             if k in os.environ and not override:
                 continue
-            os.environ[to_env(k)] = to_env(v)
+            if v is not None:
+                os.environ[to_env(k)] = to_env(v)
 
         return True
 
@@ -197,7 +198,7 @@ def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
 
 
 def resolve_nested_variables(values):
-    # type: (Dict[Text, Text]) -> Dict[Text, Text]
+    # type: (Dict[Text, Optional[Text]]) -> Dict[Text, Optional[Text]]
     def _replacement(name):
         # type: (Text) -> Text
         """
@@ -206,7 +207,7 @@ def _replacement(name):
         then look into the dotenv variables
         """
         ret = os.getenv(name, new_values.get(name, ""))
-        return ret
+        return ret  # type: ignore
 
     def _re_sub_callback(match_object):
         # type: (Match[Text]) -> Text
@@ -219,7 +220,7 @@ def _re_sub_callback(match_object):
     new_values = {}
 
     for k, v in values.items():
-        new_values[k] = __posix_variable.sub(_re_sub_callback, v)
+        new_values[k] = __posix_variable.sub(_re_sub_callback, v) if v is not None else None
 
     return new_values
 
@@ -301,6 +302,6 @@ def load_dotenv(dotenv_path=None, stream=None, verbose=False, override=False, **
 
 
 def dotenv_values(dotenv_path=None, stream=None, verbose=False, **kwargs):
-    # type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, Union[None, Text]) -> Dict[Text, Text]
+    # type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, Union[None, Text]) -> Dict[Text, Optional[Text]]
     f = dotenv_path or stream or find_dotenv()
     return DotEnv(f, verbose=verbose, **kwargs).dict()
diff --git a/src/dotenv/parser.py b/src/dotenv/parser.py
index dc8fe008..bd60cab4 100644
--- a/src/dotenv/parser.py
+++ b/src/dotenv/parser.py
@@ -20,7 +20,7 @@ def make_regex(string, extra_flags=0):
 _export = make_regex(r"(?:export[^\S\r\n]+)?")
 _single_quoted_key = make_regex(r"'([^']+)'")
 _unquoted_key = make_regex(r"([^=\#\s]+)")
-_equal_sign = make_regex(r"[^\S\r\n]*=[^\S\r\n]*")
+_equal_sign = make_regex(r"([^\S\r\n]*=[^\S\r\n]*)?")
 _single_quoted_value = make_regex(r"'((?:\\'|[^'])*)'")
 _double_quoted_value = make_regex(r'"((?:\\"|[^"])*)"')
 _unquoted_value_part = make_regex(r"([^ \r\n]*)")
@@ -194,8 +194,8 @@ def parse_binding(reader):
         reader.read_regex(_whitespace)
         reader.read_regex(_export)
         key = parse_key(reader)
-        reader.read_regex(_equal_sign)
-        value = parse_value(reader)
+        (sign,) = reader.read_regex(_equal_sign)
+        value = parse_value(reader) if sign else None
         reader.read_regex(_comment)
         reader.read_regex(_end_of_line)
         return Binding(


Test patch:
diff --git a/tests/test_parser.py b/tests/test_parser.py
index aac19302..c19ed502 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -35,7 +35,7 @@
     (u"a='b\\'c'", [Binding(key=u"a", value=u"b'c", original=Original(string=u"a='b\\'c'", line=1))]),
     (u"a=à", [Binding(key=u"a", value=u"à", original=Original(string=u"a=à", line=1))]),
     (u'a="à"', [Binding(key=u"a", value=u"à", original=Original(string=u'a="à"', line=1))]),
-    (u'garbage', [Binding(key=None, value=None, original=Original(string=u"garbage", line=1))]),
+    (u'no_value_var', [Binding(key=u'no_value_var', value=None, original=Original(string=u"no_value_var", line=1))]),
     (
         u"a=b\nc=d",
         [
@@ -87,9 +87,11 @@
         ],
     ),
     (
-        u'garbage[%$#\na=b',
+        u'uglyKey[%$=\"S3cr3t_P4ssw#rD\" #\na=b',
         [
-            Binding(key=None, value=None, original=Original(string=u"garbage[%$#\n", line=1)),
+            Binding(key=u'uglyKey[%$',
+                    value=u'S3cr3t_P4ssw#rD',
+                    original=Original(string=u"uglyKey[%$=\"S3cr3t_P4ssw#rD\" #\n", line=1)),
             Binding(key=u"a", value=u"b", original=Original(string=u'a=b', line=2)),
         ],
     ),