Skip to content

Commit

Permalink
Add configured defaults (#145)
Browse files Browse the repository at this point in the history
* Initial work.

* Finish configured defaults.

* Fix mock imports for Python 2.7

* Code review feedback.
  • Loading branch information
tjprescott authored Mar 29, 2019
1 parent 248613c commit c63836a
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 24 deletions.
26 changes: 26 additions & 0 deletions knack/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .events import (EVENT_CMDLOADER_LOAD_COMMAND_TABLE, EVENT_CMDLOADER_LOAD_ARGUMENTS,
EVENT_COMMAND_CANCELLED)
from .log import get_logger
from .validators import DefaultInt, DefaultStr

logger = get_logger(__name__)

Expand Down Expand Up @@ -71,6 +72,18 @@ def __init__(self, cli_ctx, name, handler, description=None, table_transformer=N
def should_load_description(self):
return not self.cli_ctx.data['completer_active']

def _resolve_default_value_from_config_file(self, arg, overrides):
default_key = overrides.settings.get('configured_default', None)
if not default_key:
return

defaults_section = self.cli_ctx.config.defaults_section_name
config_value = self.cli_ctx.config.get(defaults_section, default_key, None)
if config_value:
logger.info("Configured default '%s' for arg %s", config_value, arg.name)
overrides.settings['default'] = DefaultStr(config_value)
overrides.settings['required'] = False

def load_arguments(self):
if self.arguments_loader:
cmd_args = self.arguments_loader()
Expand All @@ -87,7 +100,20 @@ def add_argument(self, param_name, *option_strings, **kwargs):

def update_argument(self, param_name, argtype):
arg = self.arguments[param_name]
# resolve defaults from either environment variable or config file
self._resolve_default_value_from_config_file(arg, argtype)
arg.type.update(other=argtype)
arg_default = arg.type.settings.get('default', None)
# apply DefaultStr and DefaultInt to allow distinguishing between
# when a default was applied or when the user specified a value
# that coincides with the default
if isinstance(arg_default, str):
arg_default = DefaultStr(arg_default)
elif isinstance(arg_default, int):
arg_default = DefaultInt(arg_default)
# update the default
if arg_default:
arg.type.settings['default'] = arg_default

def execute(self, **kwargs):
return self(**kwargs)
Expand Down
2 changes: 2 additions & 0 deletions knack/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class CLIConfig(object):
_DEFAULT_CONFIG_ENV_VAR_PREFIX = 'CLI'
_DEFAULT_CONFIG_DIR = os.path.join('~', '.{}'.format('cli'))
_DEFAULT_CONFIG_FILE_NAME = 'config'
_CONFIG_DEFAULTS_SECTION = 'defaults'

def __init__(self, config_dir=None, config_env_var_prefix=None, config_file_name=None):
""" Manages configuration options available in the CLI
Expand All @@ -44,6 +45,7 @@ def __init__(self, config_dir=None, config_env_var_prefix=None, config_file_name
configuration_file_name = config_file_name or CLIConfig._DEFAULT_CONFIG_FILE_NAME
self.config_path = os.path.join(self.config_dir, configuration_file_name)
self._env_var_format = '{}{}'.format(env_var_prefix, '{section}_{option}')
self.defaults_section_name = CLIConfig._CONFIG_DEFAULTS_SECTION
self.config_parser.read(self.config_path)

def env_var_name(self, section, option):
Expand Down
5 changes: 4 additions & 1 deletion knack/testsdk/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
# --------------------------------------------------------------------------------------------

import unittest
import mock
try:
import mock
except ImportError:
from unittest import mock
from .exceptions import CliTestError


Expand Down
20 changes: 20 additions & 0 deletions knack/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------


class DefaultStr(str):

def __new__(cls, *args, **kwargs):
instance = str.__new__(cls, *args, **kwargs)
instance.is_default = True
return instance


class DefaultInt(int):

def __new__(cls, *args, **kwargs):
instance = int.__new__(cls, *args, **kwargs)
instance.is_default = True
return instance
4 changes: 4 additions & 0 deletions tests/test_cli_scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

import os
import unittest
try:
import mock
except ImportError:
from unittest import mock
import mock

from collections import OrderedDict
Expand Down
90 changes: 90 additions & 0 deletions tests/test_command_with_configured_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
from __future__ import print_function
import os
import logging
import unittest
try:
import mock
except ImportError:
from unittest import mock
from six import StringIO
import sys

from knack.arguments import ArgumentsContext
from knack.commands import CLICommandsLoader, CLICommand, CommandGroup
from knack.config import CLIConfig
from tests.util import DummyCLI, redirect_io


# a dummy callback for arg-parse
def load_params(_):
pass


def list_foo(my_param):
print(str(my_param), end='')


class TestCommandWithConfiguredDefaults(unittest.TestCase):

@classmethod
def setUpClass(cls):
# Ensure initialization has occurred correctly
logging.basicConfig(level=logging.DEBUG)

@classmethod
def tearDownClass(cls):
logging.shutdown()

def _set_up_command_table(self, required):

class TestCommandsLoader(CLICommandsLoader):

def load_command_table(self, args):
super(TestCommandsLoader, self).load_command_table(args)
with CommandGroup(self, 'foo', '{}#{{}}'.format(__name__)) as g:
g.command('list', 'list_foo')
return self.command_table

def load_arguments(self, command):
with ArgumentsContext(self, 'foo') as c:
c.argument('my_param', options_list='--my-param',
configured_default='param', required=required)
super(TestCommandsLoader, self).load_arguments(command)
self.cli_ctx = DummyCLI(commands_loader_cls=TestCommandsLoader)

@mock.patch.dict(os.environ, {'CLI_DEFAULTS_PARAM': 'myVal'})
@redirect_io
def test_apply_configured_defaults_on_required_arg(self):
self._set_up_command_table(required=True)
self.cli_ctx.invoke('foo list'.split())
actual = self.io.getvalue()
expected = 'myVal'
self.assertEqual(expected, actual)

@redirect_io
def test_no_configured_default_on_required_arg(self):
self._set_up_command_table(required=True)
with self.assertRaises(SystemExit):
self.cli_ctx.invoke('foo list'.split())
actual = self.io.getvalue()
expected = 'required: --my-param'
if sys.version_info[0] == 2:
expected = 'argument --my-param is required'
self.assertEqual(expected in actual, True)

@mock.patch.dict(os.environ, {'CLI_DEFAULTS_PARAM': 'myVal'})
@redirect_io
def test_apply_configured_defaults_on_optional_arg(self):
self._set_up_command_table(required=False)
self.cli_ctx.invoke('foo list'.split())
actual = self.io.getvalue()
expected = 'myVal'
self.assertEqual(expected, actual)


if __name__ == '__main__':
unittest.main()
5 changes: 4 additions & 1 deletion tests/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

import os
import unittest
import mock
try:
import mock
except ImportError:
from unittest import mock

from knack.completion import CLICompletion, CaseInsensitiveChoicesCompleter, ARGCOMPLETE_ENV_NAME
from tests.util import MockContext
Expand Down
5 changes: 4 additions & 1 deletion tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
import stat
import unittest
import tempfile
import mock
try:
import mock
except ImportError:
from unittest import mock
from six.moves import configparser

from knack.config import CLIConfig, get_config_parser
Expand Down
23 changes: 5 additions & 18 deletions tests/test_deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@

from __future__ import unicode_literals

import sys
import unittest
import mock
try:
import mock
except ImportError:
from unittest import mock
from threading import Lock
from six import StringIO

from knack.arguments import ArgumentsContext
from knack.commands import CLICommand, CLICommandsLoader, CommandGroup

from tests.util import DummyCLI
from tests.util import DummyCLI, redirect_io


def example_handler(arg1, arg2=None, arg3=None):
Expand All @@ -27,20 +28,6 @@ def example_arg_handler(arg1, opt1, arg2=None, opt2=None, arg3=None,
pass


original_stdout = sys.stdout
original_stderr = sys.stderr


def redirect_io(func):
def wrapper(self):
sys.stdout = sys.stderr = self.io = StringIO()
func(self)
self.io.close()
sys.stdout = original_stderr
sys.stderr = original_stderr
return wrapper


class TestCommandDeprecation(unittest.TestCase):

def setUp(self):
Expand Down
5 changes: 4 additions & 1 deletion tests/test_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
# --------------------------------------------------------------------------------------------

import unittest
import mock
try:
import mock
except ImportError:
from unittest import mock
import logging
import colorama

Expand Down
5 changes: 4 additions & 1 deletion tests/test_prompting.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

import sys
import unittest
import mock
try:
import mock
except ImportError:
from unittest import mock
from six import StringIO

from knack.prompting import (verify_is_a_tty, NoTTYException, _INVALID_PASSWORD_MSG, prompt,
Expand Down
20 changes: 19 additions & 1 deletion tests/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,29 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import mock
try:
import mock
except ImportError:
from unittest import mock
import sys
import tempfile
from six import StringIO

from knack.cli import CLI, CLICommandsLoader, CommandInvoker

def redirect_io(func):

original_stderr = sys.stderr
original_stdout = sys.stdout

def wrapper(self):
sys.stdout = sys.stderr = self.io = StringIO()
func(self)
self.io.close()
sys.stdout = original_stderr
sys.stderr = original_stderr
return wrapper


class MockContext(CLI):

Expand Down

0 comments on commit c63836a

Please sign in to comment.