Skip to content

Commit c63836a

Browse files
authored
Add configured defaults (#145)
* Initial work. * Finish configured defaults. * Fix mock imports for Python 2.7 * Code review feedback.
1 parent 248613c commit c63836a

12 files changed

+186
-24
lines changed

knack/commands.py

+26
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .events import (EVENT_CMDLOADER_LOAD_COMMAND_TABLE, EVENT_CMDLOADER_LOAD_ARGUMENTS,
1818
EVENT_COMMAND_CANCELLED)
1919
from .log import get_logger
20+
from .validators import DefaultInt, DefaultStr
2021

2122
logger = get_logger(__name__)
2223

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

75+
def _resolve_default_value_from_config_file(self, arg, overrides):
76+
default_key = overrides.settings.get('configured_default', None)
77+
if not default_key:
78+
return
79+
80+
defaults_section = self.cli_ctx.config.defaults_section_name
81+
config_value = self.cli_ctx.config.get(defaults_section, default_key, None)
82+
if config_value:
83+
logger.info("Configured default '%s' for arg %s", config_value, arg.name)
84+
overrides.settings['default'] = DefaultStr(config_value)
85+
overrides.settings['required'] = False
86+
7487
def load_arguments(self):
7588
if self.arguments_loader:
7689
cmd_args = self.arguments_loader()
@@ -87,7 +100,20 @@ def add_argument(self, param_name, *option_strings, **kwargs):
87100

88101
def update_argument(self, param_name, argtype):
89102
arg = self.arguments[param_name]
103+
# resolve defaults from either environment variable or config file
104+
self._resolve_default_value_from_config_file(arg, argtype)
90105
arg.type.update(other=argtype)
106+
arg_default = arg.type.settings.get('default', None)
107+
# apply DefaultStr and DefaultInt to allow distinguishing between
108+
# when a default was applied or when the user specified a value
109+
# that coincides with the default
110+
if isinstance(arg_default, str):
111+
arg_default = DefaultStr(arg_default)
112+
elif isinstance(arg_default, int):
113+
arg_default = DefaultInt(arg_default)
114+
# update the default
115+
if arg_default:
116+
arg.type.settings['default'] = arg_default
91117

92118
def execute(self, **kwargs):
93119
return self(**kwargs)

knack/config.py

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class CLIConfig(object):
2323
_DEFAULT_CONFIG_ENV_VAR_PREFIX = 'CLI'
2424
_DEFAULT_CONFIG_DIR = os.path.join('~', '.{}'.format('cli'))
2525
_DEFAULT_CONFIG_FILE_NAME = 'config'
26+
_CONFIG_DEFAULTS_SECTION = 'defaults'
2627

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

4951
def env_var_name(self, section, option):

knack/testsdk/patches.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
# --------------------------------------------------------------------------------------------
55

66
import unittest
7-
import mock
7+
try:
8+
import mock
9+
except ImportError:
10+
from unittest import mock
811
from .exceptions import CliTestError
912

1013

knack/validators.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
7+
class DefaultStr(str):
8+
9+
def __new__(cls, *args, **kwargs):
10+
instance = str.__new__(cls, *args, **kwargs)
11+
instance.is_default = True
12+
return instance
13+
14+
15+
class DefaultInt(int):
16+
17+
def __new__(cls, *args, **kwargs):
18+
instance = int.__new__(cls, *args, **kwargs)
19+
instance.is_default = True
20+
return instance

tests/test_cli_scenarios.py

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55

66
import os
77
import unittest
8+
try:
9+
import mock
10+
except ImportError:
11+
from unittest import mock
812
import mock
913

1014
from collections import OrderedDict
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
from __future__ import print_function
6+
import os
7+
import logging
8+
import unittest
9+
try:
10+
import mock
11+
except ImportError:
12+
from unittest import mock
13+
from six import StringIO
14+
import sys
15+
16+
from knack.arguments import ArgumentsContext
17+
from knack.commands import CLICommandsLoader, CLICommand, CommandGroup
18+
from knack.config import CLIConfig
19+
from tests.util import DummyCLI, redirect_io
20+
21+
22+
# a dummy callback for arg-parse
23+
def load_params(_):
24+
pass
25+
26+
27+
def list_foo(my_param):
28+
print(str(my_param), end='')
29+
30+
31+
class TestCommandWithConfiguredDefaults(unittest.TestCase):
32+
33+
@classmethod
34+
def setUpClass(cls):
35+
# Ensure initialization has occurred correctly
36+
logging.basicConfig(level=logging.DEBUG)
37+
38+
@classmethod
39+
def tearDownClass(cls):
40+
logging.shutdown()
41+
42+
def _set_up_command_table(self, required):
43+
44+
class TestCommandsLoader(CLICommandsLoader):
45+
46+
def load_command_table(self, args):
47+
super(TestCommandsLoader, self).load_command_table(args)
48+
with CommandGroup(self, 'foo', '{}#{{}}'.format(__name__)) as g:
49+
g.command('list', 'list_foo')
50+
return self.command_table
51+
52+
def load_arguments(self, command):
53+
with ArgumentsContext(self, 'foo') as c:
54+
c.argument('my_param', options_list='--my-param',
55+
configured_default='param', required=required)
56+
super(TestCommandsLoader, self).load_arguments(command)
57+
self.cli_ctx = DummyCLI(commands_loader_cls=TestCommandsLoader)
58+
59+
@mock.patch.dict(os.environ, {'CLI_DEFAULTS_PARAM': 'myVal'})
60+
@redirect_io
61+
def test_apply_configured_defaults_on_required_arg(self):
62+
self._set_up_command_table(required=True)
63+
self.cli_ctx.invoke('foo list'.split())
64+
actual = self.io.getvalue()
65+
expected = 'myVal'
66+
self.assertEqual(expected, actual)
67+
68+
@redirect_io
69+
def test_no_configured_default_on_required_arg(self):
70+
self._set_up_command_table(required=True)
71+
with self.assertRaises(SystemExit):
72+
self.cli_ctx.invoke('foo list'.split())
73+
actual = self.io.getvalue()
74+
expected = 'required: --my-param'
75+
if sys.version_info[0] == 2:
76+
expected = 'argument --my-param is required'
77+
self.assertEqual(expected in actual, True)
78+
79+
@mock.patch.dict(os.environ, {'CLI_DEFAULTS_PARAM': 'myVal'})
80+
@redirect_io
81+
def test_apply_configured_defaults_on_optional_arg(self):
82+
self._set_up_command_table(required=False)
83+
self.cli_ctx.invoke('foo list'.split())
84+
actual = self.io.getvalue()
85+
expected = 'myVal'
86+
self.assertEqual(expected, actual)
87+
88+
89+
if __name__ == '__main__':
90+
unittest.main()

tests/test_completion.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
import os
77
import unittest
8-
import mock
8+
try:
9+
import mock
10+
except ImportError:
11+
from unittest import mock
912

1013
from knack.completion import CLICompletion, CaseInsensitiveChoicesCompleter, ARGCOMPLETE_ENV_NAME
1114
from tests.util import MockContext

tests/test_config.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
import stat
88
import unittest
99
import tempfile
10-
import mock
10+
try:
11+
import mock
12+
except ImportError:
13+
from unittest import mock
1114
from six.moves import configparser
1215

1316
from knack.config import CLIConfig, get_config_parser

tests/test_deprecation.py

+5-18
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@
55

66
from __future__ import unicode_literals
77

8-
import sys
98
import unittest
10-
import mock
9+
try:
10+
import mock
11+
except ImportError:
12+
from unittest import mock
1113
from threading import Lock
12-
from six import StringIO
1314

1415
from knack.arguments import ArgumentsContext
1516
from knack.commands import CLICommand, CLICommandsLoader, CommandGroup
1617

17-
from tests.util import DummyCLI
18+
from tests.util import DummyCLI, redirect_io
1819

1920

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

2930

30-
original_stdout = sys.stdout
31-
original_stderr = sys.stderr
32-
33-
34-
def redirect_io(func):
35-
def wrapper(self):
36-
sys.stdout = sys.stderr = self.io = StringIO()
37-
func(self)
38-
self.io.close()
39-
sys.stdout = original_stderr
40-
sys.stderr = original_stderr
41-
return wrapper
42-
43-
4431
class TestCommandDeprecation(unittest.TestCase):
4532

4633
def setUp(self):

tests/test_log.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
# --------------------------------------------------------------------------------------------
55

66
import unittest
7-
import mock
7+
try:
8+
import mock
9+
except ImportError:
10+
from unittest import mock
811
import logging
912
import colorama
1013

tests/test_prompting.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
import sys
77
import unittest
8-
import mock
8+
try:
9+
import mock
10+
except ImportError:
11+
from unittest import mock
912
from six import StringIO
1013

1114
from knack.prompting import (verify_is_a_tty, NoTTYException, _INVALID_PASSWORD_MSG, prompt,

tests/util.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,29 @@
33
# Licensed under the MIT License. See License.txt in the project root for license information.
44
# --------------------------------------------------------------------------------------------
55

6-
import mock
6+
try:
7+
import mock
8+
except ImportError:
9+
from unittest import mock
10+
import sys
711
import tempfile
12+
from six import StringIO
813

914
from knack.cli import CLI, CLICommandsLoader, CommandInvoker
1015

16+
def redirect_io(func):
17+
18+
original_stderr = sys.stderr
19+
original_stdout = sys.stdout
20+
21+
def wrapper(self):
22+
sys.stdout = sys.stderr = self.io = StringIO()
23+
func(self)
24+
self.io.close()
25+
sys.stdout = original_stderr
26+
sys.stderr = original_stderr
27+
return wrapper
28+
1129

1230
class MockContext(CLI):
1331

0 commit comments

Comments
 (0)