Skip to content

Some more s/Python-dotenv/python-dotenv/ #552

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 9, 2025
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
5 changes: 2 additions & 3 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@

## Reporting a Vulnerability

If you believe you have identified a security issue with Python-dotenv, please email
If you believe you have identified a security issue with python-dotenv, please email
python-dotenv@saurabh-kumar.com. A maintainer will contact you acknowledging the report
and how to continue.

Be sure to include as much detail as necessary in your report. As with reporting normal
issues, a minimal reproducible example will help the maintainers address the issue faster.
If you are able, you may also include a fix for the issue generated with `git
format-patch`.
If you are able, you may also include a fix for the issue generated with `git format-patch`.
49 changes: 25 additions & 24 deletions src/dotenv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
import tempfile
from collections import OrderedDict
from contextlib import contextmanager
from typing import (IO, Dict, Iterable, Iterator, Mapping, Optional, Tuple,
Union)
from typing import IO, Dict, Iterable, Iterator, Mapping, Optional, Tuple, Union

from .parser import Binding, parse_stream
from .variables import parse_variables
Expand All @@ -17,7 +16,7 @@
# These paths may flow to `open()` and `shutil.move()`; `shutil.move()`
# only accepts string paths, not byte paths or file descriptors. See
# https://github.com/python/typeshed/pull/6832.
StrPath = Union[str, 'os.PathLike[str]']
StrPath = Union[str, "os.PathLike[str]"]

logger = logging.getLogger(__name__)

Expand All @@ -26,7 +25,7 @@ def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding
for mapping in mappings:
if mapping.error:
logger.warning(
"Python-dotenv could not parse statement starting at line %s",
"python-dotenv could not parse statement starting at line %s",
mapping.original.line,
)
yield mapping
Expand Down Expand Up @@ -60,10 +59,10 @@ def _get_stream(self) -> Iterator[IO[str]]:
else:
if self.verbose:
logger.info(
"Python-dotenv could not find configuration file %s.",
self.dotenv_path or '.env',
"python-dotenv could not find configuration file %s.",
self.dotenv_path or ".env",
)
yield io.StringIO('')
yield io.StringIO("")

def dict(self) -> Dict[str, Optional[str]]:
"""Return dotenv as dict"""
Expand All @@ -73,7 +72,9 @@ def dict(self) -> Dict[str, Optional[str]]:
raw_values = self.parse()

if self.interpolate:
self._dict = OrderedDict(resolve_variables(raw_values, override=self.override))
self._dict = OrderedDict(
resolve_variables(raw_values, override=self.override)
)
else:
self._dict = OrderedDict(raw_values)

Expand Down Expand Up @@ -101,8 +102,7 @@ def set_as_environment_variables(self) -> bool:
return True

def get(self, key: str) -> Optional[str]:
"""
"""
""" """
data = self.dict()

if key in data:
Expand Down Expand Up @@ -166,17 +166,16 @@ def set_key(
if quote_mode not in ("always", "auto", "never"):
raise ValueError(f"Unknown quote_mode: {quote_mode}")

quote = (
quote_mode == "always"
or (quote_mode == "auto" and not value_to_set.isalnum())
quote = quote_mode == "always" or (
quote_mode == "auto" and not value_to_set.isalnum()
)

if quote:
value_out = "'{}'".format(value_to_set.replace("'", "\\'"))
else:
value_out = value_to_set
if export:
line_out = f'export {key_to_set}={value_out}\n'
line_out = f"export {key_to_set}={value_out}\n"
else:
line_out = f"{key_to_set}={value_out}\n"

Expand Down Expand Up @@ -223,7 +222,9 @@ def unset_key(
dest.write(mapping.original.string)

if not removed:
logger.warning("Key %s not removed from %s - key doesn't exist.", key_to_unset, dotenv_path)
logger.warning(
"Key %s not removed from %s - key doesn't exist.", key_to_unset, dotenv_path
)
return None, key_to_unset

return removed, key_to_unset
Expand All @@ -235,7 +236,7 @@ def resolve_variables(
) -> Mapping[str, Optional[str]]:
new_values: Dict[str, Optional[str]] = {}

for (name, value) in values:
for name, value in values:
if value is None:
result = None
else:
Expand All @@ -259,7 +260,7 @@ def _walk_to_root(path: str) -> Iterator[str]:
Yield directories starting from the given directory up to the root
"""
if not os.path.exists(path):
raise IOError('Starting path not found')
raise IOError("Starting path not found")

if os.path.isfile(path):
path = os.path.dirname(path)
Expand All @@ -273,7 +274,7 @@ def _walk_to_root(path: str) -> Iterator[str]:


def find_dotenv(
filename: str = '.env',
filename: str = ".env",
raise_error_if_not_found: bool = False,
usecwd: bool = False,
) -> str:
Expand All @@ -284,14 +285,14 @@ def find_dotenv(
"""

def _is_interactive():
""" Decide whether this is running in a REPL or IPython notebook """
"""Decide whether this is running in a REPL or IPython notebook"""
try:
main = __import__('__main__', None, None, fromlist=['__file__'])
main = __import__("__main__", None, None, fromlist=["__file__"])
except ModuleNotFoundError:
return False
return not hasattr(main, '__file__')
return not hasattr(main, "__file__")

if usecwd or _is_interactive() or getattr(sys, 'frozen', False):
if usecwd or _is_interactive() or getattr(sys, "frozen", False):
# Should work without __file__, e.g. in REPL or IPython notebook.
path = os.getcwd()
else:
Expand All @@ -313,9 +314,9 @@ def _is_interactive():
return check_path

if raise_error_if_not_found:
raise IOError('File not found')
raise IOError("File not found")

return ''
return ""


def load_dotenv(
Expand Down
43 changes: 20 additions & 23 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ def test_set_key_no_file(tmp_path):
("", "a", "", (True, "a", ""), "a=''\n"),
("", "a", "b", (True, "a", "b"), "a='b'\n"),
("", "a", "'b'", (True, "a", "'b'"), "a='\\'b\\''\n"),
("", "a", "\"b\"", (True, "a", '"b"'), "a='\"b\"'\n"),
("", "a", '"b"', (True, "a", '"b"'), "a='\"b\"'\n"),
("", "a", "b'c", (True, "a", "b'c"), "a='b\\'c'\n"),
("", "a", "b\"c", (True, "a", "b\"c"), "a='b\"c'\n"),
("", "a", 'b"c', (True, "a", 'b"c'), "a='b\"c'\n"),
("a=b", "a", "c", (True, "a", "c"), "a='c'\n"),
("a=b\n", "a", "c", (True, "a", "c"), "a='c'\n"),
("a=b\n\n", "a", "c", (True, "a", "c"), "a='c'\n\n"),
Expand Down Expand Up @@ -75,20 +75,20 @@ def test_get_key_no_file(tmp_path):
nx_path = tmp_path / "nx"
logger = logging.getLogger("dotenv.main")

with mock.patch.object(logger, "info") as mock_info, \
mock.patch.object(logger, "warning") as mock_warning:
with (
mock.patch.object(logger, "info") as mock_info,
mock.patch.object(logger, "warning") as mock_warning,
):
result = dotenv.get_key(nx_path, "foo")

assert result is None
mock_info.assert_has_calls(
calls=[
mock.call("Python-dotenv could not find configuration file %s.", nx_path)
mock.call("python-dotenv could not find configuration file %s.", nx_path)
],
)
mock_warning.assert_has_calls(
calls=[
mock.call("Key %s not found in %s.", "foo", nx_path)
],
calls=[mock.call("Key %s not found in %s.", "foo", nx_path)],
)


Expand Down Expand Up @@ -249,10 +249,12 @@ def test_load_dotenv_no_file_verbose():
logger = logging.getLogger("dotenv.main")

with mock.patch.object(logger, "info") as mock_info:
result = dotenv.load_dotenv('.does_not_exist', verbose=True)
result = dotenv.load_dotenv(".does_not_exist", verbose=True)

assert result is False
mock_info.assert_called_once_with("Python-dotenv could not find configuration file %s.", ".does_not_exist")
mock_info.assert_called_once_with(
"python-dotenv could not find configuration file %s.", ".does_not_exist"
)


@mock.patch.dict(os.environ, {"a": "c"}, clear=True)
Expand Down Expand Up @@ -317,21 +319,23 @@ def test_load_dotenv_file_stream(dotenv_path):


def test_load_dotenv_in_current_dir(tmp_path):
dotenv_path = tmp_path / '.env'
dotenv_path.write_bytes(b'a=b')
code_path = tmp_path / 'code.py'
code_path.write_text(textwrap.dedent("""
dotenv_path = tmp_path / ".env"
dotenv_path.write_bytes(b"a=b")
code_path = tmp_path / "code.py"
code_path.write_text(
textwrap.dedent("""
import dotenv
import os

dotenv.load_dotenv(verbose=True)
print(os.environ['a'])
"""))
""")
)
os.chdir(tmp_path)

result = sh.Command(sys.executable)(code_path)

assert result == 'b\n'
assert result == "b\n"


def test_dotenv_values_file(dotenv_path):
Expand All @@ -352,30 +356,23 @@ def test_dotenv_values_file(dotenv_path):
({"b": "c"}, "a=${b}", True, {"a": "c"}),
({"b": "c"}, "a=${b:-d}", False, {"a": "${b:-d}"}),
({"b": "c"}, "a=${b:-d}", True, {"a": "c"}),

# Defined in file
({}, "b=c\na=${b}", True, {"a": "c", "b": "c"}),

# Undefined
({}, "a=${b}", True, {"a": ""}),
({}, "a=${b:-d}", True, {"a": "d"}),

# With quotes
({"b": "c"}, 'a="${b}"', True, {"a": "c"}),
({"b": "c"}, "a='${b}'", True, {"a": "c"}),

# With surrounding text
({"b": "c"}, "a=x${b}y", True, {"a": "xcy"}),

# Self-referential
({"a": "b"}, "a=${a}", True, {"a": "b"}),
({}, "a=${a}", True, {"a": ""}),
({"a": "b"}, "a=${a:-c}", True, {"a": "b"}),
({}, "a=${a:-c}", True, {"a": "c"}),

# Reused
({"b": "c"}, "a=${b}${b}", True, {"a": "cc"}),

# Re-defined and used in file
({"b": "c"}, "b=d\na=${b}", True, {"a": "d", "b": "d"}),
({}, "a=b\na=c\nd=${a}", True, {"a": "c", "d": "c"}),
Expand Down