Skip to content

Commit e9a5cf9

Browse files
committed
Add capability for sub-managers to manage their own options
This change *removes* the ability to mix-and-match commands and options. This means that you now can do $ manage.py --host=webserver config backend --host=appserver The actual parameter spaces must still be different.
1 parent e489630 commit e9a5cf9

File tree

5 files changed

+166
-65
lines changed

5 files changed

+166
-65
lines changed

docs/index.rst

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -305,15 +305,46 @@ Suppose you have this command::
305305

306306
You can now run the following::
307307

308-
> python manage.py hello joe -c dev.cfg
308+
> python manage.py -c dev.cfg hello joe
309309
hello JOE
310310

311311
Assuming the ``USE_UPPERCASE`` setting is **True** in your dev.cfg file.
312312

313-
Notice also that the "config" option is **not** passed to the command.
313+
Notice also that the "config" option is **not** passed to the command. In
314+
fact, this usage
315+
316+
> python manage.py hello joe -c dev.cfg
317+
318+
will show an error message because the ``-c`` option does not belong to the
319+
``hello`` command.
320+
321+
You can attach same-named options to different levels; this allows you to
322+
add an option to your app setup code without checking whether it conflicts with
323+
a command:
324+
325+
@manager.option('-n', '--name', dest='name', default='joe')
326+
@manager.option('-c', '--clue', dest='clue', default='clue')
327+
def hello(name,clue):
328+
uppercase = app.config.get('USE_UPPERCASE', False)
329+
if uppercase:
330+
name = name.upper()
331+
clue = clue.upper()
332+
print "hello {}, get a {}!".format(name,clue)
333+
334+
> python manage.py -c dev.cfg hello -c cookie -n frank
335+
hello FRANK, get a COOKIE!
336+
337+
Note that the destination variables (command arguments, corresponding to
338+
``dest`` values) must still be different; this is a limitation of Python's
339+
argument parser.
314340

315341
In order for manager options to work you must pass a factory function, rather than a Flask instance, to your
316-
``Manager`` constructor. A simple but complete example is available in `this gist <https://gist.github.com/3531881>`_.
342+
``Manager`` constructor. A simple but complete example is available in `this gist <https://gist.github.com/smurfix/9307618>`_.
343+
344+
*New in version 0.7.0.*
345+
346+
Before version 0.7, options and command names could be interspersed freely.
347+
This is no longer possible.
317348

318349
Getting user input
319350
------------------
@@ -408,17 +439,39 @@ A Sub-Manager is an instance of ``Manager`` added as a command to another Manage
408439

409440
To create a submanager::
410441

411-
sub_manager = Manager()
442+
def sub_opts(app, **kwargs):
443+
pass
444+
sub_manager = Manager(sub_opts)
412445

413446
manager = Manager(self.app)
414447
manager.add_command("sub_manager", sub_manager)
415448

416-
Restrictions
417-
- A sub-manager does not provide an app instance/factory when created, it defers the calls to it's parent Manager's
418-
- A sub-manager inhert's the parent Manager's app options (used for the app instance/factory)
419-
- A sub-manager does not get default commands added to itself (by default)
420-
- A sub-manager must be added the primary/root ``Manager`` instance via ``add_command(sub_manager)``
421-
- A sub-manager can be added to another sub-manager as long as the parent sub-manager is added to the primary/root Manager
449+
If you attach options to the sub_manager, the ``sub_opts`` procedure will
450+
receive their values. Your application is passed in ``app`` for
451+
convenience.
452+
453+
If ``sub_opts`` returns a value other than ``None``, this value will replace
454+
the ``app`` value that's passed on. This way, you can implement a
455+
sub-manager which replaces the whole app. One use case is to create a
456+
separate administrative application for improved security::
457+
458+
def gen_admin(app, **kwargs):
459+
from myweb.admin import MyAdminApp
460+
## easiest but possibly incomplete way to copy your settings
461+
return MyAdminApp(config=app.config, **kwargs)
462+
sub_manager = Manager(gen_admin)
463+
464+
manager = Manager(MyApp)
465+
manager.add_command("admin", sub_manager)
466+
467+
> python manage.py runserver
468+
[ starts your normal server ]
469+
> python manage.py admin runserver
470+
[ starts an administrative server ]
471+
472+
You can cascade sub-managers, i.e. add one sub-manager to another.
473+
474+
A sub-manager does not get default commands added to itself (by default)
422475

423476
*New in version 0.5.0.*
424477

flask_script/__init__.py

Lines changed: 56 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,11 @@ def __init__(self, app=None, with_default_commands=None, usage=None,
7474
self._commands = dict()
7575
self._options = list()
7676

77-
# Primary/root Manager instance adds default commands by default,
78-
# Sub-Managers do not
79-
if with_default_commands or (app and with_default_commands is None):
80-
self.add_default_commands()
81-
8277
self.usage = usage
8378
self.help = help if help is not None else usage
8479
self.description = description if description is not None else usage
8580
self.disable_argcomplete = disable_argcomplete
81+
self.with_default_commands = with_default_commands
8682

8783
self.parent = None
8884

@@ -92,8 +88,10 @@ def add_default_commands(self):
9288
simply add your own equivalents using add_command or decorators.
9389
"""
9490

95-
self.add_command("shell", Shell())
96-
self.add_command("runserver", Server())
91+
if "shell" not in self._commands:
92+
self.add_command("shell", Shell())
93+
if "runserver" not in self._commands:
94+
self.add_command("runserver", Server())
9795

9896
def add_option(self, *args, **kwargs):
9997
"""
@@ -129,22 +127,33 @@ def create_app(config=None):
129127

130128
self._options.append(Option(*args, **kwargs))
131129

132-
def create_app(self, **kwargs):
133-
if self.parent:
134-
# Sub-manager, defer to parent Manager
130+
def create_app(self, app=None, **kwargs):
131+
if self.app is None:
132+
# defer to parent Manager
135133
return self.parent.create_app(**kwargs)
136134

137135
if isinstance(self.app, Flask):
138136
return self.app
139137

140-
return self.app(**kwargs)
138+
return self.app(**kwargs) or app
139+
140+
def __call__(self, app=None, *args, **kwargs):
141+
"""
142+
Call self.app()
143+
"""
144+
res = self.create_app(app, *args, **kwargs)
145+
if res is None:
146+
res = app
147+
return res
148+
141149

142-
def create_parser(self, prog, parents=None):
150+
def create_parser(self, prog, func_stack=(), parents=None):
143151
"""
144152
Creates an ArgumentParser instance from options returned
145153
by get_options(), and a subparser for the given command.
146154
"""
147155
prog = os.path.basename(prog)
156+
func_stack=func_stack+(self,)
148157

149158
options_parser = argparse.ArgumentParser(add_help=False)
150159
for option in self.get_options():
@@ -166,12 +175,7 @@ def create_parser(self, prog, parents=None):
166175
help = getattr(command, 'help', command.__doc__)
167176
description = getattr(command, 'description', command.__doc__)
168177

169-
# Only pass `parents` argument for commands that support it
170-
try:
171-
command_parser = command.create_parser(name, parents=[options_parser])
172-
except TypeError:
173-
warnings.warn("create_parser for {0} command should accept a `parents` argument".format(name), DeprecationWarning)
174-
command_parser = command.create_parser(name)
178+
command_parser = command.create_parser(name, func_stack=func_stack)
175179

176180
subparser = subparsers.add_parser(name, usage=usage, help=help,
177181
description=description,
@@ -186,6 +190,7 @@ def create_parser(self, prog, parents=None):
186190
and not self.disable_argcomplete:
187191
argcomplete.autocomplete(parser, always_complete_options=True)
188192

193+
self.parser = parser
189194
return parser
190195

191196
# def foo(self, app, *args, **kwargs):
@@ -207,9 +212,6 @@ def _parse_known_args(self, arg_strings, *args, **kw):
207212
parser._parse_known_args = types.MethodType(_parse_known_args, parser)
208213

209214
def get_options(self):
210-
if self.parent:
211-
return self.parent._options
212-
213215
return self._options
214216

215217
def add_command(self, *args, **kwargs):
@@ -357,49 +359,52 @@ def _make_context(app):
357359

358360
return func
359361

360-
def handle(self, prog, args=None):
362+
def set_defaults(self):
363+
if self.with_default_commands is None:
364+
self.with_default_commands = self.parent is None
365+
if self.with_default_commands:
366+
self.add_default_commands()
367+
self.with_default_commands = False
361368

369+
def handle(self, prog, args=None):
370+
self.set_defaults()
362371
app_parser = self.create_parser(prog)
363-
372+
364373
args = list(args or [])
365374
app_namespace, remaining_args = app_parser.parse_known_args(args)
366375

367376
# get the handle function and remove it from parsed options
368377
kwargs = app_namespace.__dict__
369-
handle = kwargs.pop('func_handle', None)
370-
if not handle:
378+
func_stack = kwargs.pop('func_stack', None)
379+
if not func_stack:
371380
app_parser.error('too few arguments')
372381

373-
# get only safe config options
374-
app_config_keys = [action.dest for action in app_parser._actions
375-
if action.__class__ in safe_actions]
382+
last_func = func_stack[-1]
383+
if remaining_args and not getattr(last_func, 'capture_all_args', False):
384+
app_parser.error('too many arguments')
376385

377-
# pass only safe app config keys
378-
app_config = dict((k, v) for k, v in iteritems(kwargs)
379-
if k in app_config_keys)
386+
args = []
387+
for handle in func_stack:
380388

381-
# remove application config keys from handle kwargs
382-
kwargs = dict((k, v) for k, v in iteritems(kwargs)
383-
if k not in app_config_keys)
389+
# get only safe config options
390+
config_keys = [action.dest for action in handle.parser._actions
391+
if handle is last_func or action.__class__ in safe_actions]
384392

385-
# get command from bound handle function (py2.7+)
386-
command = handle.__self__
387-
if getattr(command, 'capture_all_args', False):
388-
positional_args = [remaining_args]
389-
else:
390-
if len(remaining_args):
391-
# raise correct exception
392-
# FIXME maybe change capture_all_args flag
393-
app_parser.parse_args(args)
394-
# sys.exit(2)
395-
pass
396-
positional_args = []
397-
398-
app = self.create_app(**app_config)
399-
# for convience usage in a command
400-
self.app = app
393+
# pass only safe app config keys
394+
config = dict((k, v) for k, v in iteritems(kwargs)
395+
if k in config_keys)
396+
397+
# remove application config keys from handle kwargs
398+
kwargs = dict((k, v) for k, v in iteritems(kwargs)
399+
if k not in config_keys)
400+
401+
if handle is last_func and getattr(last_func, 'capture_all_args', False):
402+
args.append(remaining_args)
403+
res = handle(*args, **config)
404+
args = [res]
401405

402-
return handle(app, *positional_args, **kwargs)
406+
assert not kwargs
407+
return res
403408

404409
def run(self, commands=None, default_command=None):
405410
"""

flask_script/commands.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ def get_options(self):
114114

115115
def create_parser(self, *args, **kwargs):
116116

117+
func_stack = kwargs.pop('func_stack',())
117118
parser = argparse.ArgumentParser(*args, **kwargs)
118119

119120
for option in self.get_options():
@@ -132,10 +133,20 @@ def create_parser(self, *args, **kwargs):
132133
else:
133134
parser.add_argument(*option.args, **option.kwargs)
134135

135-
parser.set_defaults(func_handle=self.handle)
136+
parser.set_defaults(func_stack=func_stack+(self,))
136137

138+
self.parser = parser
137139
return parser
138140

141+
def __call__(self, app=None, *args, **kwargs):
142+
"""
143+
Compatibility code so that we can pass outselves to argparse
144+
as `func_handle`, above.
145+
The call to handle() is not replaced, so older code can still
146+
override it.
147+
"""
148+
return self.handle(app, *args, **kwargs)
149+
139150
def handle(self, app, *args, **kwargs):
140151
"""
141152
Handles the command with given app. Default behaviour is to call within

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
setup(
3131
name='Flask-Script',
32-
version='0.6.7',
32+
version='0.7.0-dev',
3333
url='http://github.com/techniq/flask-script',
3434
license='BSD',
3535
author='Dan Jacob',

tests.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ def run(self, name):
102102
print(name)
103103

104104

105+
class CommandWithOptionalArg(Command):
106+
'command with optional arg'
107+
108+
option_list = (
109+
Option('-n','--name', required=False),
110+
)
111+
112+
def run(self, name="NotGiven"):
113+
print("OK name="+str(name))
114+
115+
105116
class CommandWithOptions(Command):
106117
'command with options'
107118

@@ -173,13 +184,15 @@ def setup(self):
173184
def test_with_default_commands(self):
174185

175186
manager = Manager(self.app)
187+
manager.set_defaults()
176188

177189
assert 'runserver' in manager._commands
178190
assert 'shell' in manager._commands
179191

180192
def test_without_default_commands(self):
181193

182194
manager = Manager(self.app, with_default_commands=False)
195+
manager.set_defaults()
183196

184197
assert 'runserver' not in manager._commands
185198
assert 'shell' not in manager._commands
@@ -395,8 +408,8 @@ def test_global_option_provided_before_and_after_command(self, capsys):
395408

396409
code = run('manage.py simple -c Development', manager.run)
397410
out, err = capsys.readouterr()
398-
assert code == 0
399-
assert 'OK' in out
411+
assert code == 2
412+
assert 'OK' not in out
400413

401414
def test_global_option_value(self, capsys):
402415

@@ -687,6 +700,24 @@ def test_submanager_has_options(self, capsys):
687700
assert code == 0
688701
assert 'OK' in out
689702

703+
704+
def test_submanager_separate_options(self, capsys):
705+
706+
sub_manager = Manager(TestApp(verbose=True), with_default_commands=False)
707+
sub_manager.add_command('opt', CommandWithOptionalArg())
708+
sub_manager.add_option('-n', '--name', dest='name_sub', required=False)
709+
710+
manager = Manager(TestApp(verbose=True), with_default_commands=False)
711+
manager.add_command('sub_manager', sub_manager)
712+
manager.add_option('-n', '--name', dest='name_main', required=False)
713+
714+
code = run('manage.py -n MyMainName sub_manager -n MySubName opt -n MyName', manager.run)
715+
out, err = capsys.readouterr()
716+
assert code == 0
717+
assert 'APP name_main=MyMainName' in out
718+
assert 'APP name_sub=MySubName' in out
719+
assert 'OK name=MyName' in out
720+
690721
def test_manager_usage_with_submanager(self, capsys):
691722

692723
sub_manager = Manager(usage='Example sub-manager')
@@ -744,6 +775,7 @@ def test_submanager_has_no_default_commands(self):
744775

745776
manager = Manager()
746777
manager.add_command('sub_manager', sub_manager)
778+
manager.set_defaults()
747779

748780
assert 'runserver' not in sub_manager._commands
749781
assert 'shell' not in sub_manager._commands

0 commit comments

Comments
 (0)