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
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ jobs:
- ubuntu-latest
python-version:
["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", pypy3.9, pypy3.10]
include:
# Windows: Test lowest and highest supported Python versions
- os: windows-latest
python-version: "3.9"
- os: windows-latest
python-version: "3.14"

steps:
- uses: actions/checkout@v6
Expand Down
11 changes: 10 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import os
import subprocess
import sys
from pathlib import Path
from typing import Optional, Sequence

import pytest
import sh

import dotenv
from dotenv.cli import cli as dotenv_cli
from dotenv.version import __version__

if sys.platform != "win32":
import sh


def invoke_sub(args: Sequence[str]) -> subprocess.CompletedProcess:
"""
Expand Down Expand Up @@ -189,6 +192,7 @@ def test_set_no_file(cli):
assert "Missing argument" in result.output


@pytest.mark.skipif(sys.platform == "win32", reason="sh module doesn't support Windows")
def test_get_default_path(tmp_path):
with sh.pushd(tmp_path):
(tmp_path / ".env").write_text("a=b")
Expand All @@ -198,6 +202,7 @@ def test_get_default_path(tmp_path):
assert result == "b\n"


@pytest.mark.skipif(sys.platform == "win32", reason="sh module doesn't support Windows")
def test_run(tmp_path):
with sh.pushd(tmp_path):
(tmp_path / ".env").write_text("a=b")
Expand All @@ -207,6 +212,7 @@ def test_run(tmp_path):
assert result == "b\n"


@pytest.mark.skipif(sys.platform == "win32", reason="sh module doesn't support Windows")
def test_run_with_existing_variable(tmp_path):
with sh.pushd(tmp_path):
(tmp_path / ".env").write_text("a=b")
Expand All @@ -218,6 +224,7 @@ def test_run_with_existing_variable(tmp_path):
assert result == "b\n"


@pytest.mark.skipif(sys.platform == "win32", reason="sh module doesn't support Windows")
def test_run_with_existing_variable_not_overridden(tmp_path):
with sh.pushd(tmp_path):
(tmp_path / ".env").write_text("a=b")
Expand All @@ -229,6 +236,7 @@ def test_run_with_existing_variable_not_overridden(tmp_path):
assert result == "c\n"


@pytest.mark.skipif(sys.platform == "win32", reason="sh module doesn't support Windows")
def test_run_with_none_value(tmp_path):
with sh.pushd(tmp_path):
(tmp_path / ".env").write_text("a=b\nc")
Expand All @@ -238,6 +246,7 @@ def test_run_with_none_value(tmp_path):
assert result == "b\n"


@pytest.mark.skipif(sys.platform == "win32", reason="sh module doesn't support Windows")
def test_run_with_other_env(dotenv_path):
dotenv_path.write_text("a=b")

Expand Down
4 changes: 1 addition & 3 deletions tests/test_fifo_dotenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@

from dotenv import load_dotenv

pytestmark = pytest.mark.skipif(
sys.platform.startswith("win"), reason="FIFOs are Unix-only"
)
pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="FIFOs are Unix-only")


def test_load_dotenv_from_fifo(tmp_path: pathlib.Path, monkeypatch):
Expand Down
10 changes: 10 additions & 0 deletions tests/test_ipython.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import os
import sys
from unittest import mock

import pytest

pytest.importorskip("IPython")


@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {}, clear=True)
def test_ipython_existing_variable_no_override(tmp_path):
from IPython.terminal.embed import InteractiveShellEmbed
Expand All @@ -22,6 +26,9 @@ def test_ipython_existing_variable_no_override(tmp_path):
assert os.environ == {"a": "c"}


@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {}, clear=True)
def test_ipython_existing_variable_override(tmp_path):
from IPython.terminal.embed import InteractiveShellEmbed
Expand All @@ -38,6 +45,9 @@ def test_ipython_existing_variable_override(tmp_path):
assert os.environ == {"a": "b"}


@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {}, clear=True)
def test_ipython_new_variable(tmp_path):
from IPython.terminal.embed import InteractiveShellEmbed
Expand Down
59 changes: 45 additions & 14 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import io
import logging
import os
import stat
import sys
import textwrap
from unittest import mock

import pytest
import sh

import dotenv

if sys.platform != "win32":
import sh


def test_set_key_no_file(tmp_path):
nx_path = tmp_path / "nx"
Expand Down Expand Up @@ -62,15 +65,25 @@ def test_set_key_encoding(dotenv_path):


@pytest.mark.skipif(
os.geteuid() == 0, reason="Root user can access files even with 000 permissions."
sys.platform != "win32" and os.geteuid() == 0,
reason="Root user can access files even with 000 permissions.",
)
def test_set_key_permission_error(dotenv_path):
dotenv_path.chmod(0o000)
if sys.platform == "win32":
# On Windows, make file read-only
dotenv_path.chmod(stat.S_IREAD)
else:
# On Unix, remove all permissions
dotenv_path.chmod(0o000)

with pytest.raises(PermissionError):
dotenv.set_key(dotenv_path, "a", "b")

dotenv_path.chmod(0o600)
# Restore permissions
if sys.platform == "win32":
dotenv_path.chmod(stat.S_IWRITE | stat.S_IREAD)
else:
dotenv_path.chmod(0o600)
assert dotenv_path.read_text() == ""


Expand Down Expand Up @@ -170,16 +183,6 @@ def test_unset_encoding(dotenv_path):
assert dotenv_path.read_text(encoding=encoding) == ""


@pytest.mark.skipif(
os.geteuid() == 0, reason="Root user can access files even with 000 permissions."
)
def test_set_key_unauthorized_file(dotenv_path):
dotenv_path.chmod(0o000)

with pytest.raises(PermissionError):
dotenv.set_key(dotenv_path, "a", "x")


Comment on lines -173 to -182
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

FYI this test is a duplicate of test test_set_key_permission_error. It seems to have been introduced a long time ago when I refactored testing.

def test_unset_non_existent_file(tmp_path):
nx_path = tmp_path / "nx"
logger = logging.getLogger("dotenv.main")
Expand Down Expand Up @@ -241,6 +244,9 @@ def test_find_dotenv_found(tmp_path):
assert result == str(dotenv_path)


@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {}, clear=True)
def test_load_dotenv_existing_file(dotenv_path):
dotenv_path.write_text("a=b")
Expand Down Expand Up @@ -312,6 +318,9 @@ def test_load_dotenv_disabled_notification(dotenv_path, flag_value):
)


@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@pytest.mark.parametrize(
"flag_value",
[
Expand Down Expand Up @@ -395,6 +404,9 @@ def test_load_dotenv_no_file_verbose():
)


@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {"a": "c"}, clear=True)
def test_load_dotenv_existing_variable_no_override(dotenv_path):
dotenv_path.write_text("a=b")
Expand All @@ -405,6 +417,9 @@ def test_load_dotenv_existing_variable_no_override(dotenv_path):
assert os.environ == {"a": "c"}


@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {"a": "c"}, clear=True)
def test_load_dotenv_existing_variable_override(dotenv_path):
dotenv_path.write_text("a=b")
Expand All @@ -415,6 +430,9 @@ def test_load_dotenv_existing_variable_override(dotenv_path):
assert os.environ == {"a": "b"}


@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {"a": "c"}, clear=True)
def test_load_dotenv_redefine_var_used_in_file_no_override(dotenv_path):
dotenv_path.write_text('a=b\nd="${a}"')
Expand All @@ -425,6 +443,9 @@ def test_load_dotenv_redefine_var_used_in_file_no_override(dotenv_path):
assert os.environ == {"a": "c", "d": "c"}


@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {"a": "c"}, clear=True)
def test_load_dotenv_redefine_var_used_in_file_with_override(dotenv_path):
dotenv_path.write_text('a=b\nd="${a}"')
Expand All @@ -435,6 +456,9 @@ def test_load_dotenv_redefine_var_used_in_file_with_override(dotenv_path):
assert os.environ == {"a": "b", "d": "b"}


@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {}, clear=True)
def test_load_dotenv_string_io_utf_8():
stream = io.StringIO("a=à")
Expand All @@ -445,6 +469,9 @@ def test_load_dotenv_string_io_utf_8():
assert os.environ == {"a": "à"}


@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {}, clear=True)
def test_load_dotenv_file_stream(dotenv_path):
dotenv_path.write_text("a=b")
Expand All @@ -456,6 +483,7 @@ def test_load_dotenv_file_stream(dotenv_path):
assert os.environ == {"a": "b"}


@pytest.mark.skipif(sys.platform == "win32", reason="sh module doesn't support Windows")
def test_load_dotenv_in_current_dir(tmp_path):
dotenv_path = tmp_path / ".env"
dotenv_path.write_bytes(b"a=b")
Expand Down Expand Up @@ -484,6 +512,9 @@ def test_dotenv_values_file(dotenv_path):
assert result == {"a": "b"}


@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@pytest.mark.parametrize(
"env,string,interpolate,expected",
[
Expand Down
6 changes: 5 additions & 1 deletion tests/test_zip_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
from unittest import mock
from zipfile import ZipFile

import sh
import pytest

if sys.platform != "win32":
import sh


def walk_to_root(path: str):
Expand Down Expand Up @@ -62,6 +65,7 @@ def test_load_dotenv_gracefully_handles_zip_imports_when_no_env_file(tmp_path):
import child1.child2.test # noqa


@pytest.mark.skipif(sys.platform == "win32", reason="sh module doesn't support Windows")
def test_load_dotenv_outside_zip_file_when_called_in_zipfile(tmp_path):
zip_file_path = setup_zipfile(
tmp_path,
Expand Down