Skip to content

Commit 1fd40c5

Browse files
ismstheskumar
authored andcommitted
Add method to walk up directories looking for .env (theskumar#23)
- Add `find_dotenv` method that will try to find a .env file by (a) guessing where to start using `__file__` or the working directory -- allowing this to work in non-file contexts such as IPython notebooks and the REPL, and then (b) walking up the dir- ectory tree looking for the specified file -- called `.env` by default. This is a bit like the "filthy magic" employed by django-dotenv[1] to serve the same purpose, and allows the user to write `load_dotenv(find_dotenv())` in many contexts. - Add test for new function [1] https://github.com/jpadilla/django-dotenv/blob/master/dotenv.py#L44-L46
1 parent 02b59d2 commit 1fd40c5

3 files changed

Lines changed: 90 additions & 4 deletions

File tree

dotenv/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from .cli import get_cli_string
2-
from .main import load_dotenv, get_key, set_key, unset_key
2+
from .main import load_dotenv, get_key, set_key, unset_key, find_dotenv
33

4-
__all__ = ['get_cli_string', 'load_dotenv', 'get_key', 'set_key', 'unset_key']
4+
__all__ = ['get_cli_string', 'load_dotenv', 'get_key', 'set_key', 'unset_key', 'find_dotenv']

dotenv/main.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# -*- coding: utf-8 -*-
2-
32
import os
43
import warnings
54

@@ -95,3 +94,45 @@ def flatten_and_write(dotenv_path, dotenv_as_dict, quote_mode="always"):
9594
str_format = '%s="%s"\n' if _mode == "always" else '%s=%s\n'
9695
f.write(str_format % (k, v))
9796
return True
97+
98+
99+
def _walk_to_root(path):
100+
"""
101+
Yield directories starting from the given directory up to the root
102+
"""
103+
if not os.path.exists(path):
104+
raise IOError('Starting path not found')
105+
106+
if os.path.isfile(path):
107+
path = os.path.dirname(path)
108+
109+
last_dir = None
110+
current_dir = os.path.abspath(path)
111+
while last_dir != current_dir:
112+
yield current_dir
113+
parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir))
114+
last_dir, current_dir = current_dir, parent_dir
115+
116+
117+
def find_dotenv(filename='.env', raise_error_if_not_found=False, usecwd=False):
118+
"""
119+
Search in increasingly higher folders for the given file
120+
121+
Returns path to the file if found, or an empty string otherwise
122+
"""
123+
if usecwd or '__file__' not in globals():
124+
# should work without __file__, e.g. in REPL or IPython notebook
125+
path = os.getcwd()
126+
else:
127+
# will work for .py files
128+
path = os.path.dirname(os.path.abspath(__file__))
129+
130+
for dirname in _walk_to_root(path):
131+
check_path = os.path.join(dirname, filename)
132+
if os.path.exists(check_path):
133+
return check_path
134+
135+
if raise_error_if_not_found:
136+
raise IOError('File not found')
137+
138+
return ''

tests/test_core.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
# -*- coding: utf8 -*-
2+
import os
3+
import pytest
4+
import tempfile
15
import warnings
26

3-
from dotenv import load_dotenv
7+
from dotenv import load_dotenv, find_dotenv
48

59

610
def test_warns_if_file_does_not_exist():
@@ -10,3 +14,44 @@ def test_warns_if_file_does_not_exist():
1014
assert len(w) == 1
1115
assert w[0].category is UserWarning
1216
assert str(w[0].message) == "Not loading .does_not_exist - it doesn't exist."
17+
18+
19+
def test_find_dotenv():
20+
"""
21+
Create a temporary folder structure like the following:
22+
23+
tmpXiWxa5/
24+
└── child1
25+
├── child2
26+
│   └── child3
27+
│   └── child4
28+
└── .env
29+
30+
Then try to automatically `find_dotenv` starting in `child4`
31+
"""
32+
tmpdir = tempfile.mkdtemp()
33+
34+
curr_dir = tmpdir
35+
dirs = []
36+
for f in ['child1', 'child2', 'child3', 'child4']:
37+
curr_dir = os.path.join(curr_dir, f)
38+
dirs.append(curr_dir)
39+
os.mkdir(curr_dir)
40+
41+
child1, child4 = dirs[0], dirs[-1]
42+
43+
# change the working directory for testing
44+
os.chdir(child4)
45+
46+
# try without a .env file and force error
47+
with pytest.raises(IOError):
48+
find_dotenv(raise_error_if_not_found=True, usecwd=True)
49+
50+
# try without a .env file and fail silently
51+
assert find_dotenv(usecwd=True) == ''
52+
53+
# now place a .env file a few levels up and make sure it's found
54+
filename = os.path.join(child1, '.env')
55+
with open(filename, 'w') as f:
56+
f.write("TEST=test\n")
57+
assert find_dotenv(usecwd=True) == filename

0 commit comments

Comments
 (0)