Skip to content

Commit 8fdc0c6

Browse files
committed
Merge pull request smurfix#55 from jirikuncar/subparsers
Use proper argparse subparsers
2 parents 5dc30c6 + 0d2c2de commit 8fdc0c6

File tree

3 files changed

+169
-111
lines changed

3 files changed

+169
-111
lines changed

flask_script/__init__.py

+93-83
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@
1616
__all__ = ["Command", "Shell", "Server", "Manager", "Group", "Option",
1717
"prompt", "prompt_pass", "prompt_bool", "prompt_choices"]
1818

19+
safe_actions = (argparse._StoreAction,
20+
argparse._StoreConstAction,
21+
argparse._StoreTrueAction,
22+
argparse._StoreFalseAction,
23+
argparse._AppendAction,
24+
argparse._AppendConstAction,
25+
argparse._CountAction)
26+
27+
28+
try:
29+
import argcomplete
30+
ARGCOMPLETE_IMPORTED = True
31+
except ImportError:
32+
ARGCOMPLETE_IMPORTED = False
33+
1934

2035
class Manager(object):
2136
"""
@@ -44,9 +59,12 @@ def run(self):
4459
:param app: Flask instance or callable returning a Flask instance.
4560
:param with_default_commands: load commands **runserver** and **shell**
4661
by default.
62+
:param disable_argcomplete: disable automatic loading of argcomplete.
63+
4764
"""
4865

49-
def __init__(self, app=None, with_default_commands=None, usage=None):
66+
def __init__(self, app=None, with_default_commands=None, usage=None,
67+
disable_argcomplete=False):
5068

5169
self.app = app
5270

@@ -58,7 +76,8 @@ def __init__(self, app=None, with_default_commands=None, usage=None):
5876
if with_default_commands or (app and with_default_commands is None):
5977
self.add_default_commands()
6078

61-
self.usage = usage
79+
self.usage = self.description = usage
80+
self.disable_argcomplete = disable_argcomplete
6281

6382
self.parent = None
6483

@@ -115,17 +134,42 @@ def create_app(self, **kwargs):
115134

116135
return self.app(**kwargs)
117136

118-
def create_parser(self, prog):
137+
def create_parser(self, prog, parents=None):
119138

120139
"""
121140
Creates an ArgumentParser instance from options returned
122141
by get_options(), and a subparser for the given command.
123142
"""
124-
125143
prog = os.path.basename(prog)
126-
parser = argparse.ArgumentParser(prog=prog)
144+
145+
option_parser = argparse.ArgumentParser(add_help=False)
127146
for option in self.get_options():
128-
parser.add_argument(*option.args, **option.kwargs)
147+
option_parser.add_argument(*option.args, **option.kwargs)
148+
149+
parser_parents = [option_parser] if parents is None else parents
150+
151+
def _create_command(item):
152+
name, command = item
153+
description = getattr(command, 'description',
154+
command.__doc__)
155+
return name, command, description, \
156+
command.create_parser(name, parents=parser_parents)
157+
158+
commands = map(_create_command, self._commands.iteritems())
159+
160+
parser = argparse.ArgumentParser(prog=prog, usage=self.usage,
161+
parents=parser_parents)
162+
163+
subparsers = parser.add_subparsers()
164+
for name, command, description, parent in commands:
165+
subparsers.add_parser(name, usage=description, help=description,
166+
parents=[parent], add_help=False)
167+
168+
## enable autocomplete only for parent parser when argcomplete is
169+
## imported and it is NOT disabled in constructor
170+
if parents is None and ARGCOMPLETE_IMPORTED \
171+
and not self.disable_argcomplete:
172+
argcomplete.autocomplete(parser, always_complete_options=True)
129173

130174
return parser
131175

@@ -167,6 +211,7 @@ def command(self, func):
167211
kwargs = dict(zip(*[reversed(l) for l in (args, defaults)]))
168212

169213
for arg in args:
214+
170215
if arg in kwargs:
171216

172217
default = kwargs[arg]
@@ -250,78 +295,50 @@ def _make_context(app):
250295

251296
return func
252297

253-
def get_usage(self):
298+
def handle(self, prog, args=None):
254299

255-
"""
256-
Returns string consisting of all commands and their
257-
descriptions.
258-
"""
259-
pad = max(map(len, self._commands.iterkeys())) + 2
260-
format = ' %%- %ds%%s' % pad
300+
app_parser = self.create_parser(prog)
261301

262-
rv = []
263-
264-
if self.usage:
265-
rv.append(self.usage)
266-
267-
for name, command in sorted(self._commands.iteritems()):
268-
usage = name
269-
270-
if isinstance(command, Manager):
271-
description = command.usage or ''
272-
else:
273-
description = command.description or ''
274-
275-
usage = format % (name, description)
276-
rv.append(usage)
277-
278-
return "\n".join(rv)
279-
280-
def print_usage(self):
281-
282-
"""
283-
Prints result of get_usage()
284-
"""
285-
286-
print self.get_usage()
287-
288-
def handle(self, prog, name, args=None):
302+
if args is None or len(args) == 0:
303+
app_parser.print_help()
304+
return 2
289305

290306
args = list(args or [])
307+
app_namespace, remaining_args = app_parser.parse_known_args(args)
291308

292-
try:
293-
command = self._commands[name]
294-
except KeyError:
295-
raise InvalidCommand("Command %s not found" % name)
309+
## get the handle function and remove it from parsed options
310+
kwargs = app_namespace.__dict__
311+
handle = kwargs['func_handle']
312+
del kwargs['func_handle']
296313

297-
if isinstance(command, Manager):
298-
# Run sub-manager, stripping first argument
299-
sys.argv = sys.argv[1:]
300-
command.run()
301-
else:
302-
help_args = ('-h', '--help')
314+
## get only safe config options
315+
app_config_keys = [action.dest for action in app_parser._actions
316+
if action.__class__ in safe_actions]
303317

304-
# remove -h/--help from args if present, and add to remaining args
305-
app_args = [a for a in args if a not in help_args]
318+
## pass only safe app config keys
319+
app_config = dict((k, v) for k, v in kwargs.iteritems()
320+
if k in app_config_keys)
306321

307-
app_parser = self.create_parser(prog)
308-
app_namespace, remaining_args = app_parser.parse_known_args(app_args)
309-
app = self.create_app(**app_namespace.__dict__)
322+
## remove application config keys from handle kwargs
323+
kwargs = dict((k, v) for k, v in kwargs.iteritems()
324+
if k not in app_config_keys)
310325

311-
for arg in help_args:
312-
if arg in args:
313-
remaining_args.append(arg)
326+
## get command from bounded handle function (py2.7+)
327+
command = handle.__self__
328+
if getattr(command, 'capture_all_args', False):
329+
positional_args = [remaining_args]
330+
else:
331+
if len(remaining_args):
332+
# raise correct exception
333+
# FIXME maybe change capture_all_args flag
334+
app_parser.parse_args(args)
335+
# sys.exit(2)
336+
pass
337+
positional_args = []
314338

315-
command_parser = command.create_parser(prog + " " + name)
316-
if getattr(command, 'capture_all_args', False):
317-
command_namespace, unparsed_args = \
318-
command_parser.parse_known_args(remaining_args)
319-
positional_args = [unparsed_args]
320-
else:
321-
command_namespace = command_parser.parse_args(remaining_args)
322-
positional_args = []
339+
app = self.create_app(**app_config)
323340

324-
return command.handle(app, *positional_args, **command_namespace.__dict__)
341+
return handle(app, *positional_args, **kwargs)
325342

326343
def run(self, commands=None, default_command=None):
327344

@@ -339,21 +356,14 @@ def run(self, commands=None, default_command=None):
339356
if commands:
340357
self._commands.update(commands)
341358

342-
try:
343-
try:
344-
command = sys.argv[1]
345-
except IndexError:
346-
command = default_command
359+
if default_command is not None and len(sys.argv) == 1:
360+
sys.argv.append(default_command)
347361

348-
if command is None:
349-
raise InvalidCommand("Please provide a command:")
350-
351-
result = self.handle(sys.argv[0], command, sys.argv[2:])
352-
353-
sys.exit(result or 0)
354-
355-
except InvalidCommand, e:
356-
print e
357-
self.print_usage()
362+
try:
363+
result = self.handle(sys.argv[0], sys.argv[1:])
364+
except SystemExit as e:
365+
result = e.code
366+
except Exception as e:
367+
raise e
358368

359-
sys.exit(1)
369+
sys.exit(result or 0)

flask_script/commands.py

+11-8
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def __init__(self, *options, **kwargs):
4848
if ((self.title or self.description) and
4949
(self.required or self.exclusive)):
5050
raise TypeError("title and/or description cannot be used with "
51-
"required and/or exclusive.")
51+
"required and/or exclusive.")
5252

5353
super(Group, self).__init__(**kwargs)
5454

@@ -113,26 +113,29 @@ def get_options(self):
113113
"""
114114
return self.option_list
115115

116-
def create_parser(self, prog):
117-
parser = argparse.ArgumentParser(prog=prog,
116+
def create_parser(self, prog, parents=None):
117+
118+
parser = argparse.ArgumentParser(prog=prog, parents=parents,
118119
description=self.description)
119120

120121
for option in self.get_options():
121122
if isinstance(option, Group):
122123
if option.exclusive:
123124
group = parser.add_mutually_exclusive_group(
124-
required=option.required,
125-
)
125+
required=option.required,
126+
)
126127
else:
127128
group = parser.add_argument_group(
128-
title=option.title,
129-
description=option.description,
130-
)
129+
title=option.title,
130+
description=option.description,
131+
)
131132
for opt in option.get_options():
132133
group.add_argument(*opt.args, **opt.kwargs)
133134
else:
134135
parser.add_argument(*option.args, **option.kwargs)
135136

137+
parser.set_defaults(func_handle=self.handle)
138+
136139
return parser
137140

138141
def handle(self, app, *args, **kwargs):

0 commit comments

Comments
 (0)