Skip to content

Commit 00a3a9a

Browse files
committed
Add envcontext helper
1 parent 495e21b commit 00a3a9a

File tree

3 files changed

+167
-0
lines changed

3 files changed

+167
-0
lines changed

pre_commit/envcontext.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from __future__ import absolute_import
2+
from __future__ import unicode_literals
3+
4+
import collections
5+
import contextlib
6+
import os
7+
8+
from pre_commit import five
9+
10+
11+
UNSET = collections.namedtuple('UNSET', ())()
12+
13+
14+
Var = collections.namedtuple('Var', ('name', 'default'))
15+
setattr(Var.__new__, five.defaults_attr, ('',))
16+
17+
18+
def format_env(parts, env):
19+
return ''.join(
20+
env.get(part.name, part.default)
21+
if isinstance(part, Var)
22+
else part
23+
for part in parts
24+
)
25+
26+
27+
@contextlib.contextmanager
28+
def envcontext(patch, _env=None):
29+
"""In this context, `os.environ` is modified according to `patch`.
30+
31+
`patch` is an iterable of 2-tuples (key, value):
32+
`key`: string
33+
`value`:
34+
- string: `environ[key] == value` inside the context.
35+
- UNSET: `key not in environ` inside the context.
36+
- template: A template is a tuple of strings and Var which will be
37+
replaced with the previous environment
38+
"""
39+
env = os.environ if _env is None else _env
40+
before = env.copy()
41+
42+
for k, v in patch:
43+
if v is UNSET:
44+
env.pop(k, None)
45+
elif isinstance(v, tuple):
46+
env[k] = format_env(v, before)
47+
else:
48+
env[k] = v
49+
50+
try:
51+
yield
52+
finally:
53+
env.clear()
54+
env.update(before)

pre_commit/five.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ def n(s):
1212
return s
1313
else:
1414
return s.encode('UTF-8')
15+
16+
defaults_attr = 'func_defaults'
1517
else: # pragma: no cover (PY3 only)
1618
text = str
1719

@@ -21,6 +23,8 @@ def n(s):
2123
else:
2224
return s.decode('UTF-8')
2325

26+
defaults_attr = '__defaults__'
27+
2428

2529
def to_text(s):
2630
return s if isinstance(s, text) else s.decode('UTF-8')

tests/envcontext_test.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from __future__ import absolute_import
2+
from __future__ import unicode_literals
3+
4+
import os
5+
6+
import mock
7+
import pytest
8+
9+
from pre_commit.envcontext import envcontext
10+
from pre_commit.envcontext import UNSET
11+
from pre_commit.envcontext import Var
12+
13+
14+
def _test(**kwargs):
15+
before = kwargs.pop('before')
16+
patch = kwargs.pop('patch')
17+
expected = kwargs.pop('expected')
18+
assert not kwargs
19+
20+
env = before.copy()
21+
with envcontext(patch, _env=env):
22+
assert env == expected
23+
assert env == before
24+
25+
26+
def test_trivial():
27+
_test(before={}, patch={}, expected={})
28+
29+
30+
def test_noop():
31+
_test(before={'foo': 'bar'}, patch=(), expected={'foo': 'bar'})
32+
33+
34+
def test_adds():
35+
_test(before={}, patch=[('foo', 'bar')], expected={'foo': 'bar'})
36+
37+
38+
def test_overrides():
39+
_test(
40+
before={'foo': 'baz'},
41+
patch=[('foo', 'bar')],
42+
expected={'foo': 'bar'},
43+
)
44+
45+
46+
def test_unset_but_nothing_to_unset():
47+
_test(before={}, patch=[('foo', UNSET)], expected={})
48+
49+
50+
def test_unset_things_to_remove():
51+
_test(
52+
before={'PYTHONHOME': ''},
53+
patch=[('PYTHONHOME', UNSET)],
54+
expected={},
55+
)
56+
57+
58+
def test_templated_environment_variable_missing():
59+
_test(
60+
before={},
61+
patch=[('PATH', ('~/bin:', Var('PATH')))],
62+
expected={'PATH': '~/bin:'},
63+
)
64+
65+
66+
def test_templated_environment_variable_defaults():
67+
_test(
68+
before={},
69+
patch=[('PATH', ('~/bin:', Var('PATH', default='/bin')))],
70+
expected={'PATH': '~/bin:/bin'},
71+
)
72+
73+
74+
def test_templated_environment_variable_there():
75+
_test(
76+
before={'PATH': '/usr/local/bin:/usr/bin'},
77+
patch=[('PATH', ('~/bin:', Var('PATH')))],
78+
expected={'PATH': '~/bin:/usr/local/bin:/usr/bin'},
79+
)
80+
81+
82+
def test_templated_environ_sources_from_previous():
83+
_test(
84+
before={'foo': 'bar'},
85+
patch=(
86+
('foo', 'baz'),
87+
('herp', ('foo: ', Var('foo'))),
88+
),
89+
expected={'foo': 'baz', 'herp': 'foo: bar'},
90+
)
91+
92+
93+
def test_exception_safety():
94+
class MyError(RuntimeError):
95+
pass
96+
97+
env = {}
98+
with pytest.raises(MyError):
99+
with envcontext([('foo', 'bar')], _env=env):
100+
raise MyError()
101+
assert env == {}
102+
103+
104+
def test_integration_os_environ():
105+
with mock.patch.dict(os.environ, {'FOO': 'bar'}, clear=True):
106+
assert os.environ == {'FOO': 'bar'}
107+
with envcontext([('HERP', 'derp')]):
108+
assert os.environ == {'FOO': 'bar', 'HERP': 'derp'}
109+
assert os.environ == {'FOO': 'bar'}

0 commit comments

Comments
 (0)