Take overrides from environment variables

Ref #1201
This commit is contained in:
ZyX 2015-01-06 22:05:51 +03:00
parent 1451b4261f
commit 917dfed842
9 changed files with 196 additions and 80 deletions

View File

@ -67,11 +67,82 @@ Powerline script has a number of options controlling powerline behavior. Here
performed by powerline script itself, but ``-p ~/.powerline`` will likely be performed by powerline script itself, but ``-p ~/.powerline`` will likely be
expanded by the shell to something like ``-p /home/user/.powerline``. expanded by the shell to something like ``-p /home/user/.powerline``.
Environment variables overrides
===============================
All bindings that use ``POWERLINE_COMMAND`` environment variable support taking
overrides from environment variables. In this case overrides should look like
the following::
OVERRIDE='key1.key2.key3=value;key4.key5={"value":1};key6=true;key1.key7=10'
. This will be parsed into
.. code-block:: Python
{
"key1": {
"key2": {
"key3": "value"
},
"key7": 10,
},
"key4": {
"key5": {
"value": 1,
},
},
"key6": True,
}
. Rules:
#. Environment variable must form a semicolon-separated list of key-value pairs:
``key=value;key2=value2``.
#. Keys are always dot-separated strings that must not contain equals sign (as
well as semicolon) or start with an underscore. They are interpreted
literally and create a nested set of dictionaries: ``k1.k2.k3`` creates
``{"k1":{"k2":{}}}`` and inside the innermost dictionary last key (``k3`` in
the example) is contained with its value.
#. Value may be empty in which case they are interpreted as an order to remove
some value: ``k1.k2=`` will form ``{"k1":{"k2":REMOVE_THIS_KEY}}`` nested
dictionary where ``k2`` value is a special value that tells
dictionary-merging function to remove ``k2`` rather then replace it with
something.
#. Value may be a JSON strings like ``{"a":1}`` (JSON dictionary), ``["a",1]``
(JSON list), ``1`` or ``-1`` (JSON number), ``"abc"`` (JSON string) or
``true``, ``false`` and ``null`` (JSON boolean objects and ``Null`` object
from JSON). General rule is that anything starting with a digit (U+0030 till
U+0039, inclusive), a hyphenminus (U+002D), a quotation mark (U+0022), a left
curly bracket (U+007B) or a left square bracket (U+005B) is considered to be
some JSON object, same for *exact* values ``true``, ``false`` and ``null``.
#. Any other value is considered to be literal string: ``k1=foo:bar`` parses to
``{"k1": "foo:bar"}``.
The following environment variables may be used for overrides according to the
above rules:
``POWERLINE_CONFIG_OVERRIDES``
Overrides values from :file:`powerline/config.json`.
``POWERLINE_THEME_OVERRIDES``
Overrides values from :file:`powerline/themes/{ext}/{key}.json`. Top-level
key is treated as a name of the theme for which overrides are used: e.g. to
disable cwd segment defined in :file:`powerline/themes/shell/default.json`
one needs to use::
POWERLINE_THEME_OVERRIDES=default.segment_data.cwd.display=false
Additionally one environment variable is a usual *colon*-separated list of
directories: ``POWERLINE_CONFIG_PATHS``. This one defines paths which will be
searched for configuration.
Zsh/zpython overrides Zsh/zpython overrides
===================== =====================
Here overrides are controlled by similarly to the powerline script, but values Here overrides are controlled by similarly to the powerline script, but values
are taken from zsh variables. are taken from zsh variables. :ref:`Environment variable overrides` are also
supported: if variable is a string this variant is used.
``POWERLINE_CONFIG_OVERRIDES`` ``POWERLINE_CONFIG_OVERRIDES``
Overrides options from :file:`powerline/config.json`. Should be a zsh Overrides options from :file:`powerline/config.json`. Should be a zsh

View File

@ -8,7 +8,7 @@ from weakref import WeakValueDictionary, ref
import zsh import zsh
from powerline.shell import ShellPowerline from powerline.shell import ShellPowerline
from powerline.lib import parsedotval from powerline.lib.overrides import parsedotval
from powerline.lib.unicode import unicode, u from powerline.lib.unicode import unicode, u
from powerline.lib.encoding import (get_preferred_output_encoding, from powerline.lib.encoding import (get_preferred_output_encoding,
get_preferred_environment_encoding) get_preferred_environment_encoding)

View File

@ -5,7 +5,9 @@ from __future__ import (division, absolute_import, print_function)
import argparse import argparse
import sys import sys
from powerline.lib import parsedotval from itertools import chain
from powerline.lib.overrides import parsedotval, parse_override_var
from powerline.lib.dict import mergeargs from powerline.lib.dict import mergeargs
from powerline.lib.encoding import get_preferred_arguments_encoding from powerline.lib.encoding import get_preferred_arguments_encoding
@ -14,21 +16,23 @@ if sys.version_info < (3,):
encoding = get_preferred_arguments_encoding() encoding = get_preferred_arguments_encoding()
def arg_to_unicode(s): def arg_to_unicode(s):
return unicode(s, encoding, 'replace') if not isinstance(s, unicode) else s return unicode(s, encoding, 'replace') if not isinstance(s, unicode) else s # NOQA
else: else:
def arg_to_unicode(s): def arg_to_unicode(s):
return s return s
def finish_args(args): def finish_args(environ, args):
if args.config_override: args.config_override = mergeargs(chain(
args.config_override = mergeargs((parsedotval(v) for v in args.config_override)) (parsedotval(v) for v in args.config_override or ()),
if args.theme_override: parse_override_var(environ.get('POWERLINE_CONFIG_OVERRIDES', '')),
args.theme_override = mergeargs((parsedotval(v) for v in args.theme_override)) ))
else: args.theme_override = mergeargs(chain(
args.theme_override = {} (parsedotval(v) for v in args.theme_override or ()),
parse_override_var(environ.get('POWERLINE_THEME_OVERRIDES', '')),
))
if args.renderer_arg: if args.renderer_arg:
args.renderer_arg = mergeargs((parsedotval(v) for v in args.renderer_arg)) args.renderer_arg = mergeargs((parsedotval(v) for v in args.renderer_arg), remove=True)
def get_argparser(ArgumentParser=argparse.ArgumentParser): def get_argparser(ArgumentParser=argparse.ArgumentParser):

View File

@ -1,12 +1,8 @@
# vim:fileencoding=utf-8:noet # vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function) from __future__ import (unicode_literals, division, absolute_import, print_function)
import json
from functools import wraps from functools import wraps
from powerline.lib.dict import REMOVE_THIS_KEY
def wraps_saveargs(wrapped): def wraps_saveargs(wrapped):
def dec(wrapper): def dec(wrapper):
@ -30,59 +26,3 @@ def add_divider_highlight_group(highlight_group):
return None return None
return f return f
return dec return dec
def parse_value(s):
'''Convert string to Python object
Rules:
* Empty string means that corresponding key should be removed from the
dictionary.
* Strings that start with a minus, digit or with some character that starts
JSON collection or string object are parsed as JSON.
* JSON special values ``null``, ``true``, ``false`` (case matters) are
parsed as JSON.
* All other values are considered to be raw strings.
:param str s: Parsed string.
:return: Python object.
'''
if not s:
return REMOVE_THIS_KEY
elif s[0] in '"{[0193456789-' or s in ('null', 'true', 'false'):
return json.loads(s)
else:
return s
def keyvaluesplit(s):
if '=' not in s:
raise TypeError('Option must look like option=json_value')
if s[0] == '_':
raise ValueError('Option names must not start with `_\'')
idx = s.index('=')
o = s[:idx]
val = parse_value(s[idx + 1:])
return (o, val)
def parsedotval(s):
if type(s) is tuple:
o, val = s
val = parse_value(val)
else:
o, val = keyvaluesplit(s)
keys = o.split('.')
if len(keys) > 1:
r = (keys[0], {})
rcur = r[1]
for key in keys[1:-1]:
rcur[key] = {}
rcur = rcur[key]
rcur[keys[-1]] = val
return r
else:
return (o, val)

View File

@ -5,26 +5,44 @@ from __future__ import (unicode_literals, division, absolute_import, print_funct
REMOVE_THIS_KEY = object() REMOVE_THIS_KEY = object()
def mergeargs(argvalue): def mergeargs(argvalue, remove=False):
if not argvalue: if not argvalue:
return None return None
r = {} r = {}
for subval in argvalue: for subval in argvalue:
mergedicts(r, dict([subval])) mergedicts(r, dict([subval]), remove=remove)
return r return r
def mergedicts(d1, d2): def _clear_special_values(d):
'''Remove REMOVE_THIS_KEY values from dictionary
'''
l = [d]
while l:
i = l.pop()
pops = []
for k, v in i.items():
if v is REMOVE_THIS_KEY:
pops.append(k)
elif isinstance(v, dict):
l.append(v)
for k in pops:
i.pop(k)
def mergedicts(d1, d2, remove=True):
'''Recursively merge two dictionaries '''Recursively merge two dictionaries
First dictionary is modified in-place. First dictionary is modified in-place.
''' '''
for k in d2: for k in d2:
if k in d1 and isinstance(d1[k], dict) and isinstance(d2[k], dict): if k in d1 and isinstance(d1[k], dict) and isinstance(d2[k], dict):
mergedicts(d1[k], d2[k]) mergedicts(d1[k], d2[k], remove)
elif d2[k] is REMOVE_THIS_KEY: elif remove and d2[k] is REMOVE_THIS_KEY:
d1.pop(k, None) d1.pop(k, None)
else: else:
if remove and isinstance(d2[k], dict):
_clear_special_values(d2[k])
d1[k] = d2[k] d1[k] = d2[k]

View File

@ -0,0 +1,80 @@
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)
import json
from powerline.lib.dict import REMOVE_THIS_KEY
def parse_value(s):
'''Convert string to Python object
Rules:
* Empty string means that corresponding key should be removed from the
dictionary.
* Strings that start with a minus, digit or with some character that starts
JSON collection or string object are parsed as JSON.
* JSON special values ``null``, ``true``, ``false`` (case matters) are
parsed as JSON.
* All other values are considered to be raw strings.
:param str s: Parsed string.
:return: Python object.
'''
if not s:
return REMOVE_THIS_KEY
elif s[0] in '"{[0193456789-' or s in ('null', 'true', 'false'):
return json.loads(s)
else:
return s
def keyvaluesplit(s):
'''Split K1.K2=VAL into K1.K2 and parsed VAL
'''
if '=' not in s:
raise TypeError('Option must look like option=json_value')
if s[0] == '_':
raise ValueError('Option names must not start with `_\'')
idx = s.index('=')
o = s[:idx]
val = parse_value(s[idx + 1:])
return (o, val)
def parsedotval(s):
'''Parse K1.K2=VAL into {"K1":{"K2":VAL}}
``VAL`` is processed according to rules defined in :py:func:`parse_value`.
'''
if type(s) is tuple:
o, val = s
val = parse_value(val)
else:
o, val = keyvaluesplit(s)
keys = o.split('.')
if len(keys) > 1:
r = (keys[0], {})
rcur = r[1]
for key in keys[1:-1]:
rcur[key] = {}
rcur = rcur[key]
rcur[keys[-1]] = val
return r
else:
return (o, val)
def parse_override_var(s):
'''Parse a semicolon-separated list of strings into a sequence of values
Emits the same items in sequence as :py:func:`parsedotval` does.
'''
return (
parsedotval(item)
for item in s.split(';')
if item
)

View File

@ -78,8 +78,11 @@ def render(args, environ, cwd):
tuple(args.config_override) if args.config_override else None, tuple(args.config_override) if args.config_override else None,
tuple(args.theme_override) if args.theme_override else None, tuple(args.theme_override) if args.theme_override else None,
tuple(args.config_path) if args.config_path else None, tuple(args.config_path) if args.config_path else None,
environ.get('POWERLINE_THEME_OVERRIDES', ''),
environ.get('POWERLINE_CONFIG_OVERRIDES', ''),
environ.get('POWERLINE_CONFIG_PATHS', ''),
) )
finish_args(args) finish_args(environ, args)
powerline = None powerline = None
try: try:
powerline = powerlines[key] powerline = powerlines[key]

View File

@ -24,7 +24,7 @@ else:
if __name__ == '__main__': if __name__ == '__main__':
args = get_argparser().parse_args() args = get_argparser().parse_args()
finish_args(args) finish_args(os.environ, args)
powerline = ShellPowerline(args, run_once=True) powerline = ShellPowerline(args, run_once=True)
segment_info = {'args': args, 'environ': os.environ} segment_info = {'args': args, 'environ': os.environ}
write_output(args, powerline, segment_info, write, get_preferred_output_encoding()) write_output(args, powerline, segment_info, write, get_preferred_output_encoding())

View File

@ -114,7 +114,7 @@ class TestParser(TestCase):
(['shell', '-c', 'common={ }'], {'ext': ['shell'], 'config_override': {'common': {}}}), (['shell', '-c', 'common={ }'], {'ext': ['shell'], 'config_override': {'common': {}}}),
]: ]:
args = parser.parse_args(argv) args = parser.parse_args(argv)
finish_args(args) finish_args({}, args)
for key, val in expargs.items(): for key, val in expargs.items():
self.assertEqual(getattr(args, key), val) self.assertEqual(getattr(args, key), val)
for key, val in args.__dict__.items(): for key, val in args.__dict__.items():