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: 21 additions & 7 deletions src/dotenv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import absolute_import, print_function, unicode_literals

import io
import logging
import os
import re
import shutil
Expand All @@ -11,8 +12,10 @@
from collections import OrderedDict
from contextlib import contextmanager

from .compat import StringIO, PY2, to_env, IS_TYPE_CHECKING
from .parser import parse_stream
from .compat import IS_TYPE_CHECKING, PY2, StringIO, to_env
from .parser import Binding, parse_stream

logger = logging.getLogger(__name__)

if IS_TYPE_CHECKING:
from typing import (
Expand All @@ -31,6 +34,17 @@
__posix_variable = re.compile(r'\$\{[^\}]*\}') # type: Pattern[Text]


def with_warn_for_invalid_lines(mappings):
# type: (Iterator[Binding]) -> Iterator[Binding]
for mapping in mappings:
if mapping.key is None or mapping.value is None:
logger.warning(
"Python-dotenv could not parse statement starting at line %s",
mapping.original.line,
)
yield mapping


class DotEnv():

def __init__(self, dotenv_path, verbose=False, encoding=None):
Expand Down Expand Up @@ -66,7 +80,7 @@ def dict(self):
def parse(self):
# type: () -> Iterator[Tuple[Text, Text]]
with self._get_stream() as stream:
for mapping in parse_stream(stream):
for mapping in with_warn_for_invalid_lines(parse_stream(stream)):
if mapping.key is not None and mapping.value is not None:
yield mapping.key, mapping.value

Expand Down Expand Up @@ -143,12 +157,12 @@ def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always"):

with rewrite(dotenv_path) as (source, dest):
replaced = False
for mapping in parse_stream(source):
for mapping in with_warn_for_invalid_lines(parse_stream(source)):
if mapping.key == key_to_set:
dest.write(line_out)
replaced = True
else:
dest.write(mapping.original)
dest.write(mapping.original.string)
if not replaced:
dest.write(line_out)

Expand All @@ -169,11 +183,11 @@ def unset_key(dotenv_path, key_to_unset, quote_mode="always"):

removed = False
with rewrite(dotenv_path) as (source, dest):
for mapping in parse_stream(source):
for mapping in with_warn_for_invalid_lines(parse_stream(source)):
if mapping.key == key_to_unset:
removed = True
else:
dest.write(mapping.original)
dest.write(mapping.original.string)

if not removed:
warnings.warn("key %s not removed from %s - key doesn't exist." % (key_to_unset, dotenv_path)) # type: ignore
Expand Down
108 changes: 82 additions & 26 deletions src/dotenv/parser.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import codecs
import re

from .compat import to_text, IS_TYPE_CHECKING

from .compat import IS_TYPE_CHECKING, to_text

if IS_TYPE_CHECKING:
from typing import ( # noqa:F401
Expand All @@ -16,6 +15,7 @@ def make_regex(string, extra_flags=0):
return re.compile(to_text(string), re.UNICODE | extra_flags)


_newline = make_regex(r"(\r\n|\n|\r)")
_whitespace = make_regex(r"\s*", extra_flags=re.MULTILINE)
_export = make_regex(r"(?:export[^\S\r\n]+)?")
_single_quoted_key = make_regex(r"'([^']+)'")
Expand All @@ -36,14 +36,62 @@ def make_regex(string, extra_flags=0):
# when we are type checking, and the linter is upset if we
# re-import
import typing
Binding = typing.NamedTuple("Binding", [("key", typing.Optional[typing.Text]),
("value", typing.Optional[typing.Text]),
("original", typing.Text)])

Original = typing.NamedTuple(
"Original",
[
("string", typing.Text),
("line", int),
],
)

Binding = typing.NamedTuple(
"Binding",
[
("key", typing.Optional[typing.Text]),
("value", typing.Optional[typing.Text]),
("original", Original),
],
)
except ImportError:
from collections import namedtuple
Binding = namedtuple("Binding", ["key", # type: ignore
"value",
"original"]) # type: Tuple[Optional[Text], Optional[Text], Text]
Original = namedtuple( # type: ignore
"Original",
[
"string",
"line",
],
)
Binding = namedtuple( # type: ignore
"Binding",
[
"key",
"value",
"original",
],
)


class Position:
def __init__(self, chars, line):
# type: (int, int) -> None
self.chars = chars
self.line = line

@classmethod
def start(cls):
# type: () -> Position
return cls(chars=0, line=1)

def set(self, other):
# type: (Position) -> None
self.chars = other.chars
self.line = other.line

def advance(self, string):
# type: (Text) -> None
self.chars += len(string)
self.line += len(re.findall(_newline, string))


class Error(Exception):
Expand All @@ -54,39 +102,42 @@ class Reader:
def __init__(self, stream):
# type: (IO[Text]) -> None
self.string = stream.read()
self.position = 0
self.mark = 0
self.position = Position.start()
self.mark = Position.start()

def has_next(self):
# type: () -> bool
return self.position < len(self.string)
return self.position.chars < len(self.string)

def set_mark(self):
# type: () -> None
self.mark = self.position
self.mark.set(self.position)

def get_marked(self):
# type: () -> Text
return self.string[self.mark:self.position]
# type: () -> Original
return Original(
string=self.string[self.mark.chars:self.position.chars],
line=self.mark.line,
)

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

def read(self, count):
# type: (int) -> Text
result = self.string[self.position:self.position + count]
result = self.string[self.position.chars:self.position.chars + count]
if len(result) < count:
raise Error("read: End of string")
self.position += count
self.position.advance(result)
return result

def read_regex(self, regex):
# type: (Pattern[Text]) -> Sequence[Text]
match = regex.match(self.string, self.position)
match = regex.match(self.string, self.position.chars)
if match is None:
raise Error("read_regex: Pattern not found")
self.position = match.end()
self.position.advance(self.string[match.start():match.end()])
return match.groups()


Expand Down Expand Up @@ -147,17 +198,22 @@ def parse_binding(reader):
value = parse_value(reader)
reader.read_regex(_comment)
reader.read_regex(_end_of_line)
return Binding(key=key, value=value, original=reader.get_marked())
return Binding(
key=key,
value=value,
original=reader.get_marked(),
)
except Error:
reader.read_regex(_rest_of_line)
return Binding(key=None, value=None, original=reader.get_marked())
return Binding(
key=None,
value=None,
original=reader.get_marked(),
)


def parse_stream(stream):
# type:(IO[Text]) -> Iterator[Binding]
# type: (IO[Text]) -> Iterator[Binding]
reader = Reader(stream)
while reader.has_next():
try:
yield parse_binding(reader)
except Error:
return
yield parse_binding(reader)
92 changes: 52 additions & 40 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,83 +2,95 @@
import pytest

from dotenv.compat import StringIO
from dotenv.parser import Binding, parse_stream
from dotenv.parser import Binding, Original, parse_stream


@pytest.mark.parametrize("test_input,expected", [
(u"", []),
(u"a=b", [Binding(key=u"a", value=u"b", original=u"a=b")]),
(u"'a'=b", [Binding(key=u"a", value=u"b", original=u"'a'=b")]),
(u"[=b", [Binding(key=u"[", value=u"b", original=u"[=b")]),
(u" a = b ", [Binding(key=u"a", value=u"b", original=u" a = b ")]),
(u"export a=b", [Binding(key=u"a", value=u"b", original=u"export a=b")]),
(u" export 'a'=b", [Binding(key=u"a", value=u"b", original=u" export 'a'=b")]),
(u"# a=b", [Binding(key=None, value=None, original=u"# a=b")]),
(u"a=b#c", [Binding(key=u"a", value=u"b#c", original=u"a=b#c")]),
(u'a=b # comment', [Binding(key=u"a", value=u"b", original=u"a=b # comment")]),
(u"a=b space ", [Binding(key=u"a", value=u"b space", original=u"a=b space ")]),
(u"a='b space '", [Binding(key=u"a", value=u"b space ", original=u"a='b space '")]),
(u'a="b space "', [Binding(key=u"a", value=u"b space ", original=u'a="b space "')]),
(u"export export_a=1", [Binding(key=u"export_a", value=u"1", original=u"export export_a=1")]),
(u"export port=8000", [Binding(key=u"port", value=u"8000", original=u"export port=8000")]),
(u'a="b\nc"', [Binding(key=u"a", value=u"b\nc", original=u'a="b\nc"')]),
(u"a='b\nc'", [Binding(key=u"a", value=u"b\nc", original=u"a='b\nc'")]),
(u'a="b\nc"', [Binding(key=u"a", value=u"b\nc", original=u'a="b\nc"')]),
(u'a="b\\nc"', [Binding(key=u"a", value=u'b\nc', original=u'a="b\\nc"')]),
(u"a='b\\nc'", [Binding(key=u"a", value=u'b\\nc', original=u"a='b\\nc'")]),
(u'a="b\\"c"', [Binding(key=u"a", value=u'b"c', original=u'a="b\\"c"')]),
(u"a='b\\'c'", [Binding(key=u"a", value=u"b'c", original=u"a='b\\'c'")]),
(u"a=à", [Binding(key=u"a", value=u"à", original=u"a=à")]),
(u'a="à"', [Binding(key=u"a", value=u"à", original=u'a="à"')]),
(u'garbage', [Binding(key=None, value=None, original=u"garbage")]),
(u"a=b", [Binding(key=u"a", value=u"b", original=Original(string=u"a=b", line=1))]),
(u"'a'=b", [Binding(key=u"a", value=u"b", original=Original(string=u"'a'=b", line=1))]),
(u"[=b", [Binding(key=u"[", value=u"b", original=Original(string=u"[=b", line=1))]),
(u" a = b ", [Binding(key=u"a", value=u"b", original=Original(string=u" a = b ", line=1))]),
(u"export a=b", [Binding(key=u"a", value=u"b", original=Original(string=u"export a=b", line=1))]),
(u" export 'a'=b", [Binding(key=u"a", value=u"b", original=Original(string=u" export 'a'=b", line=1))]),
(u"# a=b", [Binding(key=None, value=None, original=Original(string=u"# a=b", line=1))]),
(u"a=b#c", [Binding(key=u"a", value=u"b#c", original=Original(string=u"a=b#c", line=1))]),
(u'a=b # comment', [Binding(key=u"a", value=u"b", original=Original(string=u"a=b # comment", line=1))]),
(u"a=b space ", [Binding(key=u"a", value=u"b space", original=Original(string=u"a=b space ", line=1))]),
(u"a='b space '", [Binding(key=u"a", value=u"b space ", original=Original(string=u"a='b space '", line=1))]),
(u'a="b space "', [Binding(key=u"a", value=u"b space ", original=Original(string=u'a="b space "', line=1))]),
(
u"export export_a=1",
[
Binding(key=u"export_a", value=u"1", original=Original(string=u"export export_a=1", line=1))
],
),
(u"export port=8000", [Binding(key=u"port", value=u"8000", original=Original(string=u"export port=8000", line=1))]),
(u'a="b\nc"', [Binding(key=u"a", value=u"b\nc", original=Original(string=u'a="b\nc"', line=1))]),
(u"a='b\nc'", [Binding(key=u"a", value=u"b\nc", original=Original(string=u"a='b\nc'", line=1))]),
(u'a="b\nc"', [Binding(key=u"a", value=u"b\nc", original=Original(string=u'a="b\nc"', line=1))]),
(u'a="b\\nc"', [Binding(key=u"a", value=u'b\nc', original=Original(string=u'a="b\\nc"', line=1))]),
(u"a='b\\nc'", [Binding(key=u"a", value=u'b\\nc', original=Original(string=u"a='b\\nc'", line=1))]),
(u'a="b\\"c"', [Binding(key=u"a", value=u'b"c', original=Original(string=u'a="b\\"c"', line=1))]),
(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"a=b\nc=d",
[
Binding(key=u"a", value=u"b", original=u"a=b\n"),
Binding(key=u"c", value=u"d", original=u"c=d"),
Binding(key=u"a", value=u"b", original=Original(string=u"a=b\n", line=1)),
Binding(key=u"c", value=u"d", original=Original(string=u"c=d", line=2)),
],
),
(
u"a=b\rc=d",
[
Binding(key=u"a", value=u"b", original=Original(string=u"a=b\r", line=1)),
Binding(key=u"c", value=u"d", original=Original(string=u"c=d", line=2)),
],
),
(
u"a=b\r\nc=d",
[
Binding(key=u"a", value=u"b", original=u"a=b\r\n"),
Binding(key=u"c", value=u"d", original=u"c=d"),
Binding(key=u"a", value=u"b", original=Original(string=u"a=b\r\n", line=1)),
Binding(key=u"c", value=u"d", original=Original(string=u"c=d", line=2)),
],
),
(
u'a=\nb=c',
[
Binding(key=u"a", value=u'', original=u'a=\n'),
Binding(key=u"b", value=u'c', original=u"b=c"),
Binding(key=u"a", value=u'', original=Original(string=u'a=\n', line=1)),
Binding(key=u"b", value=u'c', original=Original(string=u"b=c", line=2)),
]
),
(
u'a=b\n\nc=d',
[
Binding(key=u"a", value=u"b", original=u"a=b\n"),
Binding(key=u"c", value=u"d", original=u"\nc=d"),
Binding(key=u"a", value=u"b", original=Original(string=u"a=b\n", line=1)),
Binding(key=u"c", value=u"d", original=Original(string=u"\nc=d", line=2)),
]
),
(
u'a="\nb=c',
[
Binding(key=None, value=None, original=u'a="\n'),
Binding(key=u"b", value=u"c", original=u"b=c"),
Binding(key=None, value=None, original=Original(string=u'a="\n', line=1)),
Binding(key=u"b", value=u"c", original=Original(string=u"b=c", line=2)),
]
),
(
u'# comment\na="b\nc"\nd=e\n',
[
Binding(key=None, value=None, original=u"# comment\n"),
Binding(key=u"a", value=u"b\nc", original=u'a="b\nc"\n'),
Binding(key=u"d", value=u"e", original=u"d=e\n"),
Binding(key=None, value=None, original=Original(string=u"# comment\n", line=1)),
Binding(key=u"a", value=u"b\nc", original=Original(string=u'a="b\nc"\n', line=2)),
Binding(key=u"d", value=u"e", original=Original(string=u"d=e\n", line=4)),
],
),
(
u'garbage[%$#\na=b',
[
Binding(key=None, value=None, original=u"garbage[%$#\n"),
Binding(key=u"a", value=u"b", original=u'a=b'),
Binding(key=None, value=None, original=Original(string=u"garbage[%$#\n", line=1)),
Binding(key=u"a", value=u"b", original=Original(string=u'a=b', line=2)),
],
),
])
Expand Down