Skip to content

Commit d5fe771

Browse files
committed
Add option to remember not saving config option
also add some preliminary config tests (todo setup/teardown)
1 parent 0d42906 commit d5fe771

File tree

5 files changed

+131
-23
lines changed

5 files changed

+131
-23
lines changed

pythonbits/config.py

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,31 @@
1313
CONFIG_DIR = appdirs.user_config_dir(appname.lower())
1414
CONFIG_PATH = path.join(CONFIG_DIR, CONFIG_NAME)
1515
CONFIG_VERSION = 1
16+
DEFAULT = object()
1617

1718
if not path.exists(CONFIG_DIR):
1819
makedirs(CONFIG_DIR, 0o700)
1920

2021

22+
class ConfidentialOption(Exception):
23+
pass
24+
25+
26+
class UnregisteredOption(Exception):
27+
pass
28+
29+
2130
class Config():
2231
registry = {}
2332

2433
def __init__(self, config_path=None):
2534
self.config_path = config_path or CONFIG_PATH
26-
self._config = configparser.ConfigParser()
35+
self._config = configparser.ConfigParser(allow_no_value=True)
2736

2837
def register(self, section, option, query, ask=False, getpass=False):
2938
self.registry[(section, option)] = {
3039
'query': query, 'ask': ask, 'getpass': getpass}
3140

32-
# todo: ask to remember choice if save is declined
33-
3441
def _write(self):
3542
with open(self.config_path, 'w') as configfile:
3643
self._config.write(configfile)
@@ -42,33 +49,49 @@ def set(self, section, option, value):
4249
self._config.set(section, option, value)
4350
self._write()
4451

45-
def get(self, section, option, default='dontguessthis'):
52+
def get(self, section, option, default=DEFAULT):
4653
self._config.read(self.config_path)
54+
4755
try:
48-
return self._config.get(section, option)
49-
except (configparser.NoSectionError, configparser.NoOptionError):
56+
value = self._config.get(section, option)
57+
if value is None:
58+
raise ConfidentialOption
59+
return value
60+
except (configparser.NoSectionError, configparser.NoOptionError,
61+
ConfidentialOption) as e:
62+
# if getter default is set, return it instead
63+
if default is not DEFAULT:
64+
return default
5065

66+
# get registered config option
5167
try:
5268
reg_option = self.registry[(section, option)]
5369
except KeyError:
54-
if default != 'dontguessthis': # dirty hack
55-
return default
56-
else:
57-
raise
70+
raise UnregisteredOption((section, option))
5871

72+
# get value from user query
5973
if reg_option['getpass']:
6074
value = getpass.getpass(reg_option['query'] + ": ")
6175
else:
6276
value = input(reg_option['query'] + ": ").strip()
6377

64-
if (reg_option['ask'] and
65-
input(
66-
'Would you like to save this value in {}? [Y/n] '.format(
67-
self.config_path)).lower() == 'n'):
78+
# user does not want to be prompted to save this option
79+
if isinstance(e, ConfidentialOption):
6880
return value
6981

70-
self.set(section, option, value)
82+
# user has choice ('ask') to save option value
83+
if reg_option['ask']:
84+
c = input('Would you like to save this value in {}? '
85+
'nr = no, and remember choice\n'
86+
'[Y/n/nr]'.format(self.config_path)).lower()
7187

88+
if c == 'n':
89+
return value
90+
elif c == 'nr':
91+
self.set(section, option, None)
92+
return value
93+
94+
self.set(section, option, value)
7295
return value
7396

7497

tests/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import os
2+
from unittest import mock
3+
import pythonbits.config as config
4+
5+
dir_path = os.path.dirname(os.path.realpath(__file__))
6+
config.config = config.Config(dir_path + '/pythonbits.cfg')
7+
config.config._write = mock.MagicMock()

tests/pythonbits.cfg

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
1+
[General]
2+
version = 1
3+
14
[Tracker]
2-
domain = mydomain.net
5+
domain = mydomain.net
6+
7+
[Foo]
8+
bar = baz
9+
bar2
10+
ham
11+
spam
12+
spam2 = spam2_value

tests/test_config.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import pytest
2+
from unittest import mock
3+
4+
import pythonbits.config as config
5+
c = config.config
6+
7+
c.register('Foo', 'bar', 'bar?')
8+
c.register('Foo', 'bar2', 'bar2?')
9+
c.register('Foo', 'bar3', 'bar3?', ask=True)
10+
c.register('Foo', 'bar4', 'bar4?', ask=True)
11+
c.register('Foo', 'ham', 'ham?')
12+
c.register('Foo', 'eggs', 'eggs?')
13+
14+
15+
def test_get():
16+
# todo: atomic tests with fresh config file
17+
18+
# registered option, present in config file
19+
assert c.get('Foo', 'bar') == 'baz'
20+
assert c.get('Foo', 'bar', 'bar_default') == 'baz'
21+
with mock.patch('builtins.input', lambda q: 'some_input'):
22+
assert c.get('Foo', 'ham') == 'some_input'
23+
24+
# registered option, present in config file, but empty
25+
assert c.get('Foo', 'bar2', 'bar2_default') == 'bar2_default'
26+
with mock.patch('builtins.input', lambda q: 'bar2_input'):
27+
assert c.get('Foo', 'bar2') == 'bar2_input'
28+
# assert c.get('Foo', 'bar2') == 'bar2_input' # see todo: non-mocked _write
29+
30+
# registered option, not present in config file, ask=True
31+
assert c.get('Foo', 'bar3', 'bar3_default') == 'bar3_default'
32+
with mock.patch('builtins.input', side_effect=['bar3_input', 'y']) as m:
33+
assert c.get('Foo', 'bar3') == 'bar3_input'
34+
with pytest.raises(StopIteration): # make sure all consumed
35+
next(m.side_effect)
36+
assert c.get('Foo', 'bar3') == 'bar3_input'
37+
38+
# lifecycle test: registered option, not present, ask=True
39+
assert c.get('Foo', 'bar4', 'bar4_default') == 'bar4_default'
40+
with mock.patch('builtins.input', side_effect=['bar4_input', 'n']) as m:
41+
assert c.get('Foo', 'bar4') == 'bar4_input'
42+
with pytest.raises(StopIteration): # make sure all consumed
43+
next(m.side_effect)
44+
assert c.get('Foo', 'bar4', 'bar4_default2') == 'bar4_default2'
45+
with mock.patch('builtins.input', side_effect=['bar4_input2', 'nr']) as m:
46+
assert c.get('Foo', 'bar4') == 'bar4_input2'
47+
with pytest.raises(StopIteration): # make sure all consumed
48+
next(m.side_effect)
49+
assert c.get('Foo', 'bar4', 'bar4_default3') == 'bar4_default3'
50+
with mock.patch('builtins.input', side_effect=['bar4_input3']) as m:
51+
assert c.get('Foo', 'bar4') == 'bar4_input3'
52+
with pytest.raises(StopIteration): # make sure all consumed
53+
next(m.side_effect)
54+
55+
# unregistered option, present in config file, but empty
56+
assert c.get('Foo', 'spam', 'spam_default') == 'spam_default'
57+
with pytest.raises(config.UnregisteredOption):
58+
c.get('Foo', 'spam')
59+
60+
# unregistered option, present in config file
61+
assert c.get('Foo', 'spam2') == 'spam2_value'
62+
assert c.get('Foo', 'spam2', 'spam2_default') == 'spam2_value'
63+
64+
# unregistered option, not present in config file
65+
assert c.get('Foo', 'spam3', 'spam3_default') == 'spam3_default'
66+
with pytest.raises(config.UnregisteredOption):
67+
c.get('Foo', 'spam3')
68+
69+
# registered option, not present in config file
70+
assert c.get('Foo', 'eggs', default='default_input') == 'default_input'
71+
72+
with mock.patch('builtins.input', lambda q: 'more_input'):
73+
assert c.get('Foo', 'eggs') == 'more_input'
74+
75+
assert c.get('Foo', 'eggs') == 'more_input'

tests/test_dummy.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
11
# -*- coding: utf-8 -*-
2-
import os
3-
import pythonbits.config as config
4-
5-
dir_path = os.path.dirname(os.path.realpath(__file__))
6-
config.config.config_path = dir_path + '/pythonbits.cfg'
7-
print(config.config.config_path)
8-
92
import pythonbits.submission as submission # noqa: E402
103
import pythonbits.bb as bb # noqa: E402
114
import pytest # noqa: E402

0 commit comments

Comments
 (0)