From dff225b60ea3fd644ca0900995fe2d236147d995 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Mon, 23 Jan 2017 23:52:39 -0800 Subject: [PATCH] add limited interpolation to eden config parsing Summary: This allows us to templatize centrally managed config files Reviewed By: bolinfest Differential Revision: D4444058 fbshipit-source-id: a44372084d32dcf0b27922490ab40a48478a720d --- eden/fs/cli/TARGETS | 7 +++++ eden/fs/cli/config.py | 13 +++++++-- eden/fs/cli/configinterpolator.py | 45 +++++++++++++++++++++++++++++++ eden/fs/cli/test/interp_test.py | 42 +++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 eden/fs/cli/configinterpolator.py create mode 100644 eden/fs/cli/test/interp_test.py diff --git a/eden/fs/cli/TARGETS b/eden/fs/cli/TARGETS index 0a5307918a..710e131305 100644 --- a/eden/fs/cli/TARGETS +++ b/eden/fs/cli/TARGETS @@ -14,10 +14,17 @@ python_library( name = 'lib', srcs = [ 'config.py', + 'configinterpolator.py', 'util.py', ], ) +python_unittest( + name = 'test', + srcs = glob(['test/*.py']), + deps = [':lib'] +) + for build_target, suffix in get_daemon_versions(): # The :all rule is a convenience to ensure that both the CLI and the daemon # are built. diff --git a/eden/fs/cli/config.py b/eden/fs/cli/config.py index 4b0576bbe7..3b5de55408 100644 --- a/eden/fs/cli/config.py +++ b/eden/fs/cli/config.py @@ -20,7 +20,7 @@ import subprocess import tempfile import time -from . import util +from . import (util, configinterpolator) import eden.thrift import facebook.eden.ttypes as eden_ttypes from fb303.ttypes import fb_status @@ -61,9 +61,18 @@ class Config: if not self._system_config_dir: self._system_config_dir = SYSTEM_CONFIG_DIR self._user_config_path = os.path.join(home_dir, USER_CONFIG) + self._home_dir = home_dir def _loadConfig(self): - parser = configparser.ConfigParser() + ''' to facilitate templatizing a centrally deployed config, we + allow a limited set of env vars to be expanded. + ${HOME} will be replaced by the user's home dir, + ${USER} will be replaced by the user's login name. + ''' + defaults = {'USER': os.environ.get('USER'), + 'HOME': self._home_dir} + parser = configparser.ConfigParser( + interpolation=configinterpolator.EdenConfigInterpolator(defaults)) parser.read(self.get_rc_files()) return parser diff --git a/eden/fs/cli/configinterpolator.py b/eden/fs/cli/configinterpolator.py new file mode 100644 index 0000000000..0dbc64bbf3 --- /dev/null +++ b/eden/fs/cli/configinterpolator.py @@ -0,0 +1,45 @@ +# Copyright (c) 2017-present, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import configparser + + +class EdenConfigInterpolator(configparser.Interpolation): + ''' Python provides a couple of interpolation options but neither + of them quite match the simplicity that we want. This class + will interpolate the keys of the provided map and replace + those tokens with the values from the map. There is no + recursion or referencing of values from other sections of + the config. + Limiting the scope interpolation makes it easier to replicate + this approach in the C++ implementation of the parser. + ''' + + def __init__(self, defaults): + self._defaults = {} + ''' pre-construct the token name that we're going to substitute. + eg: {"foo": "bar"} is stored as {"${foo}": "bar"} internally + ''' + for k, v in defaults.items(): + self._defaults['${' + k + '}'] = v + + def _interpolate(self, value): + ''' simple brute force replacement using the defaults that were + provided to us during construction ''' + for k, v in self._defaults.items(): + value = value.replace(k, v) + return value + + def before_get(self, parser, section, option, value, defaults): + return self._interpolate(value) + + def before_read(self, parser, section, option, value): + return self._interpolate(value) diff --git a/eden/fs/cli/test/interp_test.py b/eden/fs/cli/test/interp_test.py new file mode 100644 index 0000000000..d4b9f9564d --- /dev/null +++ b/eden/fs/cli/test/interp_test.py @@ -0,0 +1,42 @@ +# Copyright (c) 2017-present, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from .. import configinterpolator +import configparser +import unittest + + +class InterpolatorTest(unittest.TestCase): + def test_basic_subs(self): + defaults = {'USER': 'wez', 'RECURSIVE': 'a${RECURSIVE}b'} + parser = configparser.ConfigParser( + interpolation=configinterpolator.EdenConfigInterpolator(defaults)) + parser.add_section('section') + parser.set('section', 'user', '${USER}') + parser.set('section', 'rec', '${RECURSIVE}') + parser.set('section', 'simple', 'value') + + self.assertEqual('wez', parser.get('section', 'user')) + self.assertEqual('value', parser.get('section', 'simple')) + self.assertEqual('a${RECURSIVE}b', parser.get('section', 'rec')) + + actual = {} + for section in parser.sections(): + actual[section] = dict(parser.items(section)) + + expect = { + 'section': { + 'user': 'wez', + 'simple': 'value', + 'rec': 'a${RECURSIVE}b', + } + } + self.assertEqual(expect, actual)