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
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
=====================
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``
Overrides options from :file:`powerline/config.json`. Should be a zsh

View File

@ -8,7 +8,7 @@ from weakref import WeakValueDictionary, ref
import zsh
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.encoding import (get_preferred_output_encoding,
get_preferred_environment_encoding)

View File

@ -5,7 +5,9 @@ from __future__ import (division, absolute_import, print_function)
import argparse
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.encoding import get_preferred_arguments_encoding
@ -14,21 +16,23 @@ if sys.version_info < (3,):
encoding = get_preferred_arguments_encoding()
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:
def arg_to_unicode(s):
return s
def finish_args(args):
if args.config_override:
args.config_override = mergeargs((parsedotval(v) for v in args.config_override))
if args.theme_override:
args.theme_override = mergeargs((parsedotval(v) for v in args.theme_override))
else:
args.theme_override = {}
def finish_args(environ, args):
args.config_override = mergeargs(chain(
(parsedotval(v) for v in args.config_override or ()),
parse_override_var(environ.get('POWERLINE_CONFIG_OVERRIDES', '')),
))
args.theme_override = mergeargs(chain(
(parsedotval(v) for v in args.theme_override or ()),
parse_override_var(environ.get('POWERLINE_THEME_OVERRIDES', '')),
))
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):

View File

@ -1,12 +1,8 @@
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)
import json
from functools import wraps
from powerline.lib.dict import REMOVE_THIS_KEY
def wraps_saveargs(wrapped):
def dec(wrapper):
@ -30,59 +26,3 @@ def add_divider_highlight_group(highlight_group):
return None
return f
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()
def mergeargs(argvalue):
def mergeargs(argvalue, remove=False):
if not argvalue:
return None
r = {}
for subval in argvalue:
mergedicts(r, dict([subval]))
mergedicts(r, dict([subval]), remove=remove)
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
First dictionary is modified in-place.
'''
for k in d2:
if k in d1 and isinstance(d1[k], dict) and isinstance(d2[k], dict):
mergedicts(d1[k], d2[k])
elif d2[k] is REMOVE_THIS_KEY:
mergedicts(d1[k], d2[k], remove)
elif remove and d2[k] is REMOVE_THIS_KEY:
d1.pop(k, None)
else:
if remove and isinstance(d2[k], dict):
_clear_special_values(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.theme_override) if args.theme_override 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
try:
powerline = powerlines[key]

View File

@ -24,7 +24,7 @@ else:
if __name__ == '__main__':
args = get_argparser().parse_args()
finish_args(args)
finish_args(os.environ, args)
powerline = ShellPowerline(args, run_once=True)
segment_info = {'args': args, 'environ': os.environ}
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': {}}}),
]:
args = parser.parse_args(argv)
finish_args(args)
finish_args({}, args)
for key, val in expargs.items():
self.assertEqual(getattr(args, key), val)
for key, val in args.__dict__.items():