Merge lp:~vila/bzr/907279-override-from-env into lp:bzr

Proposed by Vincent Ladeuil
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merged at revision: 6411
Proposed branch: lp:~vila/bzr/907279-override-from-env
Merge into: lp:bzr
Diff against target: 249 lines (+92/-41)
4 files modified
bzrlib/config.py (+48/-35)
bzrlib/tests/test_config.py (+36/-6)
doc/developers/configuration.txt (+4/-0)
doc/en/release-notes/bzr-2.5.txt (+4/-0)
To merge this branch: bzr merge lp:~vila/bzr/907279-override-from-env
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) Needs Fixing
Review via email: mp+86737@code.launchpad.net

Commit message

Configuration option value can be overridden by os environ variables

Description of the change

This provides Option.override_from_env as a sibling of default_from_env but
specifiying environ variables that overrides all configuration options
definitions.

I.e. it matches several use cases, the later encountered being BZR_EMAIL
which I used as an example here.

I was strongly tempted to add an 'env_encoding' parameter to complete the
set but would need some feedback from mgz about the functions to be used in
osutils to get there.

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

197 + def setUp(self):
198 + super(EmailOptionTests, self).setUp()
This seems unnecessary ?

review: Needs Fixing
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

There are two whitespaces around 169/170 in the patch, there should just be one.

Revision history for this message
Vincent Ladeuil (vila) wrote :

sent to pqm by email

Revision history for this message
Martin Packman (gz) wrote :

The use of osutils.get_user_encoding is completely fine here, will be the locale set on nix, and the same codepage on windows as was used to encode unicode value to bytes by the OS. The main things to worry about are the error handling (on nix it may throw a UnicodeEncodeError as it's possible to have arbitrary bytes in the environment), and could use the new win32utils function to get a unicode environment variable. Perhaps what's needed is something that will do both, decode the value on nix and give a pretty error, and get unicode straight on windows. I was avoiding this as it should be possible to handle many values (and eventually paths) without decoding from bytes on nix, but perhaps it's the right level for the current status of bzrlib.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/config.py'
2--- bzrlib/config.py 2011-12-27 12:18:36 +0000
3+++ bzrlib/config.py 2012-01-03 09:54:28 +0000
4@@ -1652,17 +1652,6 @@
5 raise errors.NoWhoami()
6
7
8-def email_from_store(unicode_str):
9- """Unlike other env vars, BZR_EMAIL takes precedence over config settings.
10-
11- Whatever comes from a config file is then overridden.
12- """
13- value = os.environ.get('BZR_EMAIL')
14- if value:
15- return value.decode(osutils.get_user_encoding())
16- return unicode_str
17-
18-
19 def _auto_user_id():
20 """Calculate automatic user identification.
21
22@@ -2343,13 +2332,16 @@
23 encoutered, in which config files it can be stored.
24 """
25
26- def __init__(self, name, default=None, default_from_env=None,
27- help=None, from_unicode=None, invalid=None,
28- unquote=True):
29+ def __init__(self, name, override_from_env=None,
30+ default=None, default_from_env=None,
31+ help=None, from_unicode=None, invalid=None, unquote=True):
32 """Build an option definition.
33
34 :param name: the name used to refer to the option.
35
36+ :param override_from_env: A list of environment variables which can
37+ provide override any configuration setting.
38+
39 :param default: the default value to use when none exist in the config
40 stores. This is either a string that ``from_unicode`` will convert
41 into the proper type, a callable returning a unicode string so that
42@@ -2379,9 +2371,12 @@
43 safely unquote them (see http://pad.lv/906897). It is provided so
44 daughter classes can handle the quoting themselves.
45 """
46+ if override_from_env is None:
47+ override_from_env = []
48 if default_from_env is None:
49 default_from_env = []
50 self.name = name
51+ self.override_from_env = override_from_env
52 # Convert the default value to a unicode string so all values are
53 # strings internally before conversion (via from_unicode) is attempted.
54 if default is None:
55@@ -2429,6 +2424,17 @@
56 raise errors.ConfigOptionValueError(self.name, unicode_value)
57 return converted
58
59+ def get_override(self):
60+ value = None
61+ for var in self.override_from_env:
62+ try:
63+ # If the env variable is defined, its value takes precedence
64+ value = os.environ[var].decode(osutils.get_user_encoding())
65+ break
66+ except KeyError:
67+ continue
68+ return value
69+
70 def get_default(self):
71 value = None
72 for var in self.default_from_env:
73@@ -2703,8 +2709,7 @@
74 Option('editor',
75 help='The command called to launch an editor to enter a message.'))
76 option_registry.register(
77- Option('email', default=default_email,
78- from_unicode=email_from_store,
79+ Option('email', override_from_env=['BZR_EMAIL'], default=default_email,
80 help='The users identity'))
81 option_registry.register(
82 Option('gpg_signing_command',
83@@ -3468,19 +3473,7 @@
84 if expand is None:
85 expand = _get_expand_default_value()
86 value = None
87- # Ensuring lazy loading is achieved by delaying section matching (which
88- # implies querying the persistent storage) until it can't be avoided
89- # anymore by using callables to describe (possibly empty) section
90- # lists.
91 found_store = None # Where the option value has been found
92- for sections in self.sections_def:
93- for store, section in sections():
94- value = section.get(name)
95- if value is not None:
96- found_store = store
97- break
98- if value is not None:
99- break
100 # If the option is registered, it may provide additional info about
101 # value handling
102 try:
103@@ -3488,9 +3481,10 @@
104 except KeyError:
105 # Not registered
106 opt = None
107+
108 def expand_and_convert(val):
109- # This may need to be called twice if the value is None or ends up
110- # being None during expansion or conversion.
111+ # This may need to be called in different contexts if the value is
112+ # None or ends up being None during expansion or conversion.
113 if val is not None:
114 if expand:
115 if isinstance(val, basestring):
116@@ -3504,11 +3498,30 @@
117 else:
118 val = opt.convert_from_unicode(found_store, val)
119 return val
120- value = expand_and_convert(value)
121- if opt is not None and value is None:
122- # If the option is registered, it may provide a default value
123- value = opt.get_default()
124- value = expand_and_convert(value)
125+
126+ # First of all, check if the environment can override the configuration
127+ # value
128+ if opt is not None and opt.override_from_env:
129+ value = opt.get_override()
130+ value = expand_and_convert(value)
131+ if value is None:
132+ # Ensuring lazy loading is achieved by delaying section matching
133+ # (which implies querying the persistent storage) until it can't be
134+ # avoided anymore by using callables to describe (possibly empty)
135+ # section lists.
136+ for sections in self.sections_def:
137+ for store, section in sections():
138+ value = section.get(name)
139+ if value is not None:
140+ found_store = store
141+ break
142+ if value is not None:
143+ break
144+ value = expand_and_convert(value)
145+ if opt is not None and value is None:
146+ # If the option is registered, it may provide a default value
147+ value = opt.get_default()
148+ value = expand_and_convert(value)
149 for hook in ConfigHooks['get']:
150 hook(self, name, value)
151 return value
152
153=== modified file 'bzrlib/tests/test_config.py'
154--- bzrlib/tests/test_config.py 2011-12-21 20:32:50 +0000
155+++ bzrlib/tests/test_config.py 2012-01-03 09:54:28 +0000
156@@ -3377,6 +3377,35 @@
157 self.assertRaises(TypeError, conf_stack.get, 'foo')
158
159
160+class TestStackWithSimpleStore(tests.TestCase):
161+
162+ def setUp(self):
163+ super(TestStackWithSimpleStore, self).setUp()
164+ self.overrideAttr(config, 'option_registry', config.OptionRegistry())
165+ self.registry = config.option_registry
166+
167+ def get_conf(self, content=None):
168+ return config.MemoryStack(content)
169+
170+ def test_override_value_from_env(self):
171+ self.registry.register(
172+ config.Option('foo', default='bar', override_from_env=['FOO']))
173+ self.overrideEnv('FOO', 'quux')
174+ # Env variable provides a default taking over the option one
175+ conf = self.get_conf('foo=store')
176+ self.assertEquals('quux', conf.get('foo'))
177+
178+ def test_first_override_value_from_env_wins(self):
179+ self.registry.register(
180+ config.Option('foo', default='bar',
181+ override_from_env=['NO_VALUE', 'FOO', 'BAZ']))
182+ self.overrideEnv('FOO', 'foo')
183+ self.overrideEnv('BAZ', 'baz')
184+ # The first env var set wins
185+ conf = self.get_conf('foo=store')
186+ self.assertEquals('foo', conf.get('foo'))
187+
188+
189 class TestMemoryStack(tests.TestCase):
190
191 def test_get(self):
192@@ -4589,21 +4618,22 @@
193 class EmailOptionTests(tests.TestCase):
194
195 def test_default_email_uses_BZR_EMAIL(self):
196+ conf = config.MemoryStack('email=jelmer@debian.org')
197 # BZR_EMAIL takes precedence over EMAIL
198 self.overrideEnv('BZR_EMAIL', 'jelmer@samba.org')
199 self.overrideEnv('EMAIL', 'jelmer@apache.org')
200- self.assertEquals('jelmer@samba.org', config.default_email())
201+ self.assertEquals('jelmer@samba.org', conf.get('email'))
202
203 def test_default_email_uses_EMAIL(self):
204+ conf = config.MemoryStack('')
205 self.overrideEnv('BZR_EMAIL', None)
206 self.overrideEnv('EMAIL', 'jelmer@apache.org')
207- self.assertEquals('jelmer@apache.org', config.default_email())
208+ self.assertEquals('jelmer@apache.org', conf.get('email'))
209
210 def test_BZR_EMAIL_overrides(self):
211+ conf = config.MemoryStack('email=jelmer@debian.org')
212 self.overrideEnv('BZR_EMAIL', 'jelmer@apache.org')
213- self.assertEquals('jelmer@apache.org',
214- config.email_from_store('jelmer@debian.org'))
215+ self.assertEquals('jelmer@apache.org', conf.get('email'))
216 self.overrideEnv('BZR_EMAIL', None)
217 self.overrideEnv('EMAIL', 'jelmer@samba.org')
218- self.assertEquals('jelmer@debian.org',
219- config.email_from_store('jelmer@debian.org'))
220+ self.assertEquals('jelmer@debian.org', conf.get('email'))
221
222=== modified file 'doc/developers/configuration.txt'
223--- doc/developers/configuration.txt 2011-12-21 20:32:50 +0000
224+++ doc/developers/configuration.txt 2012-01-03 09:54:28 +0000
225@@ -186,6 +186,10 @@
226 suitable value for the option. If the string cannot be coerced it should
227 return None.
228
229+* override_from_env: a list of environment variables. The first variable set
230+ will be used as the option value overriding any other definition of the
231+ option.
232+
233 * default: the default value that Stack.get() should return if no value can
234 be found for the option. This can also be a callable as long as it returns
235 a unicode string.
236
237=== modified file 'doc/en/release-notes/bzr-2.5.txt'
238--- doc/en/release-notes/bzr-2.5.txt 2011-12-22 18:52:58 +0000
239+++ doc/en/release-notes/bzr-2.5.txt 2012-01-03 09:54:28 +0000
240@@ -28,6 +28,10 @@
241 parent directories.
242 (Jared Hance, Jelmer Vernooij, #253529)
243
244+* ``config.Option`` can now declare ``override_from_env``, a list of
245+ environment variables which, when set, that takes precedence over values
246+ defined in configuration files. (Vincent Ladeuil, #907279)
247+
248 Improvements
249 ************
250