From 84d9f68badb97d35b5a509598dc27e4ca4e3fa5a Mon Sep 17 00:00:00 2001 From: Elisha Date: Mon, 10 Oct 2016 02:25:16 +0300 Subject: [PATCH 01/16] When submodules are defined, require a submodule. --- begin/cmdline.py | 1 + tests/test_start.py | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/begin/cmdline.py b/begin/cmdline.py index 73e55ae..8342508 100644 --- a/begin/cmdline.py +++ b/begin/cmdline.py @@ -177,6 +177,7 @@ def create_parser(func, env_prefix=None, config_file=None, config_section=None, if len(collector) > 0: subparsers = parser.add_subparsers(title='Available subcommands', dest='_subcommand') + subparsers.required = True for subfunc in collector.commands(): funcsig = signature(subfunc) help = None diff --git a/tests/test_start.py b/tests/test_start.py index 5649bbc..8d0278a 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -232,7 +232,7 @@ def test_multiple_subcommand(self): init = mock.Mock() target = mock.Mock() try: - orig_argv= sys.argv + orig_argv = sys.argv sys.argv = orig_argv[:1] + ['subcmd', '--', 'subcmd', '--', 'subcmd'] orig_name = globals()['__name__'] globals()['__name__'] = "__main__" @@ -248,6 +248,29 @@ def main(): sys.argv = orig_argv globals()['__name__'] = orig_name + def test_subcommand_required(self): + init = mock.Mock() + target = mock.Mock() + orig_name = globals()['__name__'] + orig_argv = sys.argv + try: + sys.argv = orig_argv[:1] + globals()['__name__'] = '__main__' + with self.assertRaises(SystemExit) as _: + @begin.subcommand + def a(): + pass + @begin.subcommand + def b(): + pass + @begin.start + def main(): + pass + finally: + sys.argv = orig_argv + globals()['__name__'] = orig_name + + @mock.patch('pkg_resources.iter_entry_points') def test_plugins(self, iep): epilogue = mock.Mock() From f84fed3a7fd00be1bedc32996b88855d36e8fcd0 Mon Sep 17 00:00:00 2001 From: Sam Jacobson Date: Tue, 18 Oct 2016 08:26:53 +1300 Subject: [PATCH 02/16] add facility to provide (program) defaults to logging extension --- begin/extensions.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/begin/extensions.py b/begin/extensions.py index 3afe32d..6dcd946 100644 --- a/begin/extensions.py +++ b/begin/extensions.py @@ -62,6 +62,17 @@ class Logging(Extension): section = 'logging' + def __init__(self, func, **kwargs): + Extension.__init__(self, func) + self.args = kwargs + + def logLevelName(self, value): + if value is None: + return value + if isinstance(value, str): + return value + return logging.getLevelName(value) + def add_arguments(self, parser, defaults): "Add command line arguments for configuring the logging module" exclusive = parser.add_mutually_exclusive_group() @@ -75,16 +86,16 @@ def add_arguments(self, parser, defaults): 'Detailed control of logging output') group.add_argument('--loglvl', default=defaults.from_name( - 'level', section=self.section, default=None), + 'level', section=self.section, default=self.logLevelName(self.args.get('level'))), choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], help='Set explicit log level') group.add_argument('--logfile', default=defaults.from_name( - 'file', section=self.section, default=None), + 'file', section=self.section, default=self.args.get('filename')), help='Output log messages to file') group.add_argument('--logfmt', default=defaults.from_name( - 'format', section=self.section, default=None), + 'format', section=self.section, default=self.args.get('format')), help='Log message format') def run(self, opts): @@ -121,6 +132,15 @@ def run(self, opts): handler.setFormatter(formatter) -def logger(func): +def logger(func=None, **kwargs): "Add command line extension for logging module" + def _logger(func): + return Logging(func, **kwargs) + + # logger() is a decorator factory + if func is None and len(kwargs) > 0: + return _logger + # not correctly used to decorate a function + elif not callable(func): + raise ValueError("Function '{0!r}' is not callable".format(func)) return Logging(func) From 542be024790176d08503cd85e708830a041f5fb6 Mon Sep 17 00:00:00 2001 From: tbartlett0 Date: Fri, 4 Nov 2016 14:01:41 +1100 Subject: [PATCH 03/16] Fix for config_section bug with subcommands Reorder code in cmdline:create_parser to correct a bug where the config_section setting was being ignored for the main function when subcommands are used. (The subcommand setup is overwriting the config section used by populate_parser, so do it after setting up the main function.) --- begin/cmdline.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/begin/cmdline.py b/begin/cmdline.py index 73e55ae..a33b6c0 100644 --- a/begin/cmdline.py +++ b/begin/cmdline.py @@ -171,6 +171,15 @@ def create_parser(func, env_prefix=None, config_file=None, config_section=None, description = func.__doc__, formatter_class=formatter_class ) + # Main function + while hasattr(func, '__wrapped__') and not hasattr(func, '__signature__'): + if isinstance(func, extensions.Extension): + func.add_arguments(parser, defaults) + have_extensions = True + func = getattr(func, '__wrapped__') + funcsig = signature(func) + populate_parser(parser, defaults, funcsig, short_args, lexical_order) + # Subcommands collector = collector if collector is not None else subcommands.COLLECTORS[sub_group] if plugins is not None: collector.load_plugins(plugins) @@ -188,13 +197,6 @@ def create_parser(func, env_prefix=None, config_file=None, config_section=None, defaults.set_config_section(subfunc.__name__) populate_parser(subparser, defaults, funcsig, short_args, lexical_order) have_extensions = False - while hasattr(func, '__wrapped__') and not hasattr(func, '__signature__'): - if isinstance(func, extensions.Extension): - func.add_arguments(parser, defaults) - have_extensions = True - func = getattr(func, '__wrapped__') - funcsig = signature(func) - populate_parser(parser, defaults, funcsig, short_args, lexical_order) return parser From d50a776e8d6bd5a06aad6b9baeea1cc5166df60b Mon Sep 17 00:00:00 2001 From: ismansiete Date: Fri, 6 Jan 2017 20:07:10 +0100 Subject: [PATCH 04/16] Enbaled the possibility to provide a logger and handler to @begin.loggin decorator. --- begin/extensions.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/begin/extensions.py b/begin/extensions.py index 3afe32d..1024c79 100644 --- a/begin/extensions.py +++ b/begin/extensions.py @@ -62,6 +62,11 @@ class Logging(Extension): section = 'logging' + def __init__(self, func, **kwargs): + super(Logging, self).__init__(func) + self.logger = kwargs.pop("logger", logging.getLogger()) + self.handler = kwargs.pop("handler", None) + def add_arguments(self, parser, defaults): "Add command line arguments for configuring the logging module" exclusive = parser.add_mutually_exclusive_group() @@ -98,18 +103,21 @@ def run(self, opts): elif opts.quiet: level = logging.WARNING # logger - logger = logging.getLogger() + logger = self.logger for handler in logger.handlers: logger.removeHandler(handler) logger.setLevel(level) # handler - if opts.logfile is None: - handler = logging.StreamHandler(sys.stdout) - elif platform.system() != 'Windows': - handler = logging.handlers.WatchedFileHandler(opts.logfile) + if not self.handler: + if opts.logfile is None: + handler = logging.StreamHandler(sys.stdout) + elif platform.system() != 'Windows': + handler = logging.handlers.WatchedFileHandler(opts.logfile) + else: + handler = logging.FileHandler(opts.logfile) + logger.addHandler(handler) else: - handler = logging.FileHandler(opts.logfile) - logger.addHandler(handler) + logger.addHandler(self.handler) # formatter fmt = opts.logfmt if fmt is None: @@ -121,6 +129,9 @@ def run(self, opts): handler.setFormatter(formatter) -def logger(func): +def logger(**kwargs): "Add command line extension for logging module" - return Logging(func) + def decorator(func): + return Logging(func, **kwargs) + return decorator + From ba012a18a52841303da83f06039f655adab58f01 Mon Sep 17 00:00:00 2001 From: ismansiete Date: Fri, 6 Jan 2017 20:24:05 +0100 Subject: [PATCH 05/16] Fix the issue with old way to declare begin.logging decorator. --- begin/__init__.py | 3 ++- begin/extensions.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/begin/__init__.py b/begin/__init__.py index 7f659da..6fffd59 100644 --- a/begin/__init__.py +++ b/begin/__init__.py @@ -7,7 +7,8 @@ from begin.version import __version__ from begin.extensions import tracebacks -from begin.extensions import logger as logging +from begin.extensions import logger_old as logging +from begin.extensions import logger import begin.formatters import begin.utils diff --git a/begin/extensions.py b/begin/extensions.py index 1024c79..6138c81 100644 --- a/begin/extensions.py +++ b/begin/extensions.py @@ -129,6 +129,10 @@ def run(self, opts): handler.setFormatter(formatter) +def logger_old(func): + "Add command line extension for logging module" + return Logging(func) + def logger(**kwargs): "Add command line extension for logging module" def decorator(func): From 7f8b1b0aef83d384fa004dcbc3e197d4052e3a0e Mon Sep 17 00:00:00 2001 From: ismansiete Date: Fri, 6 Jan 2017 21:17:12 +0100 Subject: [PATCH 06/16] Trying to pass all unit test in extesions file. --- begin/__init__.py | 2 +- begin/extensions.py | 28 +++++++++++++++------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/begin/__init__.py b/begin/__init__.py index 6fffd59..ac1aa24 100644 --- a/begin/__init__.py +++ b/begin/__init__.py @@ -7,7 +7,7 @@ from begin.version import __version__ from begin.extensions import tracebacks -from begin.extensions import logger_old as logging +from begin.extensions import logger_func as logging from begin.extensions import logger import begin.formatters diff --git a/begin/extensions.py b/begin/extensions.py index 6138c81..92e2660 100644 --- a/begin/extensions.py +++ b/begin/extensions.py @@ -64,8 +64,8 @@ class Logging(Extension): def __init__(self, func, **kwargs): super(Logging, self).__init__(func) - self.logger = kwargs.pop("logger", logging.getLogger()) - self.handler = kwargs.pop("handler", None) + self.arg_logger = kwargs.pop("logger", None) + self.arg_handler = kwargs.pop("handler", None) def add_arguments(self, parser, defaults): "Add command line arguments for configuring the logging module" @@ -103,21 +103,22 @@ def run(self, opts): elif opts.quiet: level = logging.WARNING # logger - logger = self.logger + + logger = self.arg_logger if self.arg_logger else logging.getLogger() for handler in logger.handlers: logger.removeHandler(handler) logger.setLevel(level) # handler - if not self.handler: - if opts.logfile is None: - handler = logging.StreamHandler(sys.stdout) - elif platform.system() != 'Windows': - handler = logging.handlers.WatchedFileHandler(opts.logfile) - else: - handler = logging.FileHandler(opts.logfile) - logger.addHandler(handler) + + if opts.logfile is None: + handler = logging.StreamHandler(sys.stdout) + elif platform.system() != 'Windows': + handler = logging.handlers.WatchedFileHandler(opts.logfile) else: - logger.addHandler(self.handler) + handler = logging.FileHandler(opts.logfile) + + logger.addHandler(self.arg_handler if self.arg_handler else handler) + # formatter fmt = opts.logfmt if fmt is None: @@ -129,10 +130,11 @@ def run(self, opts): handler.setFormatter(formatter) -def logger_old(func): +def logger_func(func): "Add command line extension for logging module" return Logging(func) + def logger(**kwargs): "Add command line extension for logging module" def decorator(func): From bf5022808426d0b496ab1d075fb3db68c431254d Mon Sep 17 00:00:00 2001 From: ismansiete Date: Fri, 6 Jan 2017 21:32:57 +0100 Subject: [PATCH 07/16] Trying to pass unittest, update from Pullrequest in previous repo. --- .travis.yml | 5 ++++- tests/test_cmdline.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8f1e2c1..7653a52 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,12 @@ python: - 3.2 - 3.3 - 3.4 + - 3.5 - pypy install: - pip install -r requirements.txt + # coverage 4 dropped support for 3.2 so we need to downgrade + - if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install 'coverage<4'; fi - python setup.py install script: - coverage run setup.py test @@ -15,4 +18,4 @@ script: after_success: - coveralls notifications: - email: aaron.iles+travis-ci@gmail.com + email: aaron.iles+travis-ci@gmail.com \ No newline at end of file diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 3b27150..7a2eeba 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -350,7 +350,7 @@ def main(): self.opts.tracebacks = True self.opts.tbdir = None cmdline.apply_options(main, self.opts) - enable.assert_called_one(format='txt', logdir=None) + enable.assert_called_once_with(format='txt', logdir=None) def test_subcommand(self): @subcommands.subcommand @@ -374,4 +374,4 @@ def main(): if __name__ == "__main__": - unittest.begin() + unittest.begin() \ No newline at end of file From cf9a5db077d688923beb6ff8e73ccc7c452e851c Mon Sep 17 00:00:00 2001 From: tbartlett0 Date: Mon, 23 Jan 2017 22:39:47 +1100 Subject: [PATCH 08/16] Remove unused have_extensions variable --- begin/cmdline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/begin/cmdline.py b/begin/cmdline.py index a33b6c0..ce7d0d8 100644 --- a/begin/cmdline.py +++ b/begin/cmdline.py @@ -175,7 +175,6 @@ def create_parser(func, env_prefix=None, config_file=None, config_section=None, while hasattr(func, '__wrapped__') and not hasattr(func, '__signature__'): if isinstance(func, extensions.Extension): func.add_arguments(parser, defaults) - have_extensions = True func = getattr(func, '__wrapped__') funcsig = signature(func) populate_parser(parser, defaults, funcsig, short_args, lexical_order) @@ -196,7 +195,6 @@ def create_parser(func, env_prefix=None, config_file=None, config_section=None, formatter_class=formatter_class) defaults.set_config_section(subfunc.__name__) populate_parser(subparser, defaults, funcsig, short_args, lexical_order) - have_extensions = False return parser From c7681a2c43797f08c78e61dfbd4499a4dc33eb33 Mon Sep 17 00:00:00 2001 From: Valentin Haenel Date: Mon, 19 Dec 2016 13:15:15 +0100 Subject: [PATCH 09/16] fix urls --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 5f51d3e..68fcbae 100644 --- a/README.rst +++ b/README.rst @@ -298,6 +298,6 @@ be made using GitHub's `issues system`_. :target: https://coveralls.io/r/aliles/begins?branch=master :alt: Latest PyPI version -.. |pypi_version| image:: https://pypip.in/v/begins/badge.png - :target: https://crate.io/packages/begins/ +.. |pypi_version| image:: https://img.shields.io/pypi/v/bloscpack.svg + :target: https://pypi.python.org/pypi/begins :alt: Latest PyPI version From 07021ae0a39f50904f7c6fbd538c158d94c42ad7 Mon Sep 17 00:00:00 2001 From: Robin Pedersen Date: Fri, 22 May 2015 14:56:08 +0200 Subject: [PATCH 10/16] Use file log format when stdout is not a tty --- begin/extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/begin/extensions.py b/begin/extensions.py index 3afe32d..07746fc 100644 --- a/begin/extensions.py +++ b/begin/extensions.py @@ -113,7 +113,7 @@ def run(self, opts): # formatter fmt = opts.logfmt if fmt is None: - if opts.logfile is None: + if sys.stdout.isatty() and opts.logfile is None: fmt = '%(message)s' else: fmt = '[%(asctime)s] [%(levelname)s] [%(pathname)s:%(lineno)s] %(message)s' From 287f6d1623eae3323310e89b13e9ce27eb248106 Mon Sep 17 00:00:00 2001 From: Giampaolo Mancini Date: Tue, 21 Feb 2017 18:20:10 +0100 Subject: [PATCH 11/16] Fix 'config_section' parameter not working. --- begin/cmdline.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/begin/cmdline.py b/begin/cmdline.py index 78df526..764c0aa 100644 --- a/begin/cmdline.py +++ b/begin/cmdline.py @@ -163,7 +163,8 @@ def create_parser(func, env_prefix=None, config_file=None, config_section=None, arguments will raise a ValueError exception. A prefix on expected environment variables can be added using the env_prefix argument. """ - defaults = DefaultsManager(env_prefix, config_file, func.__name__) + section = config_section if config_section is not None else func.__name__ + defaults = DefaultsManager(env_prefix, config_file, section) parser = argparse.ArgumentParser( prog=program_name(sys.argv[0], func), argument_default=NODEFAULT, @@ -194,7 +195,8 @@ def create_parser(func, env_prefix=None, config_file=None, config_section=None, subparser = subparsers.add_parser(subfunc.__name__, help=help, conflict_handler='resolve', description=subfunc.__doc__, formatter_class=formatter_class) - defaults.set_config_section(subfunc.__name__) + section = config_section if config_section is not None else subfunc.__name__ + defaults.set_config_section(section) populate_parser(subparser, defaults, funcsig, short_args, lexical_order) return parser From 07a87d045b57877df50ba14def69a1068ea4324a Mon Sep 17 00:00:00 2001 From: Giampaolo Mancini Date: Tue, 21 Feb 2017 18:23:45 +0100 Subject: [PATCH 12/16] Add support for multiple sections if 'config_section' is a list. --- begin/cmdline.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/begin/cmdline.py b/begin/cmdline.py index 78df526..f37e653 100644 --- a/begin/cmdline.py +++ b/begin/cmdline.py @@ -58,10 +58,16 @@ def from_name(self, name, default=NODEFAULT, section=None): "Get default value from argument name" if len(self._parser.sections()) > 0: section = self._section if section is None else section - try: - default = self._parser.get(section, name) - except (configparser.NoSectionError, configparser.NoOptionError): - pass + if isinstance(section, list): + sections = section + else: + sections = [] + sections.append(section) + for section in sections: + try: + default = self._parser.get(section, name) + except (configparser.NoSectionError, configparser.NoOptionError): + pass if self._use_env: default = os.environ.get(self.metavar(name), default) return default @@ -163,7 +169,8 @@ def create_parser(func, env_prefix=None, config_file=None, config_section=None, arguments will raise a ValueError exception. A prefix on expected environment variables can be added using the env_prefix argument. """ - defaults = DefaultsManager(env_prefix, config_file, func.__name__) + section = config_section if config_section is not None else func.__name__ + defaults = DefaultsManager(env_prefix, config_file, section) parser = argparse.ArgumentParser( prog=program_name(sys.argv[0], func), argument_default=NODEFAULT, @@ -194,7 +201,8 @@ def create_parser(func, env_prefix=None, config_file=None, config_section=None, subparser = subparsers.add_parser(subfunc.__name__, help=help, conflict_handler='resolve', description=subfunc.__doc__, formatter_class=formatter_class) - defaults.set_config_section(subfunc.__name__) + section = config_section if config_section is not None else subfunc.__name__ + defaults.set_config_section(section) populate_parser(subparser, defaults, funcsig, short_args, lexical_order) return parser From 4f58170907c4e4a75642a075f87833a1a9c33842 Mon Sep 17 00:00:00 2001 From: Giampaolo Mancini Date: Wed, 22 Feb 2017 18:06:45 +0100 Subject: [PATCH 13/16] Refactor config_section management (multiple sections too) and add tests. --- .gitignore | 3 ++ begin/cmdline.py | 101 ++++++++++++++++++++++++++------------- tests/config_test.cfg | 7 +++ tests/test_begins.py | 2 +- tests/test_cmdline.py | 41 +++++++++++++++- tests/test_extensions.py | 2 +- tests/test_start.py | 4 +- 7 files changed, 121 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index efbe7aa..beb4bdb 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ docs/_build/ # PyBuilder target/ + +# PyCharm +.idea diff --git a/begin/cmdline.py b/begin/cmdline.py index f37e653..07bc699 100644 --- a/begin/cmdline.py +++ b/begin/cmdline.py @@ -2,6 +2,7 @@ import argparse import os import sys +import warnings try: import configparser @@ -18,7 +19,6 @@ __all__ = ['create_parser', 'populate_parser', 'apply_options', 'call_function'] - NODEFAULT = object() @@ -37,10 +37,11 @@ def __init__(self, env_prefix=None, config_file=None, config_section=None): self._use_env = env_prefix is not None self._prefix = '' if not self._use_env else env_prefix self._parser = configparser.ConfigParser() - self._section = config_section + self._section = None + self.section = config_section if config_file is not None: self._parser.read([config_file, - os.path.join(os.path.expanduser('~'), config_file)]) + os.path.join(os.path.expanduser('~'), config_file)]) def metavar(self, name): "Generate meta variable name for parameter" @@ -56,24 +57,44 @@ def from_param(self, param, default=NODEFAULT): def from_name(self, name, default=NODEFAULT, section=None): "Get default value from argument name" - if len(self._parser.sections()) > 0: - section = self._section if section is None else section - if isinstance(section, list): - sections = section - else: - sections = [] - sections.append(section) - for section in sections: + sections = self._get_list_section(section) if section is not None else self.section + if len(sections) > 0: + for sec in self.section: try: - default = self._parser.get(section, name) + default = self._parser.get(sec, name) except (configparser.NoSectionError, configparser.NoOptionError): pass if self._use_env: default = os.environ.get(self.metavar(name), default) return default + @property + def section(self): + return self._section + + @section.setter + def section(self, section): + self._section = self._get_list_section(section) + + @staticmethod + def _get_list_section(section): + if isinstance(section, list): + _section = section + elif isinstance(section, str): + _section = list() + _section.append(section) + elif section is None: + return list() + else: + raise TypeError( + "'config_section' should be of {} or {}. You passed {}.".format(type(list()), + type(str()), + type(section))) + return _section + def set_config_section(self, section): - self._section = section + warnings.warn("deprecated. Please, use 'DefaultsManager.section' setter", DeprecationWarning) + self.section = section def program_name(filename, func): @@ -100,11 +121,11 @@ def populate_flag(parser, param, defaults): if param.annotation is not param.empty: help = param.annotation + ' ' parser.add_argument('--' + param.name.replace('_', '-'), - action='store_true', default=default, dest=param.name, - help=(help + '(default: %(default)s)'if not default else '')) + action='store_true', default=default, dest=param.name, + help=(help + '(default: %(default)s)' if not default else '')) parser.add_argument('--no-' + param.name.replace('_', '-'), - action='store_false', default=default, dest=param.name, - help=(help + '(default: %(default)s)' if default else '')) + action='store_false', default=default, dest=param.name, + help=(help + '(default: %(default)s)' if default else '')) def populate_option(parser, param, defaults, short_args): @@ -139,8 +160,8 @@ def populate_parser(parser, defaults, funcsig, short_args, lexical_order): params = sorted(params, key=lambda p: p.name) for param in params: if param.kind == param.POSITIONAL_OR_KEYWORD or \ - param.kind == param.KEYWORD_ONLY or \ - param.kind == param.POSITIONAL_ONLY: + param.kind == param.KEYWORD_ONLY or \ + param.kind == param.POSITIONAL_ONLY: if isinstance(param.default, bool): populate_flag(parser, param, defaults) else: @@ -157,8 +178,8 @@ def populate_parser(parser, defaults, funcsig, short_args, lexical_order): def create_parser(func, env_prefix=None, config_file=None, config_section=None, - short_args=True, lexical_order=False, sub_group=None, plugins=None, - collector=None, formatter_class=argparse.HelpFormatter): + short_args=True, lexical_order=False, sub_group=None, plugins=None, + collector=None, formatter_class=argparse.HelpFormatter): """Create and OptionParser object from a function definition. Use the function's signature to generate an OptionParser object. Default @@ -169,14 +190,19 @@ def create_parser(func, env_prefix=None, config_file=None, config_section=None, arguments will raise a ValueError exception. A prefix on expected environment variables can be added using the env_prefix argument. """ - section = config_section if config_section is not None else func.__name__ + section = func.__name__ + if config_section is not None: + if config_file is not None: + section = config_section + else: + warnings.warn("You passed 'config_section' but 'config_file' is missing.") defaults = DefaultsManager(env_prefix, config_file, section) parser = argparse.ArgumentParser( - prog=program_name(sys.argv[0], func), - argument_default=NODEFAULT, - conflict_handler='resolve', - description = func.__doc__, - formatter_class=formatter_class + prog=program_name(sys.argv[0], func), + argument_default=NODEFAULT, + conflict_handler='resolve', + description=func.__doc__, + formatter_class=formatter_class ) # Main function while hasattr(func, '__wrapped__') and not hasattr(func, '__signature__'): @@ -191,18 +217,23 @@ def create_parser(func, env_prefix=None, config_file=None, config_section=None, collector.load_plugins(plugins) if len(collector) > 0: subparsers = parser.add_subparsers(title='Available subcommands', - dest='_subcommand') + dest='_subcommand') subparsers.required = True for subfunc in collector.commands(): - funcsig = signature(subfunc) + funcsig = signature(subfunc) help = None if subfunc.__doc__ is not None: help = subfunc.__doc__.splitlines()[0] subparser = subparsers.add_parser(subfunc.__name__, help=help, - conflict_handler='resolve', description=subfunc.__doc__, - formatter_class=formatter_class) - section = config_section if config_section is not None else subfunc.__name__ - defaults.set_config_section(section) + conflict_handler='resolve', description=subfunc.__doc__, + formatter_class=formatter_class) + section = subfunc.__name__ + if config_section is not None: + if config_file is not None: + section = config_section + else: + warnings.warn("You passed 'config_section' but 'config_file' is missing.") + defaults.section = section populate_parser(subparser, defaults, funcsig, short_args, lexical_order) return parser @@ -216,6 +247,7 @@ def call_function(func, funcsig, opts): options object or to failure to use the command line arguments list will result in a CommandLineError being raised. """ + def getoption(opts, name, default=None): if not hasattr(opts, name): msg = "Missing command line options '{0}'".format(name) @@ -229,11 +261,12 @@ def getoption(opts, name, default=None): else: value = default return value + pargs = [] kwargs = {} for param in funcsig.parameters.values(): if param.kind == param.POSITIONAL_OR_KEYWORD or \ - param.kind == param.POSITIONAL_ONLY: + param.kind == param.POSITIONAL_ONLY: pargs.append(getoption(opts, param.name)) elif param.kind == param.VAR_POSITIONAL: pargs.extend(getoption(opts, param.name, [])) diff --git a/tests/config_test.cfg b/tests/config_test.cfg index e96f693..f45f210 100644 --- a/tests/config_test.cfg +++ b/tests/config_test.cfg @@ -1,2 +1,9 @@ [main] arg = value + +[foo] +arg = alt_value +foo_arg = foo_value + +[bar] +bar_arg = bar_value diff --git a/tests/test_begins.py b/tests/test_begins.py index 3a02327..dfdab11 100644 --- a/tests/test_begins.py +++ b/tests/test_begins.py @@ -29,4 +29,4 @@ def test_readme(self): if __name__ == '__main__': - unittest.begin() + unittest.main() diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 7a2eeba..a3deb82 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -174,6 +174,45 @@ def main(arg, unk): self.assertEqual(parser._optionals._actions[1].default, 'value') self.assertEqual(parser._optionals._actions[2].default, cmdline.NODEFAULT) + @mock.patch('os.path.expanduser') + def test_configfile_config_section(self, expanduser): + def main(arg, unk, foo_arg): + pass + expanduser.return_value = os.path.join(os.curdir, 'tests') + parser = cmdline.create_parser(main, config_file='config_test.cfg', config_section='foo') + self.assertEqual(parser._optionals._actions[1].default, 'alt_value') + self.assertEqual(parser._optionals._actions[2].default, cmdline.NODEFAULT) + self.assertEqual(parser._optionals._actions[3].default, 'foo_value') + + @mock.patch('os.path.expanduser') + def test_configfile_multiple_config_section(self, expanduser): + def main(arg, unk, foo_arg, bar_arg): + pass + expanduser.return_value = os.path.join(os.curdir, 'tests') + parser = cmdline.create_parser(main, config_file='config_test.cfg', config_section=['foo', 'bar']) + self.assertEqual(parser._optionals._actions[1].default, 'alt_value') + self.assertEqual(parser._optionals._actions[2].default, cmdline.NODEFAULT) + self.assertEqual(parser._optionals._actions[3].default, 'foo_value') + self.assertEqual(parser._optionals._actions[4].default, 'bar_value') + + @mock.patch('os.path.expanduser') + def test_configfile_wrong_config_section(self, expanduser): + def main(arg, unk, foo_arg, bar_arg): + pass + expanduser.return_value = os.path.join(os.curdir, 'tests') + with self.assertRaises(TypeError): + cmdline.create_parser(main, config_file='config_test.cfg', config_section=('foo', 'bar')) + with self.assertRaises(TypeError): + cmdline.create_parser(main, config_file='config_test.cfg', config_section={'foo', 'bar'}) + + @mock.patch('os.path.expanduser') + def test_configfile_missing_config_file(self, expanduser): + def main(arg, unk): + pass + expanduser.return_value = os.path.join(os.curdir, 'tests') + with self.assertWarns(UserWarning): + cmdline.create_parser(main, config_section='foo') + def test_help_strings(self): def main(a, b=None): pass @@ -374,4 +413,4 @@ def main(): if __name__ == "__main__": - unittest.begin() \ No newline at end of file + unittest.main() diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 6fdbfc5..7cf7f7c 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -181,4 +181,4 @@ def test_run_logfile_windows(self, getlogger, system): if __name__ == '__main__': - unittest.begin() + unittest.main() diff --git a/tests/test_start.py b/tests/test_start.py index 8d0278a..350e6ba 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -81,7 +81,7 @@ def main(): def test_command_line(self): target = mock.Mock() try: - orig_argv= sys.argv + orig_argv = sys.argv sys.argv = orig_argv[:1] + ['A'] orig_name = globals()['__name__'] globals()['__name__'] = "__main__" @@ -401,4 +401,4 @@ def main(): if __name__ == '__main__': - unittest.begin() + unittest.main() From b9f281caa15bf6cc13fe5a9e3873896672d35dbc Mon Sep 17 00:00:00 2001 From: Giampaolo Mancini Date: Tue, 7 Mar 2017 13:18:31 +0100 Subject: [PATCH 14/16] Remove not needed conditional, fix bug and break when value found. Add relevant test. --- begin/cmdline.py | 12 ++++++------ tests/config_test.cfg | 1 + tests/test_cmdline.py | 1 + 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/begin/cmdline.py b/begin/cmdline.py index 07bc699..9847e4a 100644 --- a/begin/cmdline.py +++ b/begin/cmdline.py @@ -58,12 +58,12 @@ def from_param(self, param, default=NODEFAULT): def from_name(self, name, default=NODEFAULT, section=None): "Get default value from argument name" sections = self._get_list_section(section) if section is not None else self.section - if len(sections) > 0: - for sec in self.section: - try: - default = self._parser.get(sec, name) - except (configparser.NoSectionError, configparser.NoOptionError): - pass + for sec in sections: + try: + default = self._parser.get(sec, name) + break + except (configparser.NoSectionError, configparser.NoOptionError): + pass if self._use_env: default = os.environ.get(self.metavar(name), default) return default diff --git a/tests/config_test.cfg b/tests/config_test.cfg index f45f210..1181c0c 100644 --- a/tests/config_test.cfg +++ b/tests/config_test.cfg @@ -7,3 +7,4 @@ foo_arg = foo_value [bar] bar_arg = bar_value +arg = bar_alt_value \ No newline at end of file diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index a3deb82..c735ac5 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -191,6 +191,7 @@ def main(arg, unk, foo_arg, bar_arg): expanduser.return_value = os.path.join(os.curdir, 'tests') parser = cmdline.create_parser(main, config_file='config_test.cfg', config_section=['foo', 'bar']) self.assertEqual(parser._optionals._actions[1].default, 'alt_value') + self.assertNotEqual(parser._optionals._actions[1].default, 'bar_alt_value') self.assertEqual(parser._optionals._actions[2].default, cmdline.NODEFAULT) self.assertEqual(parser._optionals._actions[3].default, 'foo_value') self.assertEqual(parser._optionals._actions[4].default, 'bar_value') From 45de8d7256b0010d1f7d4ea518905a0a6d6b9dcf Mon Sep 17 00:00:00 2001 From: Giampaolo Mancini Date: Tue, 7 Mar 2017 15:16:36 +0100 Subject: [PATCH 15/16] Explicit Set creation. Try to fix Python 2.6 CI error. --- tests/test_cmdline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index c735ac5..9748c70 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -204,7 +204,7 @@ def main(arg, unk, foo_arg, bar_arg): with self.assertRaises(TypeError): cmdline.create_parser(main, config_file='config_test.cfg', config_section=('foo', 'bar')) with self.assertRaises(TypeError): - cmdline.create_parser(main, config_file='config_test.cfg', config_section={'foo', 'bar'}) + cmdline.create_parser(main, config_file='config_test.cfg', config_section=set(['foo', 'bar'])) @mock.patch('os.path.expanduser') def test_configfile_missing_config_file(self, expanduser): @@ -345,7 +345,7 @@ def main(*args): def test_variable_keywords(self): def main(**kwargs): - return dict(args) + return dict(kwargs) with self.assertRaises(cmdline.CommandLineError): value = cmdline.apply_options(main, self.opts) From bbebf895765d9e0e343f73d66c0c78a6e33af5e5 Mon Sep 17 00:00:00 2001 From: Giampaolo Mancini Date: Fri, 21 Apr 2017 16:22:00 +0200 Subject: [PATCH 16/16] Explicitly number the format fields. Try to fix Python 2.6 CI error. --- begin/cmdline.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/begin/cmdline.py b/begin/cmdline.py index 9847e4a..1861767 100644 --- a/begin/cmdline.py +++ b/begin/cmdline.py @@ -87,9 +87,10 @@ def _get_list_section(section): return list() else: raise TypeError( - "'config_section' should be of {} or {}. You passed {}.".format(type(list()), - type(str()), - type(section))) + "'config_section' should be of {0} or {1}. You passed {2}.".format( + type(list()), + type(str()), + type(section))) return _section def set_config_section(self, section):