Skip to content
Open
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
81 changes: 47 additions & 34 deletions src/dotenv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,44 +319,57 @@ def _is_interactive():


def load_dotenv(
dotenv_path: Optional[StrPath] = None,
dotenv_path: Union[str, os.PathLike, None] = None,
stream: Optional[IO[str]] = None,
*,
verbose: bool = False,
override: bool = False,
interpolate: bool = True,
encoding: Optional[str] = "utf-8",
**kwargs
) -> bool:
"""Parse a .env file and then load all the variables found as environment variables.

Parameters:
dotenv_path: Absolute or relative path to .env file.
stream: Text stream (such as `io.StringIO`) with .env content, used if
`dotenv_path` is `None`.
verbose: Whether to output a warning the .env file is missing.
override: Whether to override the system environment variables with the variables
from the `.env` file.
encoding: Encoding to be used to read the file.
Returns:
Bool: True if at least one environment variable is set else False

If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the
.env file with it's default parameters. If you need to change the default parameters
of `find_dotenv()`, you can explicitly call `find_dotenv()` and pass the result
to this function as `dotenv_path`.
"""
if dotenv_path is None and stream is None:
dotenv_path = find_dotenv()

dotenv = DotEnv(
dotenv_path=dotenv_path,
stream=stream,
verbose=verbose,
interpolate=interpolate,
override=override,
encoding=encoding,
)
return dotenv.set_as_environment_variables()


if not hasattr(load_dotenv, "_cache"):
load_dotenv._cache = {}


encoding = kwargs.get('encoding', {})


if dotenv_path:
dotenv_path = os.path.abspath(dotenv_path)


cache_key = str(dotenv_path)
if cache_key in load_dotenv._cache:
return load_dotenv._cache[cache_key]


with open(dotenv_path or '.env') as f:

content = f.read()
Comment on lines +346 to +348

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review #1

License
License

Issue:

FileNotFoundError: stream argument ignored

Where:

load_dotenv in src/dotenv/main.py

Description:

stream-based loading breaks when dotenv_path is None and no .env exists
New code removed prior stream-based behavior and now unconditionally executes open(dotenv_path or '.env') at src/dotenv/main.py:346-348. The function signature still accepts stream at src/dotenv/main.py:323, but the new implementation never uses it. Evidence-1 shows the previous implementation passed stream=stream into DotEnv and only fell back when both dotenv_path and stream were None, so this is a new API/behavior regression that can raise FileNotFoundError for stream-only callers.


+    with open(dotenv_path or '.env') as f:
+
+        content = f.read()

Feedback: 👎


success = parse_dotenv(content, verbose=verbose, override=override)
load_dotenv._cache[cache_key] = success

return success

def parse_dotenv(content: str, verbose: bool = False, override: bool = False) -> bool:

env_vars = {}

for line in content.splitlines():

if line.strip() and not line.startswith('#'):

key, value = line.split('=', 1)
Comment on lines +361 to +363

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review #2

License
License

Issue:

ValueError: malformed .env line crashes parser

Where:

parse_dotenv in src/dotenv/main.py

Description:

loading crashes when a non-comment line lacks an equals sign
New parser code only skips blank/comment lines at src/dotenv/main.py:361, then unconditionally executes line.split('=', 1) at line 363. A non-comment malformed line without = raises ValueError, and the only traced caller load_dotenv passes raw file content directly from open(...).read() to parse_dotenv with no validation or exception handling at src/dotenv/main.py:346-350.


+            key, value = line.split('=', 1)

Feedback: 👎


env_vars[key.strip()] = value.strip().strip("'").strip('"')


for key, value in env_vars.items():
if override or key not in os.environ:
os.environ[key] = value

return True

def dotenv_values(
dotenv_path: Optional[StrPath] = None,
Expand Down
32 changes: 32 additions & 0 deletions src/dotenv/validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import Dict, Any, Optional

class EnvValidator:
def __init__(self):

self.schemas = {}

def add_schema(self, var_name: str, var_type: type, required: bool = True):

self.schemas[var_name] = {
'type': var_type,
'required': required
}

def validate(self, env_vars: Dict[str, Any]) -> bool:

for var_name, schema in self.schemas.items():
if var_name not in env_vars:
if schema['required']:

raise ValueError(f"Missing required env var: {var_name}")
continue

value = env_vars[var_name]
try:

schema['type'](value)
except ValueError:

raise ValueError(f"Invalid type for {var_name}: {value}")
Comment on lines +25 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review theskumar#3

License
License

Issue:

TypeError escapes for invalid schema type callables

Where:

EnvValidator.validate in src/dotenv/validator.py

Description:

validation crashes when schema type is a callable like bool(None) replacement raising TypeError
New file in this PR. EnvValidator.validate wraps schema['type'](value) in try/except but catches only ValueError at src/dotenv/validator.py:25-30. The schema callable is caller-controlled via add_schema(..., var_type, ...) at src/dotenv/validator.py:8-13, so a callable that raises TypeError will escape unhandled. No broader enclosing handler is present in the function.


+            try:
+
+                schema['type'](value)
+            except ValueError:

Feedback: 👎


return True