diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4cc64ed --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +DOTENV=true +DOTENV_EXAMPLE=true diff --git a/dotenv.py b/dotenv.py index 09f01a5..0f4c2a6 100644 --- a/dotenv.py +++ b/dotenv.py @@ -34,7 +34,7 @@ """, re.IGNORECASE | re.VERBOSE) -def read_dotenv(dotenv=None, override=False): +def read_dotenv(dotenv=None, override=False, safe=False): """ Read a .env file into os.environ. @@ -45,6 +45,8 @@ def read_dotenv(dotenv=None, override=False): way to ensure tests run consistently across all environments. :param override: True if values in .env should override system variables. + :param safe: If True, check values in .env.example exist in system + variables. """ if dotenv is None: frame_filename = sys._getframe().f_back.f_code.co_filename @@ -64,6 +66,29 @@ def read_dotenv(dotenv=None, override=False): warnings.warn("Not reading {0} - it doesn't exist.".format(dotenv), stacklevel=2) + if safe: + dotenv_example = _read_dotenv_example(os.path.dirname(dotenv)) + keys_not_exist = _check_safe(dotenv_example) + warnings.warn("The following variables were defined in .env.example " + "but are not present in the environment:\n {}".format( + ', '.join(keys_not_exist)), stacklevel=2) + + +def _read_dotenv_example(dir_name): + dotenv_example = os.path.join(dir_name, '.env.example') + if os.path.isfile(dotenv_example): + with open(dotenv_example) as f: + return parse_dotenv(f.read()) + return {} + + +def _check_safe(dotenv_example): + keys_not_exist = [] + for k in dotenv_example.keys(): + if k not in os.environ: + keys_not_exist.append(k) + return keys_not_exist + def parse_dotenv(content): env = {} diff --git a/tests.py b/tests.py index 12971ae..434f53e 100644 --- a/tests.py +++ b/tests.py @@ -3,8 +3,12 @@ import unittest import warnings -from dotenv import parse_dotenv, read_dotenv - +from dotenv import ( + parse_dotenv, + read_dotenv, + _read_dotenv_example, + _check_safe +) class ParseDotenvTestCase(unittest.TestCase): def test_parses_unquoted_values(self): @@ -113,6 +117,7 @@ def test_parses_hash_in_quoted_values(self): class ReadDotenvTestCase(unittest.TestCase): + def test_defaults_to_dotenv(self): read_dotenv() self.assertEqual(os.environ.get('DOTENV'), 'true') @@ -132,6 +137,18 @@ def test_warns_if_file_does_not_exist(self): "Not reading .does_not_exist - it doesn't exist." ) + def test_warns_if_values_not_exist(self): + with warnings.catch_warnings(record=True) as w: + read_dotenv('.env', safe=True) + + self.assertEqual(len(w), 1) + self.assertTrue(w[0].category is UserWarning) + self.assertEqual( + str(w[0].message), + "The following variables were defined in .env.example but " + "are not present in the environment:\n DOTENV_EXAMPLE" + ) + class ParseDotenvDirectoryTestCase(unittest.TestCase): """Test parsing a dotenv file given the directory where it lives""" @@ -152,3 +169,57 @@ def tearDown(self): def test_can_read_dotenv_given_its_directory(self): read_dotenv(self.dotenv_dir) self.assertEqual(os.environ.get('DOTENV'), 'true') + + +class ReadDotenvExampleTestCase(unittest.TestCase): + + def setUp(self): + # Define our dotenv directory + self.dotenv_dir = os.path.join( + os.path.dirname(__file__), 'dotenv_dir') + self.dir_not_found = os.path.join(self.dotenv_dir, 'dir_not_found') + # Create the directories + os.mkdir(self.dotenv_dir) + os.mkdir(self.dir_not_found) + # Copy the test .env file and .env.example file to our new directory + for file in ('.env', '.env.example'): + shutil.copy2(os.path.abspath(file), self.dotenv_dir) + + def tearDown(self): + for dir_name in (self.dotenv_dir, self.dir_not_found): + if os.path.exists(dir_name): + shutil.rmtree(dir_name) + + def test_read_example(self): + dotenv_example = _read_dotenv_example(self.dotenv_dir) + expected = {'DOTENV': 'true', 'DOTENV_EXAMPLE': 'true'} + self.assertDictEqual(dotenv_example, expected) + + def test_file_not_exists(self): + dotenv_example = _read_dotenv_example(self.dir_not_found) + self.assertDictEqual(dotenv_example, {}) + + +class CheckSafeTestCase(unittest.TestCase): + + def setUp(self): + # Define our dotenv directory + self.dotenv_dir = os.path.join( + os.path.dirname(__file__), 'dotenv_dir') + # Create the directory + os.mkdir(self.dotenv_dir) + # Copy the test .env file to our new directory + for file in ('.env', '.env.example'): + shutil.copy2(os.path.abspath(file), self.dotenv_dir) + + def tearDown(self): + if os.path.exists(self.dotenv_dir): + shutil.rmtree(self.dotenv_dir) + + def test_return(self): + read_dotenv(self.dotenv_dir) + dotenv_example = _read_dotenv_example(self.dotenv_dir) + keys_not_exist = _check_safe(dotenv_example) + + expected = ['DOTENV_EXAMPLE'] + self.assertEqual(keys_not_exist, expected)