Skip to content

Commit 56d073b

Browse files
authored
Merge pull request #20 from virtualtam/cli/config/error-msg
cli: improve exception handling for erroneous configuration
2 parents 4b63579 + 8542fbe commit 56d073b

File tree

4 files changed

+185
-38
lines changed

4 files changed

+185
-38
lines changed

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ The format is based on `Keep a Changelog`_ and this project adheres to
2727
* rename ``--output`` to ``--format``
2828
* default to 'pprint' output format
2929
* improve endpoint-specific parser argument generation
30+
* improve exception handling and logging
3031

3132

3233
`v0.1.0 <https://github.com/shaarli/python-shaarli-client/releases/tag/v0.1.0>`_ - 2017-03-12

shaarli_client/config.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Configuration file utilities"""
2+
import logging
3+
import os
4+
from configparser import ConfigParser
5+
from pathlib import Path
6+
7+
8+
class InvalidConfiguration(Exception):
9+
"""Raised when invalid/no configuration is found"""
10+
11+
def __init__(self, message):
12+
"""Custom exception message"""
13+
super(InvalidConfiguration, self).__init__(
14+
"Invalid configuration: %s" % message
15+
)
16+
17+
18+
def get_credentials(args):
19+
"""Retrieve Shaarli authentication information"""
20+
if args.url and args.secret:
21+
# credentials passed as CLI arguments
22+
logging.warning("Passing credentials as arguments is unsafe"
23+
" and should be used for debugging only")
24+
return (args.url, args.secret)
25+
26+
config = ConfigParser()
27+
28+
if args.instance:
29+
instance = 'shaarli:{}'.format(args.instance)
30+
else:
31+
instance = 'shaarli'
32+
33+
if args.config:
34+
# user-specified configuration file
35+
config_files = config.read([args.config])
36+
else:
37+
# load configuration from a list of possible locations
38+
home = Path(os.path.expanduser('~'))
39+
config_files = config.read([
40+
str(home / '.config' / 'shaarli' / 'client.ini'),
41+
str(home / '.shaarli_client.ini'),
42+
'shaarli_client.ini'
43+
])
44+
45+
if not config_files:
46+
raise InvalidConfiguration("No configuration file found")
47+
48+
logging.info("Reading configuration from: %s",
49+
', '.join([str(cfg) for cfg in config_files]))
50+
51+
try:
52+
return (config[instance]['url'], config[instance]['secret'])
53+
except KeyError as exc:
54+
raise InvalidConfiguration("Missing entry: %s" % exc)

shaarli_client/main.py

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
"""shaarli-client main CLI entrypoint"""
22
import json
33
import logging
4-
import os
54
import sys
65
from argparse import ArgumentParser
7-
from configparser import ConfigParser
8-
from pathlib import Path
96

107
from .client import ShaarliV1Client
8+
from .config import InvalidConfiguration, get_credentials
119
from .utils import generate_all_endpoints_parsers
1210

1311

@@ -53,7 +51,9 @@ def main():
5351
try:
5452
url, secret = get_credentials(args)
5553
response = ShaarliV1Client(url, secret).request(args)
56-
54+
except InvalidConfiguration as exc:
55+
logging.error(exc)
56+
sys.exit(1)
5757
except (KeyError, TypeError, ValueError) as exc:
5858
logging.error(exc)
5959
parser.print_help()
@@ -65,37 +65,3 @@ def main():
6565
print(json.dumps(response.json(), sort_keys=True, indent=4))
6666
elif args.format == 'text':
6767
print(response.text)
68-
69-
70-
def get_credentials(args):
71-
"""Retrieve Shaarli authentication information"""
72-
if args.url and args.secret:
73-
# credentials passed as CLI arguments
74-
logging.warning("Passing credentials as arguments is unsafe"
75-
" and should be used for debugging only")
76-
return (args.url, args.secret)
77-
78-
config = ConfigParser()
79-
80-
if args.instance:
81-
instance = 'shaarli:{}'.format(args.instance)
82-
else:
83-
instance = 'shaarli'
84-
85-
if args.config:
86-
# user-specified configuration file
87-
logging.info("Reading configuration from: %s", args.config)
88-
with open(args.config, 'r') as f_config:
89-
config.read_file(f_config)
90-
else:
91-
# load configuration from a list of possible locations
92-
home = Path(os.path.expanduser('~'))
93-
config_files = config.read([
94-
home / '.config' / 'shaarli' / 'client.ini',
95-
home / '.shaarli_client.ini',
96-
'shaarli_client.ini'
97-
])
98-
logging.info("Reading configuration from: %s",
99-
', '.join([str(cfg) for cfg in config_files]))
100-
101-
return (config[instance]['url'], config[instance]['secret'])

tests/test_config.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""Tests for Shaarli configuration utilities"""
2+
# pylint: disable=invalid-name,redefined-outer-name
3+
from argparse import Namespace
4+
from configparser import ConfigParser
5+
6+
import pytest
7+
8+
from shaarli_client.config import InvalidConfiguration, get_credentials
9+
10+
SHAARLI_URL = 'http://shaar.li'
11+
SHAARLI_SECRET = 's3kr37'
12+
13+
14+
@pytest.fixture(scope='session')
15+
def shaarli_config(tmpdir_factory):
16+
"""Generate a client configuration file"""
17+
config = ConfigParser()
18+
config['shaarli'] = {
19+
'url': SHAARLI_URL,
20+
'secret': SHAARLI_SECRET
21+
}
22+
config['shaarli:shaaplin'] = {
23+
'url': SHAARLI_URL,
24+
'secret': SHAARLI_SECRET
25+
}
26+
config['shaarli:nourl'] = {
27+
'secret': SHAARLI_SECRET
28+
}
29+
config['shaarli:nosecret'] = {
30+
'url': SHAARLI_URL,
31+
}
32+
33+
config_path = tmpdir_factory.mktemp('config').join('shaarli_client.ini')
34+
35+
with config_path.open('w') as f_config:
36+
config.write(f_config)
37+
38+
return config_path
39+
40+
41+
def test_get_credentials_from_cli():
42+
"""Get authentication information as CLI parameters"""
43+
url, secret = get_credentials(
44+
Namespace(url=SHAARLI_URL, secret=SHAARLI_SECRET)
45+
)
46+
assert url == SHAARLI_URL
47+
assert secret == SHAARLI_SECRET
48+
49+
50+
@pytest.mark.parametrize('instance', [None, 'shaaplin'])
51+
def test_get_credentials_from_config(tmpdir, shaarli_config, instance):
52+
"""Read credentials from a standard location"""
53+
with tmpdir.as_cwd():
54+
shaarli_config.copy(tmpdir.join('shaarli_client.ini'))
55+
56+
url, secret = get_credentials(
57+
Namespace(
58+
config=None,
59+
instance=instance,
60+
url=None,
61+
secret=None
62+
)
63+
)
64+
65+
assert url == SHAARLI_URL
66+
assert secret == SHAARLI_SECRET
67+
68+
69+
@pytest.mark.parametrize('instance', [None, 'shaaplin'])
70+
def test_get_credentials_from_userconfig(shaarli_config, instance):
71+
"""Read credentials from a user-provided configuration file"""
72+
url, secret = get_credentials(
73+
Namespace(
74+
config=str(shaarli_config),
75+
instance=instance,
76+
url=None,
77+
secret=None
78+
)
79+
)
80+
assert url == SHAARLI_URL
81+
assert secret == SHAARLI_SECRET
82+
83+
84+
def test_get_credentials_no_config(monkeypatch):
85+
"""No configuration file found"""
86+
monkeypatch.setattr(ConfigParser, 'read', lambda x, y: [])
87+
88+
with pytest.raises(InvalidConfiguration) as exc:
89+
get_credentials(
90+
Namespace(
91+
config=None,
92+
instance=None,
93+
url=None,
94+
secret=None
95+
)
96+
)
97+
assert "No configuration file found" in str(exc.value)
98+
99+
100+
def test_get_credentials_missing_section(shaarli_config):
101+
"""The specified instance has no configuration section"""
102+
with pytest.raises(InvalidConfiguration) as exc:
103+
get_credentials(
104+
Namespace(
105+
config=str(shaarli_config),
106+
instance='nonexistent',
107+
url=None,
108+
secret=None
109+
)
110+
)
111+
assert "Missing entry: 'shaarli:nonexistent'" in str(exc.value)
112+
113+
114+
@pytest.mark.parametrize('attribute', ['url', 'secret'])
115+
def test_get_credentials_missing_attribute(shaarli_config, attribute):
116+
"""The specified instance has no configuration section"""
117+
with pytest.raises(InvalidConfiguration) as exc:
118+
get_credentials(
119+
Namespace(
120+
config=str(shaarli_config),
121+
instance='no{}'.format(attribute),
122+
url=None,
123+
secret=None
124+
)
125+
)
126+
assert "Missing entry: '{}'".format(attribute) in str(exc.value)

0 commit comments

Comments
 (0)