commit 45f82fab334265d8ca6162192080a3e03853f323
Author: Damian Johnson <atagar(a)torproject.org>
Date: Tue Jan 1 15:15:31 2013 -0800
Dropping the Config's synchronize() method
Choice is highly overrated. Actually, in the world of libraries providing too
many options of doing a single can make things damn confusing.
The Config's synchronize() method was a great improvement in its day, but now
that we have config_dict() it's redundant. Hopefully our module's docs are now
more friendly for newcomers.
---
stem/util/conf.py | 220 +++++++++++++++++++++++-------------------------
test/unit/util/conf.py | 55 ------------
2 files changed, 107 insertions(+), 168 deletions(-)
diff --git a/stem/util/conf.py b/stem/util/conf.py
index e8d35b6..e15313c 100644
--- a/stem/util/conf.py
+++ b/stem/util/conf.py
@@ -35,88 +35,108 @@ For instance...
"msg.greeting": "Multi-line message exclaiming of the\\nwonder and awe that is pepperjack!",
}
-Configurations are loaded or saved via the :class:`~stem.util.conf.Config`
-class. The :class:`~stem.util.conf.Config` can be be used directly with its
+Configurations are managed via the :class:`~stem.util.conf.Config` class. The
+:class:`~stem.util.conf.Config` can be be used directly with its
:func:`~stem.util.conf.Config.get` and :func:`~stem.util.conf.Config.set`
methods, but usually modules will want a local dictionary with just the
-configurations that it cares about. This can be done a couple ways...
+configurations that it cares about.
-* **Independent Dictionary**
+To do this use the :func:`~stem.util.conf.config_dict` function. For example...
-To simply get a dictionary that has configurations use the
-:func:`~stem.util.conf.Config.synchronize` method. This takes as an argument
-a dictionary with the default values.
+::
-For instance, lets say that you had a file at '/home/atagar/user_config'
-with...
+ import getpass
+ from stem.util import conf, connection
+
+ def config_validator(key, value):
+ if key == "timeout":
+ # require at least a one second timeout
+ return max(1, value)
+ elif key == "endpoint":
+ if not connection.is_valid_ip_address(value):
+ raise ValueError("'%s' isn't a valid IPv4 address" % value)
+ elif key == "port":
+ if not connection.is_valid_port(value):
+ raise ValueError("'%s' isn't a valid port" % value)
+ elif key == "retries":
+ # negative retries really don't make sense
+ return max(0, value)
+
+ CONFIG = conf.config_dict("ssh_login", {
+ "username": getpass.getuser(),
+ "password": "",
+ "timeout": 10,
+ "endpoint": "263.12.8.0",
+ "port": 22,
+ "reconnect": False,
+ "retries": 3,
+ }, config_validator)
+
+There's several things going on here so lets take it step by step...
+
+* The :func:`~stem.util.conf.config_dict` provides a dictionary that's bound
+ to a given configuration. If the "ssh_proxy_config" configuration changes
+ then so will the contents of CONFIG.
+
+* The dictionary we're passing to :func:`~stem.util.conf.config_dict` provides
+ two important pieces of information: default values and their types. See the
+ Config's :func:`~stem.util.conf.Config.get` method for how these type
+ inferences work.
+
+* The config_validator is a hook we're adding to make sure CONFIG only gets
+ values we think are valid. In this case it ensures that our timeout value
+ is at least one second, and rejects endpoints or ports that are invalid.
+
+Now lets say our user has the following configuration file...
::
username waddle_doo
- permissions.ssh true
- some.extra.stuff foobar
+ password jabberwocky
+ timeout -15
+ port 9000000
+ retries lots
+ reconnect true
+ logging debug
-... then run...
+... and we load it as follows...
::
- >>> from stem.util.conf import get_config
- >>> my_module_config = {
- ... "username": "default",
- ... "permissions.ssh": False,
- ... "permissions.sudo": False,
- ... }
-
- >>> config = get_config("user_config")
- >>> config.load("/home/atagar/user_config")
- >>> config.synchronize(my_module_config)
-
- >>> print my_module_config
- {'username': 'waddle_doo', 'permissions.sudo': False, 'permissions.ssh': True}
+ >>> from from stem.util import conf
+ >>> our_config = conf.get_config("ssh_login")
+ >>> our_config.load("/home/atagar/user_config")
+ >>> print CONFIG
+ {
+ "username": "waddle_doo",
+ "password": "jabberwocky",
+ "timeout": 1,
+ "endpoint": "263.12.8.0",
+ "port": 22,
+ "reconnect": True,
+ "retries": 3,
+ }
-The configuration file just contains string mappings, but the
-:class:`~stem.util.conf.Config` attempts to convert those values to be the same
-types as the dictionary's defaults. For more information on how type inferences
-work see the :func:`~stem.util.conf.Config.get` method.
+Here's an expanation of what happened...
-* **Linked Dictionary**
+* the username, password, and reconnect attributes took the values in the
+ configuration file
-Alternatively you can get a read-only dictionary that stays in sync with the
-:class:`~stem.util.conf.Config` by using the
-:func:`~stem.util.conf.config_dict` function...
+* the 'config_validator' we added earlier allows for a minimum timeout of one
+ and rejected the invalid port (with a log message)
-::
+* we weren't able to convert the retries' "lots" value to an integer so it kept
+ its default value and logged a warning
- >>> # Make a dictionary that watches the 'user_config' configuration for changes.
-
- >>> from stem.util.conf import config_dict
- >>> my_module_config = config_dict("user_config", {
- ... "username": "default",
- ... "permissions.ssh": False,
- ... "permissions.sudo": False,
- ... })
- >>> print my_module_config
- {'username': 'default', 'permissions.sudo': False, 'permissions.ssh': False}
-
- >>> # Something (maybe another module) loads the config, causing the
- >>> # my_module_config above to be updated.
-
- >>> from stem.util.conf import get_config
- >>> config = get_config("user_config")
- >>> config.load("/home/atagar/user_config")
-
- >>> print my_module_config
- {'username': 'waddle_doo', 'permissions.sudo': False, 'permissions.ssh': True}
-
- >>> config.set('username', 'ness')
- >>> print my_module_config
- {'username': 'ness', 'permissions.sudo': False, 'permissions.ssh': True}
+* the user didn't supply an endpoint so that remained unchanged
+
+* our CONFIG didn't have a 'logging' attribute so it was ignored
**Module Overview:**
::
- config_dict - provides a dictionary that's kept synchronized with a config
+ config_dict - provides a dictionary that's kept in sync with our config
get_config - singleton for getting configurations
parse_enum_csv - helper funcion for parsing confguration entries for enums
@@ -124,7 +144,6 @@ Alternatively you can get a read-only dictionary that stays in sync with the
|- load - reads a configuration file
|- save - writes the current configuration to a file
|- clear - empties our loaded configuration contents
- |- synchronize - replaces mappings in a dictionary with the config's values
|- add_listener - notifies the given listener when an update occurs
|- clear_listeners - removes any attached listeners
|- keys - provides keys in the loaded configuration
@@ -160,10 +179,14 @@ class _SyncListener(object):
def config_dict(handle, conf_mappings, handler = None):
"""
- Makes a dictionary that stays synchronized with a configuration. The process
- for staying in sync is similar to the :class:`~stem.util.conf.Config` class'
- :func:`~stem.util.conf.Config.synchronize` method, only changing the
- dictionary's values if we're able to cast to the same type.
+ Makes a dictionary that stays synchronized with a configuration.
+
+ This takes a dictionary of 'config_key => default_value' mappings and
+ changes the values to reflect our current configuration. This will leave
+ the previous values alone if...
+
+ * we don't have a value for that config_key
+ * we can't convert our value to be the same type as the default_value
If a handler is provided then this is called just prior to assigning new
values to the config_dict. The handler function is expected to accept the
@@ -171,6 +194,13 @@ def config_dict(handle, conf_mappings, handler = None):
into the dictionary. If this returns None then the value is updated as
normal.
+ For more information about how we convert types see our
+ :func:`~stem.util.conf.Config.get` method.
+
+ **The dictionary you get from this is manged by the
+ :class:`~stem.util.conf.Config` class and should be treated as being
+ read-only.**
+
:param str handle: unique identifier for a config instance
:param dict conf_mappings: config key/value mappings used as our defaults
:param functor handler: function referred to prior to assigning values
@@ -286,24 +316,26 @@ class Config(object):
::
- import stem.util.conf
+ from stem.util import conf
# Configuration values we'll use in this file. These are mappings of
# configuration keys to the default values we'll use if the user doesn't
# have something different in their config file (or it doesn't match this
# type).
- ssh_config = {"login.user": "atagar",
- "login.password": "pepperjack_is_awesome!",
- "destination.ip": "127.0.0.1",
- "destination.port": 22,
- "startup.run": []}
+ ssh_config = conf.config_dict("ssh_login", {
+ "login.user": "atagar",
+ "login.password": "pepperjack_is_awesome!",
+ "destination.ip": "127.0.0.1",
+ "destination.port": 22,
+ "startup.run": [],
+ })
# Makes an empty config instance with the handle of 'ssh_login'. This is
# a singleton so other classes can fetch this same configuration from
# this handle.
- user_config = stem.util.conf.get_config("ssh_login")
+ user_config = conf.get_config("ssh_login")
# Loads the user's configuration file, warning if this fails.
@@ -312,7 +344,7 @@ class Config(object):
except IOError, exc:
print "Unable to load the user's config: %s" % exc
- # Replaces the contents of ssh_config with the values from the user's
+ # This replace the contents of ssh_config with the values from the user's
# config file if...
#
# * the key is present in the config file
@@ -338,8 +370,6 @@ class Config(object):
#
# Information for what values fail to load and why are reported to
# 'stem.util.log'.
-
- user_config.synchronize(ssh_config)
"""
def __init__(self):
@@ -448,42 +478,6 @@ class Config(object):
self._raw_contents = []
self._requested_keys = set()
- def synchronize(self, conf_mappings, limits = None):
- """
- This takes a dictionary of 'config_key => default_value' mappings and
- changes the values to reflect our current configuration. This will leave
- the previous values alone if...
-
- * we don't have a value for that config_key
- * we can't convert our value to be the same type as the default_value
-
- For more information about how we convert types see our
- :func:`~stem.util.conf.Config.get` method.
-
- :param dict conf_mappings: configuration key/value mappings to be revised
- :param dict limits: mappings of limits on numeric values, expected to be of
- the form "configKey -> min" or "configKey -> (min, max)"
- """
-
- if limits is None: limits = {}
-
- for entry in conf_mappings.keys():
- val = self.get(entry, conf_mappings[entry])
-
- # if this was a numeric value then apply constraints
- if entry in limits and (isinstance(val, int) or isinstance(val, float)):
- if isinstance(limits[entry], tuple):
- val = max(val, limits[entry][0])
- val = min(val, limits[entry][1])
- else: val = max(val, limits[entry])
-
- # only use this value if it wouldn't change the type of the mapping (this
- # will only fail if the type isn't either a string or one of the types
- # recognized by the get method)
-
- if type(val) == type(conf_mappings[entry]):
- conf_mappings[entry] = val
-
def add_listener(self, listener, backfill = True):
"""
Registers the function to be notified of configuration updates. Listeners
@@ -519,9 +513,9 @@ class Config(object):
def unused_keys(self):
"""
Provides the configuration keys that have never been provided to a caller
- via the :func:`~stem.util.conf.Config.get`,
- :func:`~stem.util.conf.Config.get_value`, or
- :func:`~stem.util.conf.Config.synchronize` methods.
+ via :func:`~stem.util.conf.config_dict` or the
+ :func:`~stem.util.conf.Config.get` and
+ :func:`~stem.util.conf.Config.get_value` methods.
:returns: **set** of configuration keys we've loaded but have never been requested
"""
diff --git a/test/unit/util/conf.py b/test/unit/util/conf.py
index f3ce915..8b85216 100644
--- a/test/unit/util/conf.py
+++ b/test/unit/util/conf.py
@@ -129,61 +129,6 @@ class TestConf(unittest.TestCase):
test_config.clear()
self.assertEquals([], test_config.keys())
- def test_synchronize(self):
- """
- Tests the synchronize method.
- """
-
- my_config = {
- "bool_value": False,
- "int_value": 5,
- "str_value": "hello",
- "list_value": [],
- "map_value": {},
- }
-
- test_config = stem.util.conf.get_config("unit_testing")
- test_config.set("bool_value", "true")
- test_config.set("int_value", "11")
- test_config.set("str_value", "world")
- test_config.set("list_value", "a", False)
- test_config.set("list_value", "b", False)
- test_config.set("list_value", "c", False)
- test_config.set("map_value", "foo => bar")
-
- test_config.synchronize(my_config)
- self.assertEquals(True, my_config["bool_value"])
- self.assertEquals(11, my_config["int_value"])
- self.assertEquals("world", my_config["str_value"])
- self.assertEquals(["a", "b", "c"], my_config["list_value"])
- self.assertEquals({"foo": "bar"}, my_config["map_value"])
-
- def test_synchronize_type_mismatch(self):
- """
- Tests the synchronize method when the config file has missing entries or
- the wrong types.
- """
-
- my_config = {
- "bool_value": False,
- "int_value": 5,
- "str_value": "hello",
- "list_value": [],
- "map_value": {},
- }
-
- test_config = stem.util.conf.get_config("unit_testing")
- test_config.set("bool_value", "hello world")
- test_config.set("int_value", "11a")
- test_config.set("map_value", "foo bar")
-
- test_config.synchronize(my_config)
- self.assertEquals(False, my_config["bool_value"])
- self.assertEquals(5, my_config["int_value"])
- self.assertEquals("hello", my_config["str_value"])
- self.assertEquals([], my_config["list_value"])
- self.assertEquals({}, my_config["map_value"])
-
def test_listeners(self):
"""
Tests the add_listener and clear_listeners methods.