1+ import sys
2+ import pytest
3+ import builtins
4+ from unittest import mock
5+ from dotenv .main import find_dotenv
6+
7+
8+ class TestIsInteractive :
9+ """Tests for the _is_interactive helper function within find_dotenv.
10+
11+ The _is_interactive function is used by find_dotenv to determine if the code
12+ is running in an interactive environment (like a REPL, IPython notebook, etc.)
13+ versus a normal script execution.
14+
15+ Interactive environments include:
16+ - Python REPL (has sys.ps1 or sys.ps2)
17+ - IPython notebooks (no __file__ in __main__)
18+ - Interactive shells
19+
20+ Non-interactive environments include:
21+ - Normal script execution (has __file__ in __main__)
22+ - Module imports
23+
24+ Examples of the behavior:
25+ >>> import sys
26+ >>> # In a REPL:
27+ >>> hasattr(sys, 'ps1') # True
28+ >>> # In a script:
29+ >>> hasattr(sys, 'ps1') # False
30+ """
31+
32+ def test_is_interactive_with_ps1 (self , tmp_path , monkeypatch ):
33+ """Test that _is_interactive returns True when sys.ps1 exists."""
34+ # Create a test .env file
35+ dotenv_path = tmp_path / ".env"
36+ dotenv_path .write_text ("TEST=value" )
37+
38+ # Mock sys.ps1 to simulate interactive shell
39+ monkeypatch .setattr (sys , "ps1" , ">>> " , raising = False )
40+
41+ # Change to directory without .env to force search
42+ test_dir = tmp_path / "subdir"
43+ test_dir .mkdir ()
44+ monkeypatch .chdir (test_dir )
45+
46+ # When _is_interactive() returns True, find_dotenv should search from cwd
47+ result = find_dotenv ()
48+ assert result == str (dotenv_path )
49+
50+ def test_is_interactive_with_ps2 (self , tmp_path , monkeypatch ):
51+ """Test that _is_interactive returns True when sys.ps2 exists."""
52+ # Create a test .env file
53+ dotenv_path = tmp_path / ".env"
54+ dotenv_path .write_text ("TEST=value" )
55+
56+ # Mock sys.ps2 to simulate multi-line interactive input
57+ monkeypatch .setattr (sys , "ps2" , "... " , raising = False )
58+
59+ # Change to directory without .env to force search
60+ test_dir = tmp_path / "subdir"
61+ test_dir .mkdir ()
62+ monkeypatch .chdir (test_dir )
63+
64+ # When _is_interactive() returns True, find_dotenv should search from cwd
65+ result = find_dotenv ()
66+ assert result == str (dotenv_path )
67+
68+ def test_is_interactive_main_module_not_found (self , tmp_path , monkeypatch ):
69+ """Test that _is_interactive returns False when __main__ module import fails."""
70+ # Remove any ps1/ps2 attributes if they exist
71+ if hasattr (sys , "ps1" ):
72+ monkeypatch .delattr (sys , "ps1" )
73+ if hasattr (sys , "ps2" ):
74+ monkeypatch .delattr (sys , "ps2" )
75+
76+ # Mock __import__ to raise ModuleNotFoundError for __main__
77+ original_import = builtins .__import__
78+
79+ def mock_import (name , * args , ** kwargs ):
80+ if name == "__main__" :
81+ raise ModuleNotFoundError ("No module named '__main__'" )
82+ return original_import (name , * args , ** kwargs )
83+
84+ monkeypatch .setattr (builtins , "__import__" , mock_import )
85+
86+ # Change to directory and test
87+ monkeypatch .chdir (tmp_path )
88+
89+ # Since _is_interactive() returns False, find_dotenv should not find anything
90+ # without usecwd=True
91+ result = find_dotenv ()
92+ assert result == ""
93+
94+ def test_is_interactive_main_without_file (self , tmp_path , monkeypatch ):
95+ """Test that _is_interactive returns True when __main__ has no __file__ attribute."""
96+ # Remove any ps1/ps2 attributes if they exist
97+ if hasattr (sys , "ps1" ):
98+ monkeypatch .delattr (sys , "ps1" )
99+ if hasattr (sys , "ps2" ):
100+ monkeypatch .delattr (sys , "ps2" )
101+
102+ # Create a test .env file
103+ dotenv_path = tmp_path / ".env"
104+ dotenv_path .write_text ("TEST=value" )
105+
106+ # Mock __main__ module without __file__ attribute
107+ mock_main = mock .MagicMock ()
108+ del mock_main .__file__ # Remove __file__ attribute
109+
110+ original_import = builtins .__import__
111+
112+ def mock_import (name , * args , ** kwargs ):
113+ if name == "__main__" :
114+ return mock_main
115+ return original_import (name , * args , ** kwargs )
116+
117+ monkeypatch .setattr (builtins , "__import__" , mock_import )
118+
119+ # Change to directory without .env to force search
120+ test_dir = tmp_path / "subdir"
121+ test_dir .mkdir ()
122+ monkeypatch .chdir (test_dir )
123+
124+ # When _is_interactive() returns True, find_dotenv should search from cwd
125+ result = find_dotenv ()
126+ assert result == str (dotenv_path )
127+
128+ def test_is_interactive_main_with_file (self , tmp_path , monkeypatch ):
129+ """Test that _is_interactive returns False when __main__ has __file__ attribute."""
130+ # Remove any ps1/ps2 attributes if they exist
131+ if hasattr (sys , "ps1" ):
132+ monkeypatch .delattr (sys , "ps1" )
133+ if hasattr (sys , "ps2" ):
134+ monkeypatch .delattr (sys , "ps2" )
135+
136+ # Mock __main__ module with __file__ attribute
137+ mock_main = mock .MagicMock ()
138+ mock_main .__file__ = "/path/to/script.py"
139+
140+ original_import = builtins .__import__
141+
142+ def mock_import (name , * args , ** kwargs ):
143+ if name == "__main__" :
144+ return mock_main
145+ return original_import (name , * args , ** kwargs )
146+
147+ monkeypatch .setattr (builtins , "__import__" , mock_import )
148+
149+ # Change to directory and test
150+ monkeypatch .chdir (tmp_path )
151+
152+ # Since _is_interactive() returns False, find_dotenv should not find anything
153+ # without usecwd=True
154+ result = find_dotenv ()
155+ assert result == ""
156+
157+ def test_is_interactive_precedence_ps1_over_main (self , tmp_path , monkeypatch ):
158+ """Test that ps1/ps2 attributes take precedence over __main__ module check."""
159+ # Create a test .env file
160+ dotenv_path = tmp_path / ".env"
161+ dotenv_path .write_text ("TEST=value" )
162+
163+ # Set ps1 attribute
164+ monkeypatch .setattr (sys , "ps1" , ">>> " , raising = False )
165+
166+ # Mock __main__ module with __file__ attribute (which would normally return False)
167+ mock_main = mock .MagicMock ()
168+ mock_main .__file__ = "/path/to/script.py"
169+
170+ original_import = builtins .__import__
171+
172+ def mock_import (name , * args , ** kwargs ):
173+ if name == "__main__" :
174+ return mock_main
175+ return original_import (name , * args , ** kwargs )
176+
177+ monkeypatch .setattr (builtins , "__import__" , mock_import )
178+
179+ # Change to directory without .env to force search
180+ test_dir = tmp_path / "subdir"
181+ test_dir .mkdir ()
182+ monkeypatch .chdir (test_dir )
183+
184+ # ps1 should take precedence, so _is_interactive() returns True
185+ result = find_dotenv ()
186+ assert result == str (dotenv_path )
187+
188+ def test_is_interactive_ps1_and_ps2_both_exist (self , tmp_path , monkeypatch ):
189+ """Test that _is_interactive returns True when both ps1 and ps2 exist."""
190+ # Create a test .env file
191+ dotenv_path = tmp_path / ".env"
192+ dotenv_path .write_text ("TEST=value" )
193+
194+ # Set both ps1 and ps2 attributes
195+ monkeypatch .setattr (sys , "ps1" , ">>> " , raising = False )
196+ monkeypatch .setattr (sys , "ps2" , "... " , raising = False )
197+
198+ # Change to directory without .env to force search
199+ test_dir = tmp_path / "subdir"
200+ test_dir .mkdir ()
201+ monkeypatch .chdir (test_dir )
202+
203+ # Should return True with either attribute present
204+ result = find_dotenv ()
205+ assert result == str (dotenv_path )
206+
207+ def test_is_interactive_main_module_with_file_attribute_none (self , tmp_path , monkeypatch ):
208+ """Test _is_interactive when __main__ has __file__ attribute set to None."""
209+ # Remove any ps1/ps2 attributes if they exist
210+ if hasattr (sys , "ps1" ):
211+ monkeypatch .delattr (sys , "ps1" )
212+ if hasattr (sys , "ps2" ):
213+ monkeypatch .delattr (sys , "ps2" )
214+
215+ # Create a test .env file
216+ dotenv_path = tmp_path / ".env"
217+ dotenv_path .write_text ("TEST=value" )
218+
219+ # Mock __main__ module with __file__ = None
220+ mock_main = mock .MagicMock ()
221+ mock_main .__file__ = None
222+
223+ original_import = builtins .__import__
224+
225+ def mock_import (name , * args , ** kwargs ):
226+ if name == "__main__" :
227+ return mock_main
228+ return original_import (name , * args , ** kwargs )
229+
230+ monkeypatch .setattr (builtins , "__import__" , mock_import )
231+
232+ # Change to directory without .env to force search
233+ test_dir = tmp_path / "subdir"
234+ test_dir .mkdir ()
235+ monkeypatch .chdir (test_dir )
236+
237+ # __file__ = None should still be considered non-interactive
238+ result = find_dotenv ()
239+ assert result == ""
240+
241+ def test_is_interactive_no_ps_attributes_and_normal_execution (self , tmp_path , monkeypatch ):
242+ """Test normal script execution scenario where _is_interactive should return False."""
243+ # Remove any ps1/ps2 attributes if they exist
244+ if hasattr (sys , "ps1" ):
245+ monkeypatch .delattr (sys , "ps1" )
246+ if hasattr (sys , "ps2" ):
247+ monkeypatch .delattr (sys , "ps2" )
248+
249+ # Don't mock anything - let it use the real __main__ module
250+ # which should have a __file__ attribute in normal execution
251+
252+ # Change to directory and test
253+ monkeypatch .chdir (tmp_path )
254+
255+ # In normal execution, _is_interactive() should return False
256+ # so find_dotenv should not find anything without usecwd=True
257+ result = find_dotenv ()
258+ assert result == ""
259+
260+ def test_is_interactive_with_usecwd_override (self , tmp_path , monkeypatch ):
261+ """Test that usecwd=True overrides _is_interactive behavior."""
262+ # Remove any ps1/ps2 attributes if they exist
263+ if hasattr (sys , "ps1" ):
264+ monkeypatch .delattr (sys , "ps1" )
265+ if hasattr (sys , "ps2" ):
266+ monkeypatch .delattr (sys , "ps2" )
267+
268+ # Create a test .env file
269+ dotenv_path = tmp_path / ".env"
270+ dotenv_path .write_text ("TEST=value" )
271+
272+ # Mock __main__ module with __file__ attribute (non-interactive)
273+ mock_main = mock .MagicMock ()
274+ mock_main .__file__ = "/path/to/script.py"
275+
276+ original_import = builtins .__import__
277+
278+ def mock_import (name , * args , ** kwargs ):
279+ if name == "__main__" :
280+ return mock_main
281+ return original_import (name , * args , ** kwargs )
282+
283+ monkeypatch .setattr (builtins , "__import__" , mock_import )
284+
285+ # Change to directory without .env to force search
286+ test_dir = tmp_path / "subdir"
287+ test_dir .mkdir ()
288+ monkeypatch .chdir (test_dir )
289+
290+ # Even though _is_interactive() returns False, usecwd=True should find the file
291+ result = find_dotenv (usecwd = True )
292+ assert result == str (dotenv_path )
0 commit comments