-
-
Notifications
You must be signed in to change notification settings - Fork 315
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
1 parent
caf1ce2
commit a8871d0
Showing
5 changed files
with
320 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import os | ||
from collections.abc import MutableMapping | ||
|
||
|
||
class FileAwareMapping(MutableMapping): | ||
""" | ||
A mapping that wraps os.environ, first checking for the existance of a key | ||
appended with ``_FILE`` whenever reading a value. If a matching file key is | ||
found then the value is instead read from the file system at this location. | ||
By default, values read from the file system are cached so future lookups | ||
do not hit the disk again. | ||
A ``_FILE`` key has higher precidence than a value is set directly in the | ||
environment, and an exception is raised if the file can not be found. | ||
""" | ||
|
||
def __init__(self, env=None, cache=True): | ||
""" | ||
Initialize the mapping. | ||
:param env: | ||
where to read environment variables from (defaults to | ||
``os.environ``) | ||
:param cache: | ||
cache environment variables read from the file system (defaults to | ||
``True``) | ||
""" | ||
self.env = env if env is not None else os.environ | ||
self.cache = cache | ||
self.files_cache = {} | ||
|
||
def __getitem__(self, key): | ||
if self.cache and key in self.files_cache: | ||
return self.files_cache[key] | ||
key_file = self.env.get(key + "_FILE") | ||
if key_file: | ||
with open(key_file) as f: | ||
value = f.read() | ||
if self.cache: | ||
self.files_cache[key] = value | ||
return value | ||
return self.env[key] | ||
|
||
def __iter__(self): | ||
""" | ||
Iterate all keys, also always including the shortened key if ``_FILE`` | ||
keys are found. | ||
""" | ||
for key in self.env: | ||
yield key | ||
if key.endswith("_FILE"): | ||
no_file_key = key[:-5] | ||
if no_file_key and no_file_key not in self.env: | ||
yield no_file_key | ||
|
||
def __len__(self): | ||
""" | ||
Return the length of the file, also always counting shortened keys for | ||
any ``_FILE`` key found. | ||
""" | ||
return len(tuple(iter(self))) | ||
|
||
def __setitem__(self, key, value): | ||
self.env[key] = value | ||
if self.cache and key.endswith("_FILE"): | ||
no_file_key = key[:-5] | ||
if no_file_key and no_file_key in self.files_cache: | ||
del self.files_cache[no_file_key] | ||
|
||
def __delitem__(self, key): | ||
file_key = key + "_FILE" | ||
if file_key in self.env: | ||
del self[file_key] | ||
if key in self.env: | ||
del self.env[key] | ||
return | ||
if self.cache and key.endswith("_FILE"): | ||
no_file_key = key[:-5] | ||
if no_file_key and no_file_key in self.files_cache: | ||
del self.files_cache[no_file_key] | ||
del self.env[key] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
import os | ||
import tempfile | ||
from contextlib import contextmanager | ||
|
||
import environ | ||
import pytest | ||
|
||
|
||
@contextmanager | ||
def make_temp_file(text): | ||
with tempfile.NamedTemporaryFile("w", delete=False) as f: | ||
f.write(text) | ||
f.close() | ||
try: | ||
yield f.name | ||
finally: | ||
if os.path.exists(f.name): | ||
os.unlink(f.name) | ||
|
||
|
||
@pytest.fixture | ||
def tmp_f(): | ||
with make_temp_file(text="fish") as f_name: | ||
yield f_name | ||
|
||
|
||
def test_mapping(tmp_f): | ||
env = environ.FileAwareMapping(env={"ANIMAL_FILE": tmp_f}) | ||
assert env["ANIMAL"] == "fish" | ||
|
||
|
||
def test_precidence(tmp_f): | ||
env = environ.FileAwareMapping( | ||
env={ | ||
"ANIMAL_FILE": tmp_f, | ||
"ANIMAL": "cat", | ||
} | ||
) | ||
assert env["ANIMAL"] == "fish" | ||
|
||
|
||
def test_missing_file_raises_exception(): | ||
env = environ.FileAwareMapping(env={"ANIMAL_FILE": "non-existant-file"}) | ||
with pytest.raises(FileNotFoundError): | ||
env["ANIMAL"] | ||
|
||
|
||
def test_iter(): | ||
env = environ.FileAwareMapping( | ||
env={ | ||
"ANIMAL_FILE": "some-file", | ||
"VEGETABLE": "leek", | ||
"VEGETABLE_FILE": "some-vegetable-file", | ||
} | ||
) | ||
keys = set(env) | ||
assert keys == {"ANIMAL_FILE", "ANIMAL", "VEGETABLE", "VEGETABLE_FILE"} | ||
assert "ANIMAL" in keys | ||
|
||
|
||
def test_len(): | ||
env = environ.FileAwareMapping( | ||
env={ | ||
"ANIMAL_FILE": "some-file", | ||
"VEGETABLE": "leek", | ||
"VEGETABLE_FILE": "some-vegetable-file", | ||
} | ||
) | ||
assert len(env) == 4 | ||
|
||
|
||
def test_cache(tmp_f): | ||
env = environ.FileAwareMapping(env={"ANIMAL_FILE": tmp_f}) | ||
assert env["ANIMAL"] == "fish" | ||
|
||
with open(tmp_f, "w") as f: | ||
f.write("cat") | ||
assert env["ANIMAL"] == "fish" | ||
|
||
os.unlink(tmp_f) | ||
assert not os.path.exists(env["ANIMAL_FILE"]) | ||
assert env["ANIMAL"] == "fish" | ||
|
||
|
||
def test_no_cache(tmp_f): | ||
env = environ.FileAwareMapping( | ||
cache=False, | ||
env={"ANIMAL_FILE": tmp_f}, | ||
) | ||
assert env["ANIMAL"] == "fish" | ||
|
||
with open(tmp_f, "w") as f: | ||
f.write("cat") | ||
assert env["ANIMAL"] == "cat" | ||
|
||
os.unlink(tmp_f) | ||
assert not os.path.exists(env["ANIMAL_FILE"]) | ||
with pytest.raises(FileNotFoundError): | ||
assert env["ANIMAL"] | ||
|
||
|
||
def test_setdefault(tmp_f): | ||
env = environ.FileAwareMapping(env={"ANIMAL_FILE": tmp_f}) | ||
assert env.setdefault("FRUIT", "apple") == "apple" | ||
assert env.setdefault("ANIMAL", "cat") == "fish" | ||
assert env.env == {"ANIMAL_FILE": tmp_f, "FRUIT": "apple"} | ||
|
||
|
||
class TestDelItem: | ||
def test_del_key(self): | ||
env = environ.FileAwareMapping(env={"FRUIT": "apple"}) | ||
del env["FRUIT"] | ||
with pytest.raises(KeyError): | ||
env["FRUIT"] | ||
|
||
def test_del_key_with_file_key(self): | ||
env = environ.FileAwareMapping(env={"ANIMAL_FILE": "some-file"}) | ||
del env["ANIMAL"] | ||
with pytest.raises(KeyError): | ||
env["ANIMAL"] | ||
|
||
def test_del_shadowed_key_with_file_key(self): | ||
env = environ.FileAwareMapping( | ||
env={"ANIMAL_FILE": "some-file", "ANIMAL": "cat"} | ||
) | ||
del env["ANIMAL"] | ||
with pytest.raises(KeyError): | ||
env["ANIMAL"] | ||
|
||
def test_del_file_key(self): | ||
env = environ.FileAwareMapping( | ||
env={ | ||
"ANIMAL_FILE": "some-file", | ||
"ANIMAL": "fish", | ||
} | ||
) | ||
del env["ANIMAL_FILE"] | ||
assert env["ANIMAL"] == "fish" | ||
|
||
def test_del_file_key_clears_cache(self, tmp_f): | ||
env = environ.FileAwareMapping( | ||
env={ | ||
"ANIMAL_FILE": tmp_f, | ||
"ANIMAL": "cat", | ||
} | ||
) | ||
assert env["ANIMAL"] == "fish" | ||
del env["ANIMAL_FILE"] | ||
assert env["ANIMAL"] == "cat" | ||
|
||
|
||
class TestSetItem: | ||
def test_set_key(self): | ||
env = environ.FileAwareMapping(env={"FRUIT": "apple"}) | ||
env["FRUIT"] = "banana" | ||
assert env["FRUIT"] == "banana" | ||
|
||
def test_cant_override_key_with_file_key(self, tmp_f): | ||
env = environ.FileAwareMapping( | ||
env={ | ||
"FRUIT": "apple", | ||
"FRUIT_FILE": tmp_f, | ||
} | ||
) | ||
with open(tmp_f, "w") as f: | ||
f.write("banana") | ||
env["FRUIT"] = "cucumber" | ||
assert env["FRUIT"] == "banana" | ||
|
||
def test_set_file_key(self, tmp_f): | ||
env = environ.FileAwareMapping(env={"ANIMAL": "cat"}) | ||
env["ANIMAL_FILE"] = tmp_f | ||
assert env["ANIMAL"] == "fish" | ||
|
||
def test_change_file_key_clears_cache(self, tmp_f): | ||
env = environ.FileAwareMapping(env={"ANIMAL_FILE": tmp_f}) | ||
assert env["ANIMAL"] == "fish" | ||
with make_temp_file(text="cat") as new_tmp_f: | ||
env["ANIMAL_FILE"] = new_tmp_f | ||
assert env["ANIMAL"] == "cat" |