Repository: theskumar/python-dotenv
Pull request #220 (2020-01-14 10:37:46)

Support no value keys
Add support for variables with no value

Closes: #219 

ulyssessouza: Please note that I removed Python 3.4 on `.travis` to avoid incompatibility with `PyYAML` since it has [reached it's EOL since mid 2019](https://python-release-cycle.glitch.me/)
ulyssessouza: > Thank you for this work.

You are welcome!

> I'm curious: What functions or classes of this project would Docker-compose use?

The idea is to use `dotenv_values`. That works way better than our home brewed version. We had some bugs created relative to it's non-standard way to treat `.env` files.


bbc2: > The idea is to use dotenv_values. That works way better than our home brewed version. We had some bugs created relative to it's non-standard way to treat .env files.

OK good to know. `dotenv_values` should work well.  Parsing .env files is tough indeed, notably because there's no commonly-used specification for them.

Patch:
diff --git a/.travis.yml b/.travis.yml
index c8f56f5..5588c7f 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 6b7f947..5a1c325 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 @@ class DotEnv():
             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 @@ class DotEnv():
         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 @@ class DotEnv():
         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 resolve_nested_variables(values):
         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 resolve_nested_variables(values):
     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 dc8fe00..bd60cab 100644
--- a/src/dotenv/parser.py
+++ b/src/dotenv/parser.py
@@ -20,7 +20,7 @@ _whitespace = make_regex(r"\s*", extra_flags=re.MULTILINE)
 _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 aac1930..c19ed50 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -35,7 +35,7 @@ from dotenv.parser import Binding, Original, parse_stream
     (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 @@ from dotenv.parser import Binding, Original, parse_stream
         ],
     ),
     (
-        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)),
         ],
     ),