Skip to content

Commit 328b3d8

Browse files
Froilan Irizarryjpadilla
authored andcommitted
Change optparse for argparse. (jpadilla#238)
1 parent 1f1d185 commit 328b3d8

File tree

4 files changed

+244
-90
lines changed

4 files changed

+244
-90
lines changed

.coveragerc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ omit =
44
.tox/*
55
setup.py
66
*.egg/*
7-
*/__main__.py
7+

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ env:
2323
- TOXENV=py35-contrib_crypto
2424
- TOXENV=py36-contrib_crypto
2525
- TOXENV=py27-contrib_crypto
26+
2627
install:
2728
- pip install -U pip
2829
- pip install -U tox coveralls

jwt/__main__.py

Lines changed: 115 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -2,135 +2,161 @@
22

33
from __future__ import absolute_import, print_function
44

5+
import argparse
56
import json
6-
import optparse
77
import sys
88
import time
99

10-
from . import DecodeError, __package__, __version__, decode, encode
10+
from . import DecodeError, __version__, decode, encode
1111

1212

13-
def main():
13+
def encode_payload(args):
14+
# Try to encode
15+
if args.key is None:
16+
raise ValueError('Key is required when encoding. See --help for usage.')
17+
18+
# Build payload object to encode
19+
payload = {}
20+
21+
for arg in args.payload:
22+
k, v = arg.split('=', 1)
23+
24+
# exp +offset special case?
25+
if k == 'exp' and v[0] == '+' and len(v) > 1:
26+
v = str(int(time.time()+int(v[1:])))
27+
28+
# Cast to integer?
29+
if v.isdigit():
30+
v = int(v)
31+
else:
32+
# Cast to float?
33+
try:
34+
v = float(v)
35+
except ValueError:
36+
pass
37+
38+
# Cast to true, false, or null?
39+
constants = {'true': True, 'false': False, 'null': None}
40+
41+
if v in constants:
42+
v = constants[v]
43+
44+
payload[k] = v
45+
46+
token = encode(
47+
payload,
48+
key=args.key,
49+
algorithm=args.algorithm
50+
)
51+
52+
return token.decode('utf-8')
53+
54+
55+
def decode_payload(args):
56+
try:
57+
if sys.stdin.isatty():
58+
token = sys.stdin.read()
59+
else:
60+
token = args.token
61+
62+
token = token.encode('utf-8')
63+
data = decode(token, key=args.key, verify=args.verify)
64+
65+
return json.dumps(data)
66+
67+
except DecodeError as e:
68+
raise DecodeError('There was an error decoding the token: %s' % e)
69+
1470

15-
usage = '''Encodes or decodes JSON Web Tokens based on input.
71+
def build_argparser():
1672

17-
%prog [options] input
73+
usage = '''
74+
Encodes or decodes JSON Web Tokens based on input.
1875
19-
Decoding examples:
76+
%(prog)s [options] <command> [options] input
2077
21-
%prog --key=secret json.web.token
22-
%prog --no-verify json.web.token
78+
Decoding examples:
2379
24-
Encoding requires the key option and takes space separated key/value pairs
25-
separated by equals (=) as input. Examples:
80+
%(prog)s --key=secret decode json.web.token
81+
%(prog)s decode --no-verify json.web.token
2682
27-
%prog --key=secret iss=me exp=1302049071
28-
%prog --key=secret foo=bar exp=+10
83+
Encoding requires the key option and takes space separated key/value pairs
84+
separated by equals (=) as input. Examples:
2985
30-
The exp key is special and can take an offset to current Unix time.\
31-
'''
32-
p = optparse.OptionParser(
33-
usage=usage,
86+
%(prog)s --key=secret encode iss=me exp=1302049071
87+
%(prog)s --key=secret encode foo=bar exp=+10
88+
89+
The exp key is special and can take an offset to current Unix time.
90+
'''
91+
92+
arg_parser = argparse.ArgumentParser(
3493
prog='pyjwt',
35-
version='%s %s' % (__package__, __version__),
94+
usage=usage
3695
)
3796

38-
p.add_option(
39-
'-n', '--no-verify',
40-
action='store_false',
41-
dest='verify',
42-
default=True,
43-
help='ignore signature and claims verification on decode'
97+
arg_parser.add_argument(
98+
'-v', '--version',
99+
action='version',
100+
version='%(prog)s ' + __version__
44101
)
45102

46-
p.add_option(
103+
arg_parser.add_argument(
47104
'--key',
48105
dest='key',
49106
metavar='KEY',
50107
default=None,
51108
help='set the secret key to sign with'
52109
)
53110

54-
p.add_option(
111+
arg_parser.add_argument(
55112
'--alg',
56113
dest='algorithm',
57114
metavar='ALG',
58115
default='HS256',
59116
help='set crypto algorithm to sign with. default=HS256'
60117
)
61118

62-
options, arguments = p.parse_args()
119+
subparsers = arg_parser.add_subparsers(
120+
title='PyJWT subcommands',
121+
description='valid subcommands',
122+
help='additional help'
123+
)
63124

64-
if len(arguments) > 0 or not sys.stdin.isatty():
65-
if len(arguments) == 1 and (not options.verify or options.key):
66-
# Try to decode
67-
try:
68-
if not sys.stdin.isatty():
69-
token = sys.stdin.read()
70-
else:
71-
token = arguments[0]
125+
# Encode subcommand
126+
encode_parser = subparsers.add_parser('encode', help='use to encode a supplied payload')
72127

73-
token = token.encode('utf-8')
74-
data = decode(token, key=options.key, verify=options.verify)
128+
payload_help = """Payload to encode. Must be a space separated list of key/value
129+
pairs separated by equals (=) sign."""
75130

76-
print(json.dumps(data))
77-
sys.exit(0)
78-
except DecodeError as e:
79-
print(e)
80-
sys.exit(1)
131+
encode_parser.add_argument('payload', nargs='+', help=payload_help)
132+
encode_parser.set_defaults(func=encode_payload)
81133

82-
# Try to encode
83-
if options.key is None:
84-
print('Key is required when encoding. See --help for usage.')
85-
sys.exit(1)
134+
# Decode subcommand
135+
decode_parser = subparsers.add_parser('decode', help='use to decode a supplied JSON web token')
136+
decode_parser.add_argument('token', help='JSON web token to decode.')
86137

87-
# Build payload object to encode
88-
payload = {}
138+
decode_parser.add_argument(
139+
'-n', '--no-verify',
140+
action='store_false',
141+
dest='verify',
142+
default=True,
143+
help='ignore signature and claims verification on decode'
144+
)
89145

90-
for arg in arguments:
91-
try:
92-
k, v = arg.split('=', 1)
146+
decode_parser.set_defaults(func=decode_payload)
147+
148+
return arg_parser
93149

94-
# exp +offset special case?
95-
if k == 'exp' and v[0] == '+' and len(v) > 1:
96-
v = str(int(time.time()+int(v[1:])))
97150

98-
# Cast to integer?
99-
if v.isdigit():
100-
v = int(v)
101-
else:
102-
# Cast to float?
103-
try:
104-
v = float(v)
105-
except ValueError:
106-
pass
151+
def main():
152+
arg_parser = build_argparser()
107153

108-
# Cast to true, false, or null?
109-
constants = {'true': True, 'false': False, 'null': None}
154+
try:
155+
arguments = arg_parser.parse_args(sys.argv[1:])
110156

111-
if v in constants:
112-
v = constants[v]
157+
output = arguments.func(arguments)
113158

114-
payload[k] = v
115-
except ValueError:
116-
print('Invalid encoding input at {}'.format(arg))
117-
sys.exit(1)
118-
119-
try:
120-
token = encode(
121-
payload,
122-
key=options.key,
123-
algorithm=options.algorithm
124-
)
125-
126-
print(token)
127-
sys.exit(0)
128-
except Exception as e:
129-
print(e)
130-
sys.exit(1)
131-
else:
132-
p.print_help()
133-
134-
135-
if __name__ == '__main__':
136-
main()
159+
print(output)
160+
except Exception as e:
161+
print('There was an unforseen error: ', e)
162+
arg_parser.print_help()

tests/test_cli.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
2+
import argparse
3+
import json
4+
import sys
5+
6+
import jwt
7+
from jwt.__main__ import build_argparser, decode_payload, encode_payload, main
8+
9+
import pytest
10+
11+
12+
class TestCli:
13+
14+
def test_build_argparse(self):
15+
args = ['--key', '1234', 'encode', 'name=Vader']
16+
parser = build_argparser()
17+
parsed_args = parser.parse_args(args)
18+
19+
assert parsed_args.key == '1234'
20+
21+
def test_encode_payload_raises_value_error_key_is_required(self):
22+
encode_args = ['encode', 'name=Vader', 'job=Sith']
23+
parser = build_argparser()
24+
25+
args = parser.parse_args(encode_args)
26+
27+
with pytest.raises(ValueError) as excinfo:
28+
encode_payload(args)
29+
30+
assert 'Key is required when encoding' in str(excinfo.value)
31+
32+
def test_decode_payload_raises_decoded_error(self):
33+
decode_args = ['--key', '1234', 'decode', 'wrong-token']
34+
parser = build_argparser()
35+
36+
args = parser.parse_args(decode_args)
37+
38+
with pytest.raises(jwt.DecodeError) as excinfo:
39+
decode_payload(args)
40+
41+
assert 'There was an error decoding the token' in str(excinfo.value)
42+
43+
def test_decode_payload_raises_decoded_error_isatty(self, monkeypatch):
44+
def patched_sys_stdin_read():
45+
raise jwt.DecodeError()
46+
47+
decode_args = ['--key', '1234', 'decode', 'wrong-token']
48+
parser = build_argparser()
49+
50+
args = parser.parse_args(decode_args)
51+
52+
monkeypatch.setattr(sys.stdin, 'isatty', lambda: True)
53+
monkeypatch.setattr(sys.stdin, 'read', patched_sys_stdin_read)
54+
55+
with pytest.raises(jwt.DecodeError) as excinfo:
56+
decode_payload(args)
57+
58+
assert 'There was an error decoding the token' in str(excinfo.value)
59+
60+
@pytest.mark.parametrize('key,name,job,exp,verify', [
61+
('1234', 'Vader', 'Sith', None, None),
62+
('4567', 'Anakin', 'Jedi', '+1', None),
63+
('4321', 'Padme', 'Queen', '4070926800', 'true'),
64+
])
65+
def test_encode_decode(self, key, name, job, exp, verify):
66+
encode_args = [
67+
'--key={0}'.format(key),
68+
'encode',
69+
'name={0}'.format(name),
70+
'job={0}'.format(job),
71+
]
72+
if exp:
73+
encode_args.append('exp={0}'.format(exp))
74+
if verify:
75+
encode_args.append('verify={0}'.format(verify))
76+
77+
parser = build_argparser()
78+
parsed_encode_args = parser.parse_args(encode_args)
79+
token = encode_payload(parsed_encode_args)
80+
assert token is not None
81+
assert token is not ''
82+
83+
decode_args = [
84+
'--key={0}'.format(key),
85+
'decode',
86+
token
87+
]
88+
parser = build_argparser()
89+
parsed_decode_args = parser.parse_args(decode_args)
90+
91+
actual = json.loads(decode_payload(parsed_decode_args))
92+
expected = {
93+
'job': job,
94+
'name': name,
95+
}
96+
assert actual['name'] == expected['name']
97+
assert actual['job'] == expected['job']
98+
99+
@pytest.mark.parametrize('key,name,job,exp,verify', [
100+
('1234', 'Vader', 'Sith', None, None),
101+
('4567', 'Anakin', 'Jedi', '+1', None),
102+
('4321', 'Padme', 'Queen', '4070926800', 'true'),
103+
])
104+
def test_main(self, monkeypatch, key, name, job, exp, verify):
105+
args = [
106+
'test_cli.py',
107+
'--key={0}'.format(key),
108+
'encode',
109+
'name={0}'.format(name),
110+
'job={0}'.format(job),
111+
]
112+
if exp:
113+
args.append('exp={0}'.format(exp))
114+
if verify:
115+
args.append('verify={0}'.format(verify))
116+
monkeypatch.setattr(sys, 'argv', args)
117+
main()
118+
119+
def test_main_throw_exception(self, monkeypatch, capsys):
120+
def patched_argparser_parse_args(self, args):
121+
raise Exception('NOOOOOOOOOOO!')
122+
123+
monkeypatch.setattr(argparse.ArgumentParser, 'parse_args', patched_argparser_parse_args)
124+
main()
125+
out, _ = capsys.readouterr()
126+
127+
assert 'NOOOOOOOOOOO!' in out

0 commit comments

Comments
 (0)