[tor-commits] [stem/master] Rearranging conf util to improve usability
atagar at torproject.org
atagar at torproject.org
Sun Nov 13 23:42:29 UTC 2011
commit f9aeefb2805e2ef9cd63f236c7a27308718575e9
Author: Damian Johnson <atagar at torproject.org>
Date: Sun Nov 13 14:01:55 2011 -0800
Rearranging conf util to improve usability
Adding the standard configuration header (... sooo helpful) and moving the rest
of the util's contents around to be more intuitive.
---
stem/util/conf.py | 398 +++++++++++++++++++++++++++--------------------------
1 files changed, 205 insertions(+), 193 deletions(-)
diff --git a/stem/util/conf.py b/stem/util/conf.py
index 7581a98..7ba2f11 100644
--- a/stem/util/conf.py
+++ b/stem/util/conf.py
@@ -3,73 +3,26 @@ This provides handlers for specially formatted configuration files. Entries are
expected to consist of simple key/value pairs, and anything after "#" is
stripped as a comment. Excess whitespace is trimmed and empty lines are
ignored. For instance:
-# This is my sample config
-user.name Galen
-user.password yabba1234 # here's an inline comment
-user.notes takes a fancy to pepperjack chese
-blankEntry.example
+ # This is my sample config
+ user.name Galen
+ user.password yabba1234 # here's an inline comment
+ user.notes takes a fancy to pepperjack chese
+ blankEntry.example
-would be loaded as four entries (the last one's value being an empty string).
-If a key's defined multiple times then the last instance of it is used.
+would be loaded as four entries, the last one's value being an empty string.
-Example usage:
- User has a file at '/home/atagar/myConfig' with...
- destination.ip 1.2.3.4
- destination.port blarg
-
- startup.run export PATH=$PATH:~/bin
- startup.run alias l=ls
-
- And they have a script with...
- import stem.util.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": []}
-
- # 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")
-
- # Loads the user's configuration file, warning if this fails.
-
- try:
- user_config.load("/home/atagar/myConfig")
- 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
- # config file if...
- # - the key is present in the config file
- # - we're able to convert the configuration file's value to the same type
- # as what's in the mapping (see the Config.get() method for how these
- # type inferences work)
- #
- # For instance in this case the login values are left alone (because they
- # aren't in the user's config file), and the 'destination.port' is also
- # left with the value of 22 because we can't turn "blarg" into an integer.
- #
- # The other values are replaced, so ssh_config now becomes...
- # {"login.user": "atagar",
- # "login.password": "pepperjack_is_awesome!",
- # "destination.ip": "1.2.3.4",
- # "destination.port": 22,
- # "startup.run": ["export PATH=$PATH:~/bin", "alias l=ls"]}
- #
- # Information for what values fail to load and why are reported to
- # 'stem.util.log'.
-
- user_config.update(ssh_config)
+get_config - Singleton for getting configurations
+Config - Custom configuration.
+ |- load - reads a configuration file
+ |- clear - empties our loaded configuration contents
+ |- update - replaces mappings in a dictionary with the config's values
+ |- keys - provides keys in the loaded configuration
+ |- unused_keys - provides keys that have never been requested
+ |- get - provides the value for a given key, with type inference
+ |- get_value - provides the value for a given key as a string
+ |- get_str_csv - gets a value as a comma separated list of strings
+ +- get_int_csv - gets a value as a comma separated list of integers
"""
import logging
@@ -107,6 +60,65 @@ class Config():
"""
Handler for easily working with custom configurations, providing persistence
to and from files. All operations are thread safe.
+
+ Example usage:
+ User has a file at '/home/atagar/myConfig' with...
+ destination.ip 1.2.3.4
+ destination.port blarg
+
+ startup.run export PATH=$PATH:~/bin
+ startup.run alias l=ls
+
+ And they have a script with...
+ import stem.util.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": []}
+
+ # 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")
+
+ # Loads the user's configuration file, warning if this fails.
+
+ try:
+ user_config.load("/home/atagar/myConfig")
+ 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
+ # config file if...
+ # - the key is present in the config file
+ # - we're able to convert the configuration file's value to the same type
+ # as what's in the mapping (see the Config.get() method for how these
+ # type inferences work)
+ #
+ # For instance in this case the login values are left alone (because they
+ # aren't in the user's config file), and the 'destination.port' is also
+ # left with the value of 22 because we can't turn "blarg" into an
+ # integer.
+ #
+ # The other values are replaced, so ssh_config now becomes...
+ # {"login.user": "atagar",
+ # "login.password": "pepperjack_is_awesome!",
+ # "destination.ip": "1.2.3.4",
+ # "destination.port": 22,
+ # "startup.run": ["export PATH=$PATH:~/bin", "alias l=ls"]}
+ #
+ # Information for what values fail to load and why are reported to
+ # 'stem.util.log'.
+
+ user_config.update(ssh_config)
"""
def __init__(self):
@@ -124,35 +136,116 @@ class Config():
# keys that have been requested (used to provide unused config contents)
self._requested_keys = set()
- def get_value(self, key, default = None, multiple = False):
+ def load(self, path):
"""
- This provides the currently value associated with a given key.
+ Reads in the contents of the given path, adding its configuration values
+ to our current contents.
Arguments:
- key (str) - config setting to be fetched
- default (object) - value provided if no such key exists
- multiple (bool) - provides back a list of all values if true, otherwise
- this returns the last loaded configuration value
+ path (str) - file path to be loaded
- Returns:
- string or list of string configuration values associated with the given
- key, providing the default if no such key exists
+ Raises:
+ IOError if we fail to read the file (it doesn't exist, insufficient
+ permissions, etc)
"""
+ config_file = open(path, "r")
+ read_contents = config_file.readlines()
+ config_file.close()
+
self._contents_lock.acquire()
+ self._raw_contents = read_contents
- if key in self._contents:
- val = self._contents[key]
- if not multiple: val = val[-1]
- self._requested_keys.add(key)
- else:
- msg = "config entry '%s' not found, defaulting to '%s'" % (key, default)
- LOGGER.debug(msg)
- val = default
+ for line in self._raw_contents:
+ # strips any commenting or excess whitespace
+ comment_start = line.find("#")
+ if comment_start != -1: line = line[:comment_start]
+ line = line.strip()
+
+ # parse the key/value pair
+ if line:
+ if " " in line:
+ key, value = line.split(" ", 1)
+ value = value.strip()
+ else:
+ key, value = line, ""
+
+ if key in self._contents: self._contents[key].append(value)
+ else: self._contents[key] = [value]
+ self._path = path
self._contents_lock.release()
+
+ def clear(self):
+ """
+ Drops the configuration contents and reverts back to a blank, unloaded
+ state.
+ """
- return val
+ self._contents_lock.acquire()
+ self._path = None
+ self._contents.clear()
+ self._raw_contents = []
+ self._requested_keys = set()
+ self._contents_lock.release()
+
+ def update(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...
+
+ a. we don't have a value for that config_key
+ b. 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 get() method.
+
+ Arguments:
+ conf_mappings (dict) - configuration key/value mappings to be revised
+ limits (dict) - mappings of limits on numeric values, expected to
+ be of the form "configKey -> min" or "configKey ->
+ (min, max)"
+ """
+
+ if limits == 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 keys(self):
+ """
+ Provides all keys in the currently loaded configuration.
+
+ Returns:
+ list if strings for the configuration keys we've loaded
+ """
+
+ return self._contents.keys()
+
+ def unused_keys(self):
+ """
+ Provides the configuration keys that have never been provided to a caller
+ via the get, get_value, or update methods.
+
+ Returns:
+ set of configuration keys we've loaded but have never been requested
+ """
+
+ return set(self.get_keys()).difference(self._requested_keys)
def get(self, key, default = None):
"""
@@ -242,6 +335,36 @@ class Config():
return val
+ def get_value(self, key, default = None, multiple = False):
+ """
+ This provides the currently value associated with a given key.
+
+ Arguments:
+ key (str) - config setting to be fetched
+ default (object) - value provided if no such key exists
+ multiple (bool) - provides back a list of all values if true, otherwise
+ this returns the last loaded configuration value
+
+ Returns:
+ string or list of string configuration values associated with the given
+ key, providing the default if no such key exists
+ """
+
+ self._contents_lock.acquire()
+
+ if key in self._contents:
+ val = self._contents[key]
+ if not multiple: val = val[-1]
+ self._requested_keys.add(key)
+ else:
+ msg = "config entry '%s' not found, defaulting to '%s'" % (key, default)
+ LOGGER.debug(msg)
+ val = default
+
+ self._contents_lock.release()
+
+ return val
+
def get_str_csv(self, key, default = None, count = None):
"""
Fetches the given key as a comma separated value.
@@ -319,115 +442,4 @@ class Config():
LOGGER.info(error_msg)
return default
else: return [int(val) for val in conf_comp]
-
- def update(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...
-
- a. we don't have a value for that config_key
- b. 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 get() method.
-
- Arguments:
- conf_mappings (dict) - configuration key/value mappings to be revised
- limits (dict) - mappings of limits on numeric values, expected to
- be of the form "configKey -> min" or "configKey ->
- (min, max)"
- """
-
- if limits == 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 keys(self):
- """
- Provides all keys in the currently loaded configuration.
-
- Returns:
- list if strings for the configuration keys we've loaded
- """
-
- return self._contents.keys()
-
- def unused_keys(self):
- """
- Provides the configuration keys that have never been provided to a caller
- via the get, get_value, or update methods.
-
- Returns:
- set of configuration keys we've loaded but have never been requested
- """
-
- return set(self.get_keys()).difference(self._requested_keys)
-
- def reset(self):
- """
- Drops the configuration contents and reverts back to a blank, unloaded
- state.
- """
-
- self._contents_lock.acquire()
- self._path = None
- self._contents.clear()
- self._raw_contents = []
- self._requested_keys = set()
- self._contents_lock.release()
-
- def load(self, path):
- """
- Reads in the contents of the given path, adding its configuration values
- to our current contents.
-
- Arguments:
- path (str) - file path to be loaded
-
- Raises:
- IOError if we fail to read the file (it doesn't exist, insufficient
- permissions, etc)
- """
-
- config_file = open(path, "r")
- read_contents = config_file.readlines()
- config_file.close()
-
- self._contents_lock.acquire()
- self._raw_contents = read_contents
-
- for line in self._raw_contents:
- # strips any commenting or excess whitespace
- comment_start = line.find("#")
- if comment_start != -1: line = line[:comment_start]
- line = line.strip()
-
- # parse the key/value pair
- if line:
- if " " in line:
- key, value = line.split(" ", 1)
- value = value.strip()
- else:
- key, value = line, ""
-
- if key in self._contents: self._contents[key].append(value)
- else: self._contents[key] = [value]
-
- self._path = path
- self._contents_lock.release()
More information about the tor-commits
mailing list