From 9801c75720ceb5715e805c26d4be07c734155cac Mon Sep 17 00:00:00 2001 From: Romain Dorgueil Date: Wed, 5 Jul 2017 12:41:14 +0200 Subject: [PATCH] [settings] Better impl. of Setting class, tests for it and refactor hardcoded settings to use it. --- bonobo/_api.py | 2 +- bonobo/commands/__init__.py | 6 ++-- bonobo/commands/run.py | 4 +-- bonobo/ext/console.py | 4 +-- bonobo/logging.py | 2 +- bonobo/nodes/basics.py | 2 +- bonobo/settings.py | 53 ++++++++++++++++++++++++------- tests/test_settings.py | 63 +++++++++++++++++++++++++++++++++++++ 8 files changed, 115 insertions(+), 21 deletions(-) create mode 100644 tests/test_settings.py diff --git a/bonobo/_api.py b/bonobo/_api.py index 6cf328c..ab890c6 100644 --- a/bonobo/_api.py +++ b/bonobo/_api.py @@ -45,7 +45,7 @@ def run(graph, strategy=None, plugins=None, services=None): from bonobo import settings settings.check() - if not settings.QUIET: # pragma: no cover + if not settings.QUIET.get(): # pragma: no cover if _is_interactive_console(): from bonobo.ext.console import ConsoleOutputPlugin if ConsoleOutputPlugin not in plugins: diff --git a/bonobo/commands/__init__.py b/bonobo/commands/__init__.py index 59e6dfb..4e183a3 100644 --- a/bonobo/commands/__init__.py +++ b/bonobo/commands/__init__.py @@ -27,9 +27,9 @@ def entrypoint(args=None): args = parser.parse_args(args).__dict__ if args.pop('debug', False): - settings.DEBUG = True - settings.LOGGING_LEVEL = logging.DEBUG - logging.set_level(settings.LOGGING_LEVEL) + settings.DEBUG.set(True) + settings.LOGGING_LEVEL.set(logging.DEBUG) + logging.set_level(settings.LOGGING_LEVEL.get()) logger.debug('Command: ' + args['command'] + ' Arguments: ' + repr(args)) commands[args.pop('command')](**args) diff --git a/bonobo/commands/run.py b/bonobo/commands/run.py index 7f29d3f..6de6bf6 100644 --- a/bonobo/commands/run.py +++ b/bonobo/commands/run.py @@ -31,10 +31,10 @@ def execute(filename, module, install=False, quiet=False, verbose=False): from bonobo import Graph, run, settings if quiet: - settings.QUIET = True + settings.QUIET.set(True) if verbose: - settings.DEBUG = True + settings.DEBUG.set(True) if filename: if os.path.isdir(filename): diff --git a/bonobo/ext/console.py b/bonobo/ext/console.py index f30fae0..acf464b 100644 --- a/bonobo/ext/console.py +++ b/bonobo/ext/console.py @@ -65,7 +65,7 @@ class ConsoleOutputPlugin(Plugin): for i in context.graph.topologically_sorted_indexes: node = context[i] - name_suffix = '({})'.format(i) if settings.DEBUG else '' + name_suffix = '({})'.format(i) if settings.DEBUG.get() else '' if node.alive: _line = ''.join( ( @@ -100,7 +100,7 @@ class ConsoleOutputPlugin(Plugin): print(MOVE_CURSOR_UP(t_cnt + 2), file=sys.stderr) def _write(self, graph_context, rewind): - if settings.PROFILE: + if settings.PROFILE.get(): if self.counter % 10 and self._append_cache: append = self._append_cache else: diff --git a/bonobo/logging.py b/bonobo/logging.py index 17bdeb7..3784600 100644 --- a/bonobo/logging.py +++ b/bonobo/logging.py @@ -75,4 +75,4 @@ def get_logger(name='bonobo'): getLogger = get_logger # Setup formating and level. -setup(level=settings.LOGGING_LEVEL) +setup(level=settings.LOGGING_LEVEL.get()) diff --git a/bonobo/nodes/basics.py b/bonobo/nodes/basics.py index 85b2114..164eeb1 100644 --- a/bonobo/nodes/basics.py +++ b/bonobo/nodes/basics.py @@ -69,7 +69,7 @@ def _count_counter(self, context): class PrettyPrinter(Configurable): def call(self, *args, **kwargs): - formater = self._format_quiet if settings.QUIET else self._format_console + formater = self._format_quiet if settings.QUIET.get() else self._format_console for i, (item, value) in enumerate(itertools.chain(enumerate(args), kwargs.items())): print(formater(i, item, value)) diff --git a/bonobo/settings.py b/bonobo/settings.py index 8e8a780..e5edd83 100644 --- a/bonobo/settings.py +++ b/bonobo/settings.py @@ -5,6 +5,10 @@ from bonobo.errors import ValidationError def to_bool(s): + if s is None: + return False + if type(s) is bool: + return s if len(s): if s.lower() in ('f', 'false', 'n', 'no', '0'): return False @@ -13,7 +17,18 @@ def to_bool(s): class Setting: - def __init__(self, name, default=None, validator=None): + __all__ = {} + + @classmethod + def clear_all(cls): + for setting in Setting.__all__.values(): + setting.clear() + + def __new__(cls, name, *args, **kwargs): + Setting.__all__[name] = super().__new__(cls) + return Setting.__all__[name] + + def __init__(self, name, default=None, validator=None, formatter=None): self.name = name if default: @@ -21,15 +36,14 @@ class Setting: else: self.default = lambda: None - if validator: - self.validator = validator - else: - self.validator = None + self.validator = validator + self.formatter = formatter def __repr__(self): return ''.format(self.name, self.get()) def set(self, value): + value = self.formatter(value) if self.formatter else value if self.validator and not self.validator(value): raise ValidationError('Invalid value {!r} for setting {}.'.format(value, self.name)) self.value = value @@ -38,21 +52,35 @@ class Setting: try: return self.value except AttributeError: - self.value = self.default() + value = os.environ.get(self.name, None) + if value is None: + value = self.default() + self.set(value) return self.value + def clear(self): + try: + del self.value + except AttributeError: + pass + # Debug/verbose mode. -DEBUG = to_bool(os.environ.get('DEBUG', 'f')) +DEBUG = Setting('DEBUG', formatter=to_bool, default=False) # Profile mode. -PROFILE = to_bool(os.environ.get('PROFILE', 'f')) +PROFILE = Setting('PROFILE', formatter=to_bool, default=False) # Quiet mode. -QUIET = to_bool(os.environ.get('QUIET', 'f')) +QUIET = Setting('QUIET', formatter=to_bool, default=False) # Logging level. -LOGGING_LEVEL = logging.DEBUG if DEBUG else logging.INFO +LOGGING_LEVEL = Setting( + 'LOGGING_LEVEL', + formatter=logging._checkLevel, + validator=logging._checkLevel, + default=lambda: logging.DEBUG if DEBUG.get() else logging.INFO +) # Input/Output format for transformations IOFORMAT_ARG0 = 'arg0' @@ -67,5 +95,8 @@ IOFORMAT = Setting('IOFORMAT', default=IOFORMAT_KWARGS, validator=IOFORMATS.__co def check(): - if DEBUG and QUIET: + if DEBUG.get() and QUIET.get(): raise RuntimeError('I cannot be verbose and quiet at the same time.') + + +clear_all = Setting.clear_all diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 0000000..c8313c5 --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,63 @@ +import logging +from os import environ +from unittest.mock import patch + +import pytest + +from bonobo import settings + +TEST_SETTING = 'TEST_SETTING' + + +def test_to_bool(): + assert not settings.to_bool('') + assert not settings.to_bool('FALSE') + assert not settings.to_bool('NO') + assert not settings.to_bool('0') + + assert settings.to_bool('yup') + assert settings.to_bool('True') + assert settings.to_bool('yes') + assert settings.to_bool('1') + + +def test_setting(): + s = settings.Setting(TEST_SETTING) + assert s.get() is None + + with patch.dict(environ, {TEST_SETTING: 'hello'}): + assert s.get() is None + s.clear() + assert s.get() == 'hello' + + s = settings.Setting(TEST_SETTING, default='nope') + assert s.get() is 'nope' + + with patch.dict(environ, {TEST_SETTING: 'hello'}): + assert s.get() == 'nope' + s.clear() + assert s.get() == 'hello' + + +def test_default_settings(): + settings.clear_all() + + assert settings.DEBUG.get() == False + assert settings.PROFILE.get() == False + assert settings.QUIET.get() == False + assert settings.LOGGING_LEVEL.get() == logging._checkLevel('INFO') + + with patch.dict(environ, {'DEBUG': 't'}): + settings.clear_all() + assert settings.LOGGING_LEVEL.get() == logging._checkLevel('DEBUG') + + settings.clear_all() + + +def test_check(): + settings.check() + with patch.dict(environ, {'DEBUG': 't', 'PROFILE': 't', 'QUIET': 't'}): + settings.clear_all() + with pytest.raises(RuntimeError): + settings.check() + settings.clear_all()