File: test_is_interactive.py

package info (click to toggle)
python-dotenv 1.2.1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 316 kB
  • sloc: python: 2,118; makefile: 52
file content (232 lines) | stat: -rw-r--r-- 8,921 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
import builtins
import sys
from unittest import mock

from dotenv.main import find_dotenv


class TestIsInteractive:
    """Tests for the _is_interactive helper function within find_dotenv.

    The _is_interactive function is used by find_dotenv to determine if the code
    is running in an interactive environment (like a REPL, IPython notebook, etc.)
    versus a normal script execution.

    Interactive environments include:
    - Python REPL (has sys.ps1 or sys.ps2)
    - IPython notebooks (no __file__ in __main__)
    - Interactive shells

    Non-interactive environments include:
    - Normal script execution (has __file__ in __main__)
    - Module imports

    Examples of the behavior:
    >>> import sys
    >>> # In a REPL:
    >>> hasattr(sys, 'ps1')  # True
    >>> # In a script:
    >>> hasattr(sys, 'ps1')  # False
    """

    def _create_dotenv_file(self, tmp_path):
        """Helper to create a test .env file."""
        dotenv_path = tmp_path / ".env"
        dotenv_path.write_text("TEST=value")
        return dotenv_path

    def _setup_subdir_and_chdir(self, tmp_path, monkeypatch):
        """Helper to create subdirectory and change to it."""
        test_dir = tmp_path / "subdir"
        test_dir.mkdir()
        monkeypatch.chdir(test_dir)
        return test_dir

    def _remove_ps_attributes(self, monkeypatch):
        """Helper to remove ps1/ps2 attributes if they exist."""
        if hasattr(sys, "ps1"):
            monkeypatch.delattr(sys, "ps1")
        if hasattr(sys, "ps2"):
            monkeypatch.delattr(sys, "ps2")

    def _mock_main_import(self, monkeypatch, mock_main_module):
        """Helper to mock __main__ module import."""
        original_import = builtins.__import__

        def mock_import(name, *args, **kwargs):
            if name == "__main__":
                return mock_main_module
            return original_import(name, *args, **kwargs)

        monkeypatch.setattr(builtins, "__import__", mock_import)

    def _mock_main_import_error(self, monkeypatch):
        """Helper to mock __main__ module import that raises ModuleNotFoundError."""
        original_import = builtins.__import__

        def mock_import(name, *args, **kwargs):
            if name == "__main__":
                raise ModuleNotFoundError("No module named '__main__'")
            return original_import(name, *args, **kwargs)

        monkeypatch.setattr(builtins, "__import__", mock_import)

    def test_is_interactive_with_ps1(self, tmp_path, monkeypatch):
        """Test that _is_interactive returns True when sys.ps1 exists."""
        dotenv_path = self._create_dotenv_file(tmp_path)

        # Mock sys.ps1 to simulate interactive shell
        monkeypatch.setattr(sys, "ps1", ">>> ", raising=False)

        self._setup_subdir_and_chdir(tmp_path, monkeypatch)

        # When _is_interactive() returns True, find_dotenv should search from cwd
        result = find_dotenv()
        assert result == str(dotenv_path)

    def test_is_interactive_with_ps2(self, tmp_path, monkeypatch):
        """Test that _is_interactive returns True when sys.ps2 exists."""
        dotenv_path = self._create_dotenv_file(tmp_path)

        # Mock sys.ps2 to simulate multi-line interactive input
        monkeypatch.setattr(sys, "ps2", "... ", raising=False)

        self._setup_subdir_and_chdir(tmp_path, monkeypatch)

        # When _is_interactive() returns True, find_dotenv should search from cwd
        result = find_dotenv()
        assert result == str(dotenv_path)

    def test_is_interactive_main_module_not_found(self, tmp_path, monkeypatch):
        """Test that _is_interactive returns False when __main__ module import fails."""
        self._remove_ps_attributes(monkeypatch)
        self._mock_main_import_error(monkeypatch)

        # Change to directory and test
        monkeypatch.chdir(tmp_path)

        # Since _is_interactive() returns False, find_dotenv should not find anything
        # without usecwd=True
        result = find_dotenv()
        assert result == ""

    def test_is_interactive_main_without_file(self, tmp_path, monkeypatch):
        """Test that _is_interactive returns True when __main__ has no __file__ attribute."""
        self._remove_ps_attributes(monkeypatch)
        dotenv_path = self._create_dotenv_file(tmp_path)

        # Mock __main__ module without __file__ attribute
        mock_main = mock.MagicMock()
        del mock_main.__file__  # Remove __file__ attribute

        self._mock_main_import(monkeypatch, mock_main)
        self._setup_subdir_and_chdir(tmp_path, monkeypatch)

        # When _is_interactive() returns True, find_dotenv should search from cwd
        result = find_dotenv()
        assert result == str(dotenv_path)

    def test_is_interactive_main_with_file(self, tmp_path, monkeypatch):
        """Test that _is_interactive returns False when __main__ has __file__ attribute."""
        self._remove_ps_attributes(monkeypatch)

        # Mock __main__ module with __file__ attribute
        mock_main = mock.MagicMock()
        mock_main.__file__ = "/path/to/script.py"

        self._mock_main_import(monkeypatch, mock_main)

        # Change to directory and test
        monkeypatch.chdir(tmp_path)

        # Since _is_interactive() returns False, find_dotenv should not find anything
        # without usecwd=True
        result = find_dotenv()
        assert result == ""

    def test_is_interactive_precedence_ps1_over_main(self, tmp_path, monkeypatch):
        """Test that ps1/ps2 attributes take precedence over __main__ module check."""
        dotenv_path = self._create_dotenv_file(tmp_path)

        # Set ps1 attribute
        monkeypatch.setattr(sys, "ps1", ">>> ", raising=False)

        # Mock __main__ module with __file__ attribute (which would normally return False)
        mock_main = mock.MagicMock()
        mock_main.__file__ = "/path/to/script.py"

        self._mock_main_import(monkeypatch, mock_main)
        self._setup_subdir_and_chdir(tmp_path, monkeypatch)

        # ps1 should take precedence, so _is_interactive() returns True
        result = find_dotenv()
        assert result == str(dotenv_path)

    def test_is_interactive_ps1_and_ps2_both_exist(self, tmp_path, monkeypatch):
        """Test that _is_interactive returns True when both ps1 and ps2 exist."""
        dotenv_path = self._create_dotenv_file(tmp_path)

        # Set both ps1 and ps2 attributes
        monkeypatch.setattr(sys, "ps1", ">>> ", raising=False)
        monkeypatch.setattr(sys, "ps2", "... ", raising=False)

        self._setup_subdir_and_chdir(tmp_path, monkeypatch)

        # Should return True with either attribute present
        result = find_dotenv()
        assert result == str(dotenv_path)

    def test_is_interactive_main_module_with_file_attribute_none(
        self, tmp_path, monkeypatch
    ):
        """Test _is_interactive when __main__ has __file__ attribute set to None."""
        self._remove_ps_attributes(monkeypatch)

        # Mock __main__ module with __file__ = None
        mock_main = mock.MagicMock()
        mock_main.__file__ = None

        self._mock_main_import(monkeypatch, mock_main)

        # Mock sys.gettrace to ensure debugger detection returns False
        monkeypatch.setattr("sys.gettrace", lambda: None)

        monkeypatch.chdir(tmp_path)

        # __file__ = None should still be considered non-interactive
        # and with no debugger, find_dotenv should not search from cwd
        result = find_dotenv()
        assert result == ""

    def test_is_interactive_no_ps_attributes_and_normal_execution(
        self, tmp_path, monkeypatch
    ):
        """Test normal script execution scenario where _is_interactive should return False."""
        self._remove_ps_attributes(monkeypatch)

        # Don't mock anything - let it use the real __main__ module
        # which should have a __file__ attribute in normal execution

        # Change to directory and test
        monkeypatch.chdir(tmp_path)

        # In normal execution, _is_interactive() should return False
        # so find_dotenv should not find anything without usecwd=True
        result = find_dotenv()
        assert result == ""

    def test_is_interactive_with_usecwd_override(self, tmp_path, monkeypatch):
        """Test that usecwd=True overrides _is_interactive behavior."""
        self._remove_ps_attributes(monkeypatch)
        dotenv_path = self._create_dotenv_file(tmp_path)

        # Mock __main__ module with __file__ attribute (non-interactive)
        mock_main = mock.MagicMock()
        mock_main.__file__ = "/path/to/script.py"

        self._mock_main_import(monkeypatch, mock_main)
        self._setup_subdir_and_chdir(tmp_path, monkeypatch)

        # Even though _is_interactive() returns False, usecwd=True should find the file
        result = find_dotenv(usecwd=True)
        assert result == str(dotenv_path)