Skip to content

Commit 4d8aa19

Browse files
committed
Print a warning on malformed line
If a line can't be parsed (e.g. `FOO: BAR` is invalid), it is printed as a warning on stderr. As usual, after such an error, Python-dotenv tries to parse the rest of the file and the application can use the successfully parsed variable bindings.
1 parent f0b54b2 commit 4d8aa19

3 files changed

Lines changed: 138 additions & 63 deletions

File tree

‎src/dotenv/main.py‎

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@
22
from __future__ import absolute_import, print_function, unicode_literals
33

44
import io
5+
import logging
56
import os
67
import re
78
import shutil
89
import sys
910
import tempfile
10-
from typing import (Dict, Iterator, List, Match, Optional, # noqa
11+
from typing import (Dict, Iterator, Match, Optional, # noqa
1112
Pattern, Union, TYPE_CHECKING, Text, IO, Tuple)
1213
import warnings
1314
from collections import OrderedDict
1415
from contextlib import contextmanager
1516

1617
from .compat import StringIO, PY2, to_env
17-
from .parser import parse_stream
18+
from .parser import Binding, parse_stream
19+
20+
logger = logging.getLogger(__name__)
1821

1922
if TYPE_CHECKING: # pragma: no cover
2023
if sys.version_info >= (3, 6):
@@ -30,6 +33,17 @@
3033
__posix_variable = re.compile(r'\$\{[^\}]*\}') # type: Pattern[Text]
3134

3235

36+
def with_warn_for_invalid_lines(mappings):
37+
# type: (Iterator[Binding]) -> Iterator[Binding]
38+
for mapping in mappings:
39+
if mapping.key is None or mapping.value is None:
40+
logger.warning(
41+
"Python-dotenv could not parse statement starting at line %s",
42+
mapping.original.line,
43+
)
44+
yield mapping
45+
46+
3347
class DotEnv():
3448

3549
def __init__(self, dotenv_path, verbose=False, encoding=None):
@@ -65,7 +79,7 @@ def dict(self):
6579
def parse(self):
6680
# type: () -> Iterator[Tuple[Text, Text]]
6781
with self._get_stream() as stream:
68-
for mapping in parse_stream(stream):
82+
for mapping in with_warn_for_invalid_lines(parse_stream(stream)):
6983
if mapping.key is not None and mapping.value is not None:
7084
yield mapping.key, mapping.value
7185

@@ -142,12 +156,12 @@ def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always"):
142156

143157
with rewrite(dotenv_path) as (source, dest):
144158
replaced = False
145-
for mapping in parse_stream(source):
159+
for mapping in with_warn_for_invalid_lines(parse_stream(source)):
146160
if mapping.key == key_to_set:
147161
dest.write(line_out)
148162
replaced = True
149163
else:
150-
dest.write(mapping.original)
164+
dest.write(mapping.original.string)
151165
if not replaced:
152166
dest.write(line_out)
153167

@@ -168,11 +182,11 @@ def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
168182

169183
removed = False
170184
with rewrite(dotenv_path) as (source, dest):
171-
for mapping in parse_stream(source):
185+
for mapping in with_warn_for_invalid_lines(parse_stream(source)):
172186
if mapping.key == key_to_unset:
173187
removed = True
174188
else:
175-
dest.write(mapping.original)
189+
dest.write(mapping.original.string)
176190

177191
if not removed:
178192
warnings.warn("key %s not removed from %s - key doesn't exist." % (key_to_unset, dotenv_path)) # type: ignore

‎src/dotenv/parser.py‎

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def make_regex(string, extra_flags=0):
1111
return re.compile(to_text(string), re.UNICODE | extra_flags)
1212

1313

14+
_newline = make_regex(r"(\r\n|\n|\r)")
1415
_whitespace = make_regex(r"\s*", extra_flags=re.MULTILINE)
1516
_export = make_regex(r"(?:export[^\S\r\n]+)?")
1617
_single_quoted_key = make_regex(r"'([^']+)'")
@@ -25,9 +26,46 @@ def make_regex(string, extra_flags=0):
2526
_double_quote_escapes = make_regex(r"\\[\\'\"abfnrtv]")
2627
_single_quote_escapes = make_regex(r"\\[\\']")
2728

28-
Binding = NamedTuple("Binding", [("key", Optional[Text]),
29-
("value", Optional[Text]),
30-
("original", Text)])
29+
30+
class Position:
31+
def __init__(self, chars, line):
32+
# type: (int, int) -> None
33+
self.chars = chars
34+
self.line = line
35+
36+
@classmethod
37+
def start(cls):
38+
# type: () -> Position
39+
return cls(chars=0, line=1)
40+
41+
def set(self, other):
42+
# type: (Position) -> None
43+
self.chars = other.chars
44+
self.line = other.line
45+
46+
def advance(self, string):
47+
# type: (Text) -> None
48+
self.chars += len(string)
49+
self.line += len(re.findall(_newline, string))
50+
51+
52+
Original = NamedTuple(
53+
"Original",
54+
[
55+
("string", Text),
56+
("line", int),
57+
],
58+
)
59+
60+
61+
Binding = NamedTuple(
62+
"Binding",
63+
[
64+
("key", Optional[Text]),
65+
("value", Optional[Text]),
66+
("original", Original),
67+
],
68+
)
3169

3270

3371
class Error(Exception):
@@ -38,39 +76,42 @@ class Reader:
3876
def __init__(self, stream):
3977
# type: (IO[Text]) -> None
4078
self.string = stream.read()
41-
self.position = 0
42-
self.mark = 0
79+
self.position = Position.start()
80+
self.mark = Position.start()
4381

4482
def has_next(self):
4583
# type: () -> bool
46-
return self.position < len(self.string)
84+
return self.position.chars < len(self.string)
4785

4886
def set_mark(self):
4987
# type: () -> None
50-
self.mark = self.position
88+
self.mark.set(self.position)
5189

5290
def get_marked(self):
53-
# type: () -> Text
54-
return self.string[self.mark:self.position]
91+
# type: () -> Original
92+
return Original(
93+
string=self.string[self.mark.chars:self.position.chars],
94+
line=self.mark.line,
95+
)
5596

5697
def peek(self, count):
5798
# type: (int) -> Text
58-
return self.string[self.position:self.position + count]
99+
return self.string[self.position.chars:self.position.chars + count]
59100

60101
def read(self, count):
61102
# type: (int) -> Text
62-
result = self.string[self.position:self.position + count]
103+
result = self.string[self.position.chars:self.position.chars + count]
63104
if len(result) < count:
64105
raise Error("read: End of string")
65-
self.position += count
106+
self.position.advance(result)
66107
return result
67108

68109
def read_regex(self, regex):
69110
# type: (Pattern[Text]) -> Sequence[Text]
70-
match = regex.match(self.string, self.position)
111+
match = regex.match(self.string, self.position.chars)
71112
if match is None:
72113
raise Error("read_regex: Pattern not found")
73-
self.position = match.end()
114+
self.position.advance(self.string[match.start():match.end()])
74115
return match.groups()
75116

76117

@@ -131,10 +172,18 @@ def parse_binding(reader):
131172
value = parse_value(reader)
132173
reader.read_regex(_comment)
133174
reader.read_regex(_end_of_line)
134-
return Binding(key=key, value=value, original=reader.get_marked())
175+
return Binding(
176+
key=key,
177+
value=value,
178+
original=reader.get_marked(),
179+
)
135180
except Error:
136181
reader.read_regex(_rest_of_line)
137-
return Binding(key=None, value=None, original=reader.get_marked())
182+
return Binding(
183+
key=None,
184+
value=None,
185+
original=reader.get_marked(),
186+
)
138187

139188

140189
def parse_stream(stream):

‎tests/test_parser.py‎

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,83 +2,95 @@
22
import pytest
33

44
from dotenv.compat import StringIO
5-
from dotenv.parser import Binding, parse_stream
5+
from dotenv.parser import Binding, Original, parse_stream
66

77

88
@pytest.mark.parametrize("test_input,expected", [
99
(u"", []),
10-
(u"a=b", [Binding(key=u"a", value=u"b", original=u"a=b")]),
11-
(u"'a'=b", [Binding(key=u"a", value=u"b", original=u"'a'=b")]),
12-
(u"[=b", [Binding(key=u"[", value=u"b", original=u"[=b")]),
13-
(u" a = b ", [Binding(key=u"a", value=u"b", original=u" a = b ")]),
14-
(u"export a=b", [Binding(key=u"a", value=u"b", original=u"export a=b")]),
15-
(u" export 'a'=b", [Binding(key=u"a", value=u"b", original=u" export 'a'=b")]),
16-
(u"# a=b", [Binding(key=None, value=None, original=u"# a=b")]),
17-
(u"a=b#c", [Binding(key=u"a", value=u"b#c", original=u"a=b#c")]),
18-
(u'a=b # comment', [Binding(key=u"a", value=u"b", original=u"a=b # comment")]),
19-
(u"a=b space ", [Binding(key=u"a", value=u"b space", original=u"a=b space ")]),
20-
(u"a='b space '", [Binding(key=u"a", value=u"b space ", original=u"a='b space '")]),
21-
(u'a="b space "', [Binding(key=u"a", value=u"b space ", original=u'a="b space "')]),
22-
(u"export export_a=1", [Binding(key=u"export_a", value=u"1", original=u"export export_a=1")]),
23-
(u"export port=8000", [Binding(key=u"port", value=u"8000", original=u"export port=8000")]),
24-
(u'a="b\nc"', [Binding(key=u"a", value=u"b\nc", original=u'a="b\nc"')]),
25-
(u"a='b\nc'", [Binding(key=u"a", value=u"b\nc", original=u"a='b\nc'")]),
26-
(u'a="b\nc"', [Binding(key=u"a", value=u"b\nc", original=u'a="b\nc"')]),
27-
(u'a="b\\nc"', [Binding(key=u"a", value=u'b\nc', original=u'a="b\\nc"')]),
28-
(u"a='b\\nc'", [Binding(key=u"a", value=u'b\\nc', original=u"a='b\\nc'")]),
29-
(u'a="b\\"c"', [Binding(key=u"a", value=u'b"c', original=u'a="b\\"c"')]),
30-
(u"a='b\\'c'", [Binding(key=u"a", value=u"b'c", original=u"a='b\\'c'")]),
31-
(u"a=à", [Binding(key=u"a", value=u"à", original=u"a=à")]),
32-
(u'a="à"', [Binding(key=u"a", value=u"à", original=u'a="à"')]),
33-
(u'garbage', [Binding(key=None, value=None, original=u"garbage")]),
10+
(u"a=b", [Binding(key=u"a", value=u"b", original=Original(string=u"a=b", line=1))]),
11+
(u"'a'=b", [Binding(key=u"a", value=u"b", original=Original(string=u"'a'=b", line=1))]),
12+
(u"[=b", [Binding(key=u"[", value=u"b", original=Original(string=u"[=b", line=1))]),
13+
(u" a = b ", [Binding(key=u"a", value=u"b", original=Original(string=u" a = b ", line=1))]),
14+
(u"export a=b", [Binding(key=u"a", value=u"b", original=Original(string=u"export a=b", line=1))]),
15+
(u" export 'a'=b", [Binding(key=u"a", value=u"b", original=Original(string=u" export 'a'=b", line=1))]),
16+
(u"# a=b", [Binding(key=None, value=None, original=Original(string=u"# a=b", line=1))]),
17+
(u"a=b#c", [Binding(key=u"a", value=u"b#c", original=Original(string=u"a=b#c", line=1))]),
18+
(u'a=b # comment', [Binding(key=u"a", value=u"b", original=Original(string=u"a=b # comment", line=1))]),
19+
(u"a=b space ", [Binding(key=u"a", value=u"b space", original=Original(string=u"a=b space ", line=1))]),
20+
(u"a='b space '", [Binding(key=u"a", value=u"b space ", original=Original(string=u"a='b space '", line=1))]),
21+
(u'a="b space "', [Binding(key=u"a", value=u"b space ", original=Original(string=u'a="b space "', line=1))]),
22+
(
23+
u"export export_a=1",
24+
[
25+
Binding(key=u"export_a", value=u"1", original=Original(string=u"export export_a=1", line=1))
26+
],
27+
),
28+
(u"export port=8000", [Binding(key=u"port", value=u"8000", original=Original(string=u"export port=8000", line=1))]),
29+
(u'a="b\nc"', [Binding(key=u"a", value=u"b\nc", original=Original(string=u'a="b\nc"', line=1))]),
30+
(u"a='b\nc'", [Binding(key=u"a", value=u"b\nc", original=Original(string=u"a='b\nc'", line=1))]),
31+
(u'a="b\nc"', [Binding(key=u"a", value=u"b\nc", original=Original(string=u'a="b\nc"', line=1))]),
32+
(u'a="b\\nc"', [Binding(key=u"a", value=u'b\nc', original=Original(string=u'a="b\\nc"', line=1))]),
33+
(u"a='b\\nc'", [Binding(key=u"a", value=u'b\\nc', original=Original(string=u"a='b\\nc'", line=1))]),
34+
(u'a="b\\"c"', [Binding(key=u"a", value=u'b"c', original=Original(string=u'a="b\\"c"', line=1))]),
35+
(u"a='b\\'c'", [Binding(key=u"a", value=u"b'c", original=Original(string=u"a='b\\'c'", line=1))]),
36+
(u"a=à", [Binding(key=u"a", value=u"à", original=Original(string=u"a=à", line=1))]),
37+
(u'a="à"', [Binding(key=u"a", value=u"à", original=Original(string=u'a="à"', line=1))]),
38+
(u'garbage', [Binding(key=None, value=None, original=Original(string=u"garbage", line=1))]),
3439
(
3540
u"a=b\nc=d",
3641
[
37-
Binding(key=u"a", value=u"b", original=u"a=b\n"),
38-
Binding(key=u"c", value=u"d", original=u"c=d"),
42+
Binding(key=u"a", value=u"b", original=Original(string=u"a=b\n", line=1)),
43+
Binding(key=u"c", value=u"d", original=Original(string=u"c=d", line=2)),
44+
],
45+
),
46+
(
47+
u"a=b\rc=d",
48+
[
49+
Binding(key=u"a", value=u"b", original=Original(string=u"a=b\r", line=1)),
50+
Binding(key=u"c", value=u"d", original=Original(string=u"c=d", line=2)),
3951
],
4052
),
4153
(
4254
u"a=b\r\nc=d",
4355
[
44-
Binding(key=u"a", value=u"b", original=u"a=b\r\n"),
45-
Binding(key=u"c", value=u"d", original=u"c=d"),
56+
Binding(key=u"a", value=u"b", original=Original(string=u"a=b\r\n", line=1)),
57+
Binding(key=u"c", value=u"d", original=Original(string=u"c=d", line=2)),
4658
],
4759
),
4860
(
4961
u'a=\nb=c',
5062
[
51-
Binding(key=u"a", value=u'', original=u'a=\n'),
52-
Binding(key=u"b", value=u'c', original=u"b=c"),
63+
Binding(key=u"a", value=u'', original=Original(string=u'a=\n', line=1)),
64+
Binding(key=u"b", value=u'c', original=Original(string=u"b=c", line=2)),
5365
]
5466
),
5567
(
5668
u'a=b\n\nc=d',
5769
[
58-
Binding(key=u"a", value=u"b", original=u"a=b\n"),
59-
Binding(key=u"c", value=u"d", original=u"\nc=d"),
70+
Binding(key=u"a", value=u"b", original=Original(string=u"a=b\n", line=1)),
71+
Binding(key=u"c", value=u"d", original=Original(string=u"\nc=d", line=2)),
6072
]
6173
),
6274
(
6375
u'a="\nb=c',
6476
[
65-
Binding(key=None, value=None, original=u'a="\n'),
66-
Binding(key=u"b", value=u"c", original=u"b=c"),
77+
Binding(key=None, value=None, original=Original(string=u'a="\n', line=1)),
78+
Binding(key=u"b", value=u"c", original=Original(string=u"b=c", line=2)),
6779
]
6880
),
6981
(
7082
u'# comment\na="b\nc"\nd=e\n',
7183
[
72-
Binding(key=None, value=None, original=u"# comment\n"),
73-
Binding(key=u"a", value=u"b\nc", original=u'a="b\nc"\n'),
74-
Binding(key=u"d", value=u"e", original=u"d=e\n"),
84+
Binding(key=None, value=None, original=Original(string=u"# comment\n", line=1)),
85+
Binding(key=u"a", value=u"b\nc", original=Original(string=u'a="b\nc"\n', line=2)),
86+
Binding(key=u"d", value=u"e", original=Original(string=u"d=e\n", line=4)),
7587
],
7688
),
7789
(
7890
u'garbage[%$#\na=b',
7991
[
80-
Binding(key=None, value=None, original=u"garbage[%$#\n"),
81-
Binding(key=u"a", value=u"b", original=u'a=b'),
92+
Binding(key=None, value=None, original=Original(string=u"garbage[%$#\n", line=1)),
93+
Binding(key=u"a", value=u"b", original=Original(string=u'a=b', line=2)),
8294
],
8395
),
8496
])

0 commit comments

Comments
 (0)