Commits

Can Xue committed 6a5f32c

make type=bool support 'on', 'off', etc, change doc strings and comments to English

  • Participants
  • Parent commits e549828

Comments (0)

Files changed (1)

File kahgean/options.py

-# -*- coding: utf-8 -*-
 # Copyright (C) 2012 Xue Can <xuecan@gmail.com> and contributors.
 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license
 
-"""配置管理
+"""
+Options
+=======
 
-这个模块为提供了一个从命令行参数和配置文件读取配置信息的统一的对象。该模块\
-使用标准库 argparse 定义配置项,并分析和处理命令行参数,如果命令行参数中指\
-定了配置文件,则使用标准库 ConfigParser 读取配置文件,并将配置信息以类似命\
-令行参数的形式使用 ArgumentParser 进行分析处理。
+The ``kahgean.options`` module makes it easy to parse command-line
+arguments and configuration file by just one set of defines. It's based
+on `argparse`_ and  `ConfigParser`_ in the Python standard library.
 
-应用程序通过创建 Options 类的实例,随后使用 add_option() 方法添加配置项,\
-通过调用 parse_options() 方法获取配置信息。默认的,命令行参数 --config-file
-被用于指定配置文件,若指定了配置文件,将会从配置文件中读取配置项。应用程序\
-可以修改类属性 config_argument 来更改指定配置文件的参数,修改类属性
-config_file_dest 来更改最终保存该参数的属性名。例如:
+.. _argparse: http://docs.python.org/library/argparse.html
+.. _ConfigParser: http://docs.python.org/library/configparser.html
 
-    Options.config_argument = ["-k", "--konfig-file"]
-    Options.config_file_dest = 'konfig_file'
+The program creates an instance of class ``Options``, then uses its
+``add_option()`` method to define what options it requires. The arguments
+of ``add_option()`` are same as ``ArgumentParser.add_argument()`` from
+module ``argparse``. By default, ``Options`` will add an optional argument
+which can be provided by command line ``-f`` or ``--config-file`` for
+tell the program where to find the configuration file. After then,
+by call the ``parse_options()`` method, ``Options`` will figure out how to
+parse those options out of ``sys.argv`` and/or from a given configuration file.
 
-这样,就可以通过命令行参数 -k 或者 --konfig-file 指定配置文件的路径,使用
-get('konfig_file') 或直接访问 Options 对象的 konfig_file 属性获取该文件对象。
+Options in a configuration file are listed under the "main" section. for
+example, command line arguments ``--host`` and ``--port`` can looked like
+below in a configuration file:
 
-配置文件中,若小节名为“main”,则直接使用其中的配置项名称,例如,小节“[main]”\
-中有配置“pidfile = /var/run/foo.pid”,则将被当作“--pidfile /var/run/foo.pid”\
-处理。否则,将以形如“小节名-配置名”的形式作为配置项名称。例如,小节“[logging]”\
-下有配置"level = warning",则将被当作 “--logging-level warning”处理。应用程序\
-可以修改类属性 main_section 来改变不进行参数名前缀添加的小节名。
+    [main]
+    host = 0.0.0.0
+    port = 8080
 
-同一个配置项若同时出现在命令行参数和配置文件中且相互冲突,则若命令行参数未\
-指定或指定的是该配置的默认值,则以配置文件指定的值为准;否则,以命令行参数的\
-值为准。
+If there are a set of options with same prefix, then they can be grouped by
+a section which name is the prefix, for example, command line arguments
+``--log-level`` and ``--log-filename`` can be grouped in the "log" section:
 
-当应用程序调用了 parse_options() 方法之后,可以通过 get() 方法获取经过分析得到\
-的配置,也可以直接访问 Options 对象对应配置名的属性获取配置。例如:
+    [log]
+    level = info
+    filename = /path/to/logfile
+
+If an option's type is ``bool``, then we can use '1', 'on', 'yes', and
+'true' for ``True``, and '0', 'off', 'no', and 'false' for ``False``.
+
+If both the configuration file and command-line define a same option, then
+the one comes from command-line will win. In order to check whether or not
+an option is provided by command-line, ``Options`` uses ``SUPPRESS`` as its
+default value internal, thus we cannot use ``nargs='*'`` or ``N``.
+
+Once ``parse_options()`` has been called, the program can use ``get()``
+to fetch an option value, for example:
 
     options = Options()
     options.add_option('--host', default='0.0.0.0')
-    options.add_option('--port', type=int, default=SUPPRESS)
+    options.add_option('--port', type=int)
     options.parse_options()
-    host = options.host
-    port = options.get('port', 80) # 若无该配置,使用 80 作为默认值
+    host = options.get('host')
+    port = options.get('port', 8080) # if neither config file nor command-line
+                                     # provide this, use 8080
 
-由于本模块主要是基于标准库 argparse 实现,有关定义配置项的详细说明,请参考\
-标准库关于 argparse 模块的说明。
+For more information, read the code please :-)
 """
 
 import sys
+import warnings
 from argparse import ArgumentParser, HelpFormatter, SUPPRESS
 from ConfigParser import SafeConfigParser
+import shlex
 
 __all__ = ['SUPPRESS', 'Options']
 
 
+def _bool(value):
+    value = str(value).lower()
+    if value in ['0', 'off', 'no', 'false']:
+        return False
+    elif value in ['1', 'on', 'yes', 'true']:
+        return True
+    else:
+        raise ValueError('unsupport boolean value')
+
+
 class Options(object):
-    """配置对象
-    
-    请参考标准库文档关于 ArgumentParser 类的说明了解实例化该类的参数
+    """the Options class
     """
     
-    config_argument = ['-f', '--config-file']
-    config_file_dest = 'config_file'
-    main_section = 'main'
-    
     def __init__(self, prog=None, description=None, epilog=None,
-                 argument_default=SUPPRESS, formatter_class=HelpFormatter):
+                 argument_default=SUPPRESS, formatter_class=HelpFormatter,
+                 config_argument=None, config_file_dest='config_file',
+                 main_section='main'):
+        self.config_argument = config_argument or ['-f', '--config-file']
+        self.config_file_dest = config_file_dest
+        self.main_section = main_section
         self._actions = dict()
+        self._defaults = dict()
         self._namespace = None
         self._arg_parser = ArgumentParser(prog, None, description, epilog,
                                           argument_default=argument_default,
                                           formatter_class=formatter_class)
         self.add_option(*self.config_argument, dest=self.config_file_dest,
                           metavar='filename', type=file, default=SUPPRESS,
-                          help='configuration file')
+                          help='path to the configuration file')
 
     def add_option(self, *args, **kwargs):
-        """添加配置项
+        """add option define
         
-        请参考 argparse 模块文档关于 add_argument 方法的说明以了解该方法的参数
+        For more information, please read the `argparse`_ document.
+        
+        .. _argparse:http://docs.python.org/library/argparse.html#\
+the-add-argument-method
         """
         action = self._arg_parser.add_argument(*args, **kwargs)
         self._actions[action.dest] = action
     
     def parse_options(self, args=None):
-        """从命令行和配置文件(如果在命令行中指定的话)中读取配置信息"""
-        namespace = self._parse_args(args)
-        if hasattr(namespace, self.config_file_dest):
-            ns = self._load_options(getattr(namespace, self.config_file_dest))
-            for key in ns.__dict__:
-                # 若 default 为 SUPPRESS 且未在命令行中指定该参数,
-                # self._namespace 中不会有该 key
-                # 此时使用配置文件的设置
-                if not hasattr(namespace, key):
-                    setattr(namespace, key, ns.__dict__[key])
-                    continue
-                # 若命令行参数中指定的参数为该参数的默认值
-                # 则使用配置文件的配置覆盖该参数
-                if getattr(namespace, key)==self._actions[key].default:
-                    setattr(namespace, key, ns.__dict__[key])
-        self._namespace = namespace
-
-    def _parse_args(self, args=None):
-        """从命令行参数中读取配置信息"""
-        namespace = self._arg_parser.parse_args(args)
-        return namespace
+        """parse options from command-line and/or configuration file"""
+        # preparing...
+        for action in self._actions:
+            # make bool support 'on', 'off', etc.
+            if action.type == bool:
+                action.type = _bool
+            # use SUPPRESS as default, so we can check whether or not
+            # an option is given by command-line
+            if action.default != SUPPRESS:
+                self._defaults[action.dest] = action.default
+                action.default = SUPPRESS
+            # there is a side effect, we cannot support nargs="+" or N
+            # use nargs="*" instead
+            if action.nargs=='+' or isinstance(action.nargs, int):
+                warnings.warn('not support nargs="+" or N, use "*" instead',
+                              Warning)
+                action.nargs = '*'
+        # parsing the command-line...
+        ns_a = self._arg_parser.parse_args(args)
+        if hasattr(ns_a, self.config_file_dest):
+            # parsing the configuration file...
+            ns_c = self._load_options(getattr(ns_a, self.config_file_dest))
+            for key in ns_c.__dict__:
+                # use values in the configuration file if they are not
+                # given in the command-line
+                if not hasattr(ns_a, key):
+                    setattr(ns_a, key, ns_c.__dict__[key])
+        # fetch back defaults
+        for dest in self._defaults:
+            if not hasattr(ns_a, dest):
+                setattr(ns_a, dest, self._defaults[dest])
+        self._namespace = ns_a
 
     def _option_to_arg(self, section, option, value):
-        # 将 [section] 下的 option = value 转变成 --section-option value
-        # 以便能以同命令行一样的方式处理
+        # convert option = value under [section] to --section-option value
+        # convert option under [section] to --section-option (if allow_no_value
+        # is True)
         if section == self.main_section:
             argument = '--%s' % option
         else:
             argument = '--%s-%s' % (section, option)
-        return [argument, value] if value else [argument]
+        return '%s %s' % (argument, value) if value else argument
 
-    def _load_options(self, file):
-        """从指定的文件中读取配置信息"""
-        # 从配置文件中读取数据并生成形如 --section-option value 的序列
-        # 以便后续可以使用命令行处理器分析处理
+    def _load_options(self, file_):
+        # generate a '--section-option value' sequence
         args = list()
         if sys.version_info >= (2, 7):
             parser = SafeConfigParser(allow_no_value=True)
         else:
+            warnings.warn('not "allow_no_value" supprt when '
+                          'parsing configuration files', Warning)
             parser = SafeConfigParser()
         parser.optionxform = str
-        parser.readfp(file)
+        parser.readfp(file_)
         sections = parser.sections()
         for section in sections:
             options = parser.options(section)
             for option in options:
                 value = parser.get(section, option)
-                args.extend(self._option_to_arg(section, option, value))
-        file.close()
+                args.append(self._option_to_arg(section, option, value))
+        file_.close()
+        # list -> str
+        args = ' '.join(args)
+        # str -> list again, by shlex.split(), so that it can parse quotes
+        args = shlex.split(args)
+        # configuration file may provide options for other program
+        # so we use parse_known_args
         namespace, _ = self._arg_parser.parse_known_args(args)
         return namespace
-    
-    def get(self, option, default=None):
-        """返回分析后的配置选项"""
+
+    def get(self, option, *args):
+        """get an option's value"""
         if not self._namespace:
             raise RuntimeError("parse_options() has not been call")
         option = option.replace('-', '_')
-        return getattr(self._namespace, option, default)
-    
-    def __getattr__(self, option):
-        if hasattr(self._namespace, option):
-            return getattr(self._namespace, option)
-        raise AttributeError("'Options' object has no attribute '%s'" % option)
+        return getattr(self._namespace, option, *args)