|
| 1 | +import io |
| 2 | +import logging |
| 3 | +import os |
| 4 | +import sys |
| 5 | +import textwrap |
| 6 | +from unittest import mock |
| 7 | + |
| 8 | +import pytest |
| 9 | +import sh |
| 10 | + |
| 11 | +import dotenv |
| 12 | + |
| 13 | + |
| 14 | +def test_set_key_no_file(tmp_path): |
| 15 | + nx_path = tmp_path / "nx" |
| 16 | + logger = logging.getLogger("dotenv.main") |
| 17 | + |
| 18 | + with mock.patch.object(logger, "warning"): |
| 19 | + result = dotenv.set_key(nx_path, "foo", "bar") |
| 20 | + |
| 21 | + assert result == (True, "foo", "bar") |
| 22 | + assert nx_path.exists() |
| 23 | + |
| 24 | + |
| 25 | +@pytest.mark.parametrize( |
| 26 | + "before,key,value,expected,after", |
| 27 | + [ |
| 28 | + ("", "a", "", (True, "a", ""), "a=''\n"), |
| 29 | + ("", "a", "b", (True, "a", "b"), "a='b'\n"), |
| 30 | + ("", "a", "'b'", (True, "a", "'b'"), "a='\\'b\\''\n"), |
| 31 | + ("", "a", '"b"', (True, "a", '"b"'), "a='\"b\"'\n"), |
| 32 | + ("", "a", "b'c", (True, "a", "b'c"), "a='b\\'c'\n"), |
| 33 | + ("", "a", 'b"c', (True, "a", 'b"c'), "a='b\"c'\n"), |
| 34 | + ("a=b", "a", "c", (True, "a", "c"), "a='c'\n"), |
| 35 | + ("a=b\n", "a", "c", (True, "a", "c"), "a='c'\n"), |
| 36 | + ("a=b\n\n", "a", "c", (True, "a", "c"), "a='c'\n\n"), |
| 37 | + ("a=b\nc=d", "a", "e", (True, "a", "e"), "a='e'\nc=d"), |
| 38 | + ("a=b\nc=d\ne=f", "c", "g", (True, "c", "g"), "a=b\nc='g'\ne=f"), |
| 39 | + ("a=b\n", "c", "d", (True, "c", "d"), "a=b\nc='d'\n"), |
| 40 | + ("a=b", "c", "d", (True, "c", "d"), "a=b\nc='d'\n"), |
| 41 | + ], |
| 42 | +) |
| 43 | +def test_set_key(dotenv_path, before, key, value, expected, after): |
| 44 | + logger = logging.getLogger("dotenv.main") |
| 45 | + dotenv_path.write_text(before) |
| 46 | + |
| 47 | + with mock.patch.object(logger, "warning") as mock_warning: |
| 48 | + result = dotenv.set_key(dotenv_path, key, value) |
| 49 | + |
| 50 | + assert result == expected |
| 51 | + assert dotenv_path.read_text() == after |
| 52 | + mock_warning.assert_not_called() |
| 53 | + |
| 54 | + |
| 55 | +def test_set_key_encoding(dotenv_path): |
| 56 | + encoding = "latin-1" |
| 57 | + |
| 58 | + result = dotenv.set_key(dotenv_path, "a", "é", encoding=encoding) |
| 59 | + |
| 60 | + assert result == (True, "a", "é") |
| 61 | + assert dotenv_path.read_text(encoding=encoding) == "a='é'\n" |
| 62 | + |
| 63 | + |
| 64 | +def test_set_key_permission_error(dotenv_path): |
| 65 | + dotenv_path.chmod(0o000) |
| 66 | + |
| 67 | + with pytest.raises(Exception): |
| 68 | + dotenv.set_key(dotenv_path, "a", "b") |
| 69 | + |
| 70 | + dotenv_path.chmod(0o600) |
| 71 | + assert dotenv_path.read_text() == "" |
| 72 | + |
| 73 | + |
| 74 | +def test_get_key_no_file(tmp_path): |
| 75 | + nx_path = tmp_path / "nx" |
| 76 | + logger = logging.getLogger("dotenv.main") |
| 77 | + |
| 78 | + with ( |
| 79 | + mock.patch.object(logger, "info") as mock_info, |
| 80 | + mock.patch.object(logger, "warning") as mock_warning, |
| 81 | + ): |
| 82 | + result = dotenv.get_key(nx_path, "foo") |
| 83 | + |
| 84 | + assert result is None |
| 85 | + mock_info.assert_has_calls( |
| 86 | + calls=[ |
| 87 | + mock.call("python-dotenv could not find configuration file %s.", nx_path) |
| 88 | + ], |
| 89 | + ) |
| 90 | + mock_warning.assert_has_calls( |
| 91 | + calls=[mock.call("Key %s not found in %s.", "foo", nx_path)], |
| 92 | + ) |
| 93 | + |
| 94 | + |
| 95 | +def test_get_key_not_found(dotenv_path): |
| 96 | + logger = logging.getLogger("dotenv.main") |
| 97 | + |
| 98 | + with mock.patch.object(logger, "warning") as mock_warning: |
| 99 | + result = dotenv.get_key(dotenv_path, "foo") |
| 100 | + |
| 101 | + assert result is None |
| 102 | + mock_warning.assert_called_once_with("Key %s not found in %s.", "foo", dotenv_path) |
| 103 | + |
| 104 | + |
| 105 | +def test_get_key_ok(dotenv_path): |
| 106 | + logger = logging.getLogger("dotenv.main") |
| 107 | + dotenv_path.write_text("foo=bar") |
| 108 | + |
| 109 | + with mock.patch.object(logger, "warning") as mock_warning: |
| 110 | + result = dotenv.get_key(dotenv_path, "foo") |
| 111 | + |
| 112 | + assert result == "bar" |
| 113 | + mock_warning.assert_not_called() |
| 114 | + |
| 115 | + |
| 116 | +def test_get_key_encoding(dotenv_path): |
| 117 | + encoding = "latin-1" |
| 118 | + dotenv_path.write_text("é=è", encoding=encoding) |
| 119 | + |
| 120 | + result = dotenv.get_key(dotenv_path, "é", encoding=encoding) |
| 121 | + |
| 122 | + assert result == "è" |
| 123 | + |
| 124 | + |
| 125 | +def test_get_key_none(dotenv_path): |
| 126 | + logger = logging.getLogger("dotenv.main") |
| 127 | + dotenv_path.write_text("foo") |
| 128 | + |
| 129 | + with mock.patch.object(logger, "warning") as mock_warning: |
| 130 | + result = dotenv.get_key(dotenv_path, "foo") |
| 131 | + |
| 132 | + assert result is None |
| 133 | + mock_warning.assert_not_called() |
| 134 | + |
| 135 | + |
| 136 | +def test_unset_with_value(dotenv_path): |
| 137 | + logger = logging.getLogger("dotenv.main") |
| 138 | + dotenv_path.write_text("a=b\nc=d") |
| 139 | + |
| 140 | + with mock.patch.object(logger, "warning") as mock_warning: |
| 141 | + result = dotenv.unset_key(dotenv_path, "a") |
| 142 | + |
| 143 | + assert result == (True, "a") |
| 144 | + assert dotenv_path.read_text() == "c=d" |
| 145 | + mock_warning.assert_not_called() |
| 146 | + |
| 147 | + |
| 148 | +def test_unset_no_value(dotenv_path): |
| 149 | + logger = logging.getLogger("dotenv.main") |
| 150 | + dotenv_path.write_text("foo") |
| 151 | + |
| 152 | + with mock.patch.object(logger, "warning") as mock_warning: |
| 153 | + result = dotenv.unset_key(dotenv_path, "foo") |
| 154 | + |
| 155 | + assert result == (True, "foo") |
| 156 | + assert dotenv_path.read_text() == "" |
| 157 | + mock_warning.assert_not_called() |
| 158 | + |
| 159 | + |
| 160 | +def test_unset_encoding(dotenv_path): |
| 161 | + encoding = "latin-1" |
| 162 | + dotenv_path.write_text("é=x", encoding=encoding) |
| 163 | + |
| 164 | + result = dotenv.unset_key(dotenv_path, "é", encoding=encoding) |
| 165 | + |
| 166 | + assert result == (True, "é") |
| 167 | + assert dotenv_path.read_text(encoding=encoding) == "" |
| 168 | + |
| 169 | + |
| 170 | +def test_set_key_unauthorized_file(dotenv_path): |
| 171 | + dotenv_path.chmod(0o000) |
| 172 | + |
| 173 | + with pytest.raises(PermissionError): |
| 174 | + dotenv.set_key(dotenv_path, "a", "x") |
| 175 | + |
| 176 | + |
| 177 | +def test_unset_non_existent_file(tmp_path): |
| 178 | + nx_path = tmp_path / "nx" |
| 179 | + logger = logging.getLogger("dotenv.main") |
| 180 | + |
| 181 | + with mock.patch.object(logger, "warning") as mock_warning: |
| 182 | + result = dotenv.unset_key(nx_path, "foo") |
| 183 | + |
| 184 | + assert result == (None, "foo") |
| 185 | + mock_warning.assert_called_once_with( |
| 186 | + "Can't delete from %s - it doesn't exist.", |
| 187 | + nx_path, |
| 188 | + ) |
| 189 | + |
| 190 | + |
| 191 | +def prepare_file_hierarchy(path): |
| 192 | + """ |
| 193 | + Create a temporary folder structure like the following: |
| 194 | +
|
| 195 | + test_find_dotenv0/ |
| 196 | + └── child1 |
| 197 | + ├── child2 |
| 198 | + │ └── child3 |
| 199 | + │ └── child4 |
| 200 | + └── .env |
| 201 | +
|
| 202 | + Then try to automatically `find_dotenv` starting in `child4` |
| 203 | + """ |
| 204 | + |
| 205 | + leaf = path / "child1" / "child2" / "child3" / "child4" |
| 206 | + leaf.mkdir(parents=True, exist_ok=True) |
| 207 | + return leaf |
| 208 | + |
| 209 | + |
| 210 | +def test_find_dotenv_no_file_raise(tmp_path): |
| 211 | + leaf = prepare_file_hierarchy(tmp_path) |
| 212 | + os.chdir(leaf) |
| 213 | + |
| 214 | + with pytest.raises(IOError): |
| 215 | + dotenv.find_dotenv(raise_error_if_not_found=True, usecwd=True) |
| 216 | + |
| 217 | + |
| 218 | +def test_find_dotenv_no_file_no_raise(tmp_path): |
| 219 | + leaf = prepare_file_hierarchy(tmp_path) |
| 220 | + os.chdir(leaf) |
| 221 | + |
| 222 | + result = dotenv.find_dotenv(usecwd=True) |
| 223 | + |
| 224 | + assert result == "" |
| 225 | + |
| 226 | + |
| 227 | +def test_find_dotenv_found(tmp_path): |
| 228 | + leaf = prepare_file_hierarchy(tmp_path) |
| 229 | + os.chdir(leaf) |
| 230 | + dotenv_path = tmp_path / ".env" |
| 231 | + dotenv_path.write_bytes(b"TEST=test\n") |
| 232 | + |
| 233 | + result = dotenv.find_dotenv(usecwd=True) |
| 234 | + |
| 235 | + assert result == str(dotenv_path) |
| 236 | + |
| 237 | + |
| 238 | +@mock.patch.dict(os.environ, {}, clear=True) |
| 239 | +def test_load_dotenv_existing_file(dotenv_path): |
| 240 | + dotenv_path.write_text("a=b") |
| 241 | + |
| 242 | + result = dotenv.load_dotenv(dotenv_path) |
| 243 | + |
| 244 | + assert result is True |
| 245 | + assert os.environ == {"a": "b"} |
| 246 | + |
| 247 | + |
| 248 | +def test_load_dotenv_no_file_verbose(): |
| 249 | + logger = logging.getLogger("dotenv.main") |
| 250 | + |
| 251 | + with mock.patch.object(logger, "info") as mock_info: |
| 252 | + result = dotenv.load_dotenv(".does_not_exist", verbose=True) |
| 253 | + |
| 254 | + assert result is False |
| 255 | + mock_info.assert_called_once_with( |
| 256 | + "python-dotenv could not find configuration file %s.", ".does_not_exist" |
| 257 | + ) |
| 258 | + |
| 259 | + |
| 260 | +@mock.patch.dict(os.environ, {"a": "c"}, clear=True) |
| 261 | +def test_load_dotenv_existing_variable_no_override(dotenv_path): |
| 262 | + dotenv_path.write_text("a=b") |
| 263 | + |
| 264 | + result = dotenv.load_dotenv(dotenv_path, override=False) |
| 265 | + |
| 266 | + assert result is True |
| 267 | + assert os.environ == {"a": "c"} |
| 268 | + |
| 269 | + |
| 270 | +@mock.patch.dict(os.environ, {"a": "c"}, clear=True) |
| 271 | +def test_load_dotenv_existing_variable_override(dotenv_path): |
| 272 | + dotenv_path.write_text("a=b") |
| 273 | + |
| 274 | + result = dotenv.load_dotenv(dotenv_path, override=True) |
| 275 | + |
| 276 | + assert result is True |
| 277 | + assert os.environ == {"a": "b"} |
| 278 | + |
| 279 | + |
| 280 | +@mock.patch.dict(os.environ, {"a": "c"}, clear=True) |
| 281 | +def test_load_dotenv_redefine_var_used_in_file_no_override(dotenv_path): |
| 282 | + dotenv_path.write_text('a=b\nd="${a}"') |
| 283 | + |
| 284 | + result = dotenv.load_dotenv(dotenv_path) |
| 285 | + |
| 286 | + assert result is True |
| 287 | + assert os.environ == {"a": "c", "d": "c"} |
| 288 | + |
| 289 | + |
| 290 | +@mock.patch.dict(os.environ, {"a": "c"}, clear=True) |
| 291 | +def test_load_dotenv_redefine_var_used_in_file_with_override(dotenv_path): |
| 292 | + dotenv_path.write_text('a=b\nd="${a}"') |
| 293 | + |
| 294 | + result = dotenv.load_dotenv(dotenv_path, override=True) |
| 295 | + |
| 296 | + assert result is True |
| 297 | + assert os.environ == {"a": "b", "d": "b"} |
| 298 | + |
| 299 | + |
| 300 | +@mock.patch.dict(os.environ, {}, clear=True) |
| 301 | +def test_load_dotenv_string_io_utf_8(): |
| 302 | + stream = io.StringIO("a=à") |
| 303 | + |
| 304 | + result = dotenv.load_dotenv(stream=stream) |
| 305 | + |
| 306 | + assert result is True |
| 307 | + assert os.environ == {"a": "à"} |
| 308 | + |
| 309 | + |
| 310 | +@mock.patch.dict(os.environ, {}, clear=True) |
| 311 | +def test_load_dotenv_file_stream(dotenv_path): |
| 312 | + dotenv_path.write_text("a=b") |
| 313 | + |
| 314 | + with dotenv_path.open() as f: |
| 315 | + result = dotenv.load_dotenv(stream=f) |
| 316 | + |
| 317 | + assert result is True |
| 318 | + assert os.environ == {"a": "b"} |
| 319 | + |
| 320 | + |
| 321 | +def test_load_dotenv_in_current_dir(tmp_path): |
| 322 | + dotenv_path = tmp_path / ".env" |
| 323 | + dotenv_path.write_bytes(b"a=b") |
| 324 | + code_path = tmp_path / "code.py" |
| 325 | + code_path.write_text( |
| 326 | + textwrap.dedent(""" |
| 327 | + import dotenv |
| 328 | + import os |
| 329 | +
|
| 330 | + dotenv.load_dotenv(verbose=True) |
| 331 | + print(os.environ['a']) |
| 332 | + """) |
| 333 | + ) |
| 334 | + os.chdir(tmp_path) |
| 335 | + |
| 336 | + result = sh.Command(sys.executable)(code_path) |
| 337 | + |
| 338 | + assert result == "b\n" |
| 339 | + |
| 340 | + |
| 341 | +def test_dotenv_values_file(dotenv_path): |
| 342 | + dotenv_path.write_text("a=b") |
| 343 | + |
| 344 | + result = dotenv.dotenv_values(dotenv_path) |
| 345 | + |
| 346 | + assert result == {"a": "b"} |
| 347 | + |
| 348 | + |
| 349 | +@pytest.mark.parametrize( |
| 350 | + "env,string,interpolate,expected", |
| 351 | + [ |
| 352 | + # Defined in environment, with and without interpolation |
| 353 | + ({"b": "c"}, "a=$b", False, {"a": "$b"}), |
| 354 | + ({"b": "c"}, "a=$b", True, {"a": "$b"}), |
| 355 | + ({"b": "c"}, "a=${b}", False, {"a": "${b}"}), |
| 356 | + ({"b": "c"}, "a=${b}", True, {"a": "c"}), |
| 357 | + ({"b": "c"}, "a=${b:-d}", False, {"a": "${b:-d}"}), |
| 358 | + ({"b": "c"}, "a=${b:-d}", True, {"a": "c"}), |
| 359 | + # Defined in file |
| 360 | + ({}, "b=c\na=${b}", True, {"a": "c", "b": "c"}), |
| 361 | + # Undefined |
| 362 | + ({}, "a=${b}", True, {"a": ""}), |
| 363 | + ({}, "a=${b:-d}", True, {"a": "d"}), |
| 364 | + # With quotes |
| 365 | + ({"b": "c"}, 'a="${b}"', True, {"a": "c"}), |
| 366 | + ({"b": "c"}, "a='${b}'", True, {"a": "c"}), |
| 367 | + # With surrounding text |
| 368 | + ({"b": "c"}, "a=x${b}y", True, {"a": "xcy"}), |
| 369 | + # Self-referential |
| 370 | + ({"a": "b"}, "a=${a}", True, {"a": "b"}), |
| 371 | + ({}, "a=${a}", True, {"a": ""}), |
| 372 | + ({"a": "b"}, "a=${a:-c}", True, {"a": "b"}), |
| 373 | + ({}, "a=${a:-c}", True, {"a": "c"}), |
| 374 | + # Reused |
| 375 | + ({"b": "c"}, "a=${b}${b}", True, {"a": "cc"}), |
| 376 | + # Re-defined and used in file |
| 377 | + ({"b": "c"}, "b=d\na=${b}", True, {"a": "d", "b": "d"}), |
| 378 | + ({}, "a=b\na=c\nd=${a}", True, {"a": "c", "d": "c"}), |
| 379 | + ({}, "a=b\nc=${a}\nd=e\nc=${d}", True, {"a": "b", "c": "e", "d": "e"}), |
| 380 | + ], |
| 381 | +) |
| 382 | +def test_dotenv_values_string_io(env, string, interpolate, expected): |
| 383 | + with mock.patch.dict(os.environ, env, clear=True): |
| 384 | + stream = io.StringIO(string) |
| 385 | + stream.seek(0) |
| 386 | + |
| 387 | + result = dotenv.dotenv_values(stream=stream, interpolate=interpolate) |
| 388 | + |
| 389 | + assert result == expected |
| 390 | + |
| 391 | + |
| 392 | +def test_dotenv_values_file_stream(dotenv_path): |
| 393 | + dotenv_path.write_text("a=b") |
| 394 | + |
| 395 | + with dotenv_path.open() as f: |
| 396 | + result = dotenv.dotenv_values(stream=f) |
| 397 | + |
| 398 | + assert result == {"a": "b"} |
0 commit comments