Skip to content
This repository has been archived by the owner on Mar 10, 2020. It is now read-only.

Commit

Permalink
Add capability for sub-managers to manage their own options
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
smurfix committed Mar 2, 2014
1 parent e489630 commit e9a5cf9
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 65 deletions.
73 changes: 63 additions & 10 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -305,15 +305,46 @@ Suppose you have this command::

You can now run the following::

> python manage.py hello joe -c dev.cfg
> python manage.py -c dev.cfg hello joe
hello JOE

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

Notice also that the "config" option is **not** passed to the command.
Notice also that the "config" option is **not** passed to the command. In
fact, this usage

> python manage.py hello joe -c dev.cfg

will show an error message because the ``-c`` option does not belong to the
``hello`` command.

You can attach same-named options to different levels; this allows you to
add an option to your app setup code without checking whether it conflicts with
a command:

@manager.option('-n', '--name', dest='name', default='joe')
@manager.option('-c', '--clue', dest='clue', default='clue')
def hello(name,clue):
uppercase = app.config.get('USE_UPPERCASE', False)
if uppercase:
name = name.upper()
clue = clue.upper()
print "hello {}, get a {}!".format(name,clue)

> python manage.py -c dev.cfg hello -c cookie -n frank
hello FRANK, get a COOKIE!

Note that the destination variables (command arguments, corresponding to
``dest`` values) must still be different; this is a limitation of Python's
argument parser.

In order for manager options to work you must pass a factory function, rather than a Flask instance, to your
``Manager`` constructor. A simple but complete example is available in `this gist <https://gist.github.com/3531881>`_.
``Manager`` constructor. A simple but complete example is available in `this gist <https://gist.github.com/smurfix/9307618>`_.

*New in version 0.7.0.*

Before version 0.7, options and command names could be interspersed freely.
This is no longer possible.

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

To create a submanager::

sub_manager = Manager()
def sub_opts(app, **kwargs):
pass
sub_manager = Manager(sub_opts)

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

Restrictions
- A sub-manager does not provide an app instance/factory when created, it defers the calls to it's parent Manager's
- A sub-manager inhert's the parent Manager's app options (used for the app instance/factory)
- A sub-manager does not get default commands added to itself (by default)
- A sub-manager must be added the primary/root ``Manager`` instance via ``add_command(sub_manager)``
- A sub-manager can be added to another sub-manager as long as the parent sub-manager is added to the primary/root Manager
If you attach options to the sub_manager, the ``sub_opts`` procedure will
receive their values. Your application is passed in ``app`` for
convenience.

If ``sub_opts`` returns a value other than ``None``, this value will replace
the ``app`` value that's passed on. This way, you can implement a
sub-manager which replaces the whole app. One use case is to create a
separate administrative application for improved security::

def gen_admin(app, **kwargs):
from myweb.admin import MyAdminApp
## easiest but possibly incomplete way to copy your settings
return MyAdminApp(config=app.config, **kwargs)
sub_manager = Manager(gen_admin)

manager = Manager(MyApp)
manager.add_command("admin", sub_manager)

> python manage.py runserver
[ starts your normal server ]
> python manage.py admin runserver
[ starts an administrative server ]

You can cascade sub-managers, i.e. add one sub-manager to another.

A sub-manager does not get default commands added to itself (by default)

*New in version 0.5.0.*

Expand Down
107 changes: 56 additions & 51 deletions flask_script/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,11 @@ def __init__(self, app=None, with_default_commands=None, usage=None,
self._commands = dict()
self._options = list()

# Primary/root Manager instance adds default commands by default,
# Sub-Managers do not
if with_default_commands or (app and with_default_commands is None):
self.add_default_commands()

self.usage = usage
self.help = help if help is not None else usage
self.description = description if description is not None else usage
self.disable_argcomplete = disable_argcomplete
self.with_default_commands = with_default_commands

self.parent = None

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

self.add_command("shell", Shell())
self.add_command("runserver", Server())
if "shell" not in self._commands:
self.add_command("shell", Shell())
if "runserver" not in self._commands:
self.add_command("runserver", Server())

def add_option(self, *args, **kwargs):
"""
Expand Down Expand Up @@ -129,22 +127,33 @@ def create_app(config=None):

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

def create_app(self, **kwargs):
if self.parent:
# Sub-manager, defer to parent Manager
def create_app(self, app=None, **kwargs):
if self.app is None:
# defer to parent Manager
return self.parent.create_app(**kwargs)

if isinstance(self.app, Flask):
return self.app

return self.app(**kwargs)
return self.app(**kwargs) or app

def __call__(self, app=None, *args, **kwargs):
"""
Call self.app()
"""
res = self.create_app(app, *args, **kwargs)
if res is None:
res = app
return res


def create_parser(self, prog, parents=None):
def create_parser(self, prog, func_stack=(), parents=None):
"""
Creates an ArgumentParser instance from options returned
by get_options(), and a subparser for the given command.
"""
prog = os.path.basename(prog)
func_stack=func_stack+(self,)

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

# Only pass `parents` argument for commands that support it
try:
command_parser = command.create_parser(name, parents=[options_parser])
except TypeError:
warnings.warn("create_parser for {0} command should accept a `parents` argument".format(name), DeprecationWarning)
command_parser = command.create_parser(name)
command_parser = command.create_parser(name, func_stack=func_stack)

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

self.parser = parser
return parser

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

def get_options(self):
if self.parent:
return self.parent._options

return self._options

def add_command(self, *args, **kwargs):
Expand Down Expand Up @@ -357,49 +359,52 @@ def _make_context(app):

return func

def handle(self, prog, args=None):
def set_defaults(self):
if self.with_default_commands is None:
self.with_default_commands = self.parent is None
if self.with_default_commands:
self.add_default_commands()
self.with_default_commands = False

def handle(self, prog, args=None):
self.set_defaults()
app_parser = self.create_parser(prog)

args = list(args or [])
app_namespace, remaining_args = app_parser.parse_known_args(args)

# get the handle function and remove it from parsed options
kwargs = app_namespace.__dict__
handle = kwargs.pop('func_handle', None)
if not handle:
func_stack = kwargs.pop('func_stack', None)
if not func_stack:
app_parser.error('too few arguments')

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

# pass only safe app config keys
app_config = dict((k, v) for k, v in iteritems(kwargs)
if k in app_config_keys)
args = []
for handle in func_stack:

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

# get command from bound handle function (py2.7+)
command = handle.__self__
if getattr(command, 'capture_all_args', False):
positional_args = [remaining_args]
else:
if len(remaining_args):
# raise correct exception
# FIXME maybe change capture_all_args flag
app_parser.parse_args(args)
# sys.exit(2)
pass
positional_args = []

app = self.create_app(**app_config)
# for convience usage in a command
self.app = app
# pass only safe app config keys
config = dict((k, v) for k, v in iteritems(kwargs)
if k in config_keys)

# remove application config keys from handle kwargs
kwargs = dict((k, v) for k, v in iteritems(kwargs)
if k not in config_keys)

if handle is last_func and getattr(last_func, 'capture_all_args', False):
args.append(remaining_args)
res = handle(*args, **config)
args = [res]

return handle(app, *positional_args, **kwargs)
assert not kwargs
return res

def run(self, commands=None, default_command=None):
"""
Expand Down
13 changes: 12 additions & 1 deletion flask_script/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def get_options(self):

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

func_stack = kwargs.pop('func_stack',())
parser = argparse.ArgumentParser(*args, **kwargs)

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

parser.set_defaults(func_handle=self.handle)
parser.set_defaults(func_stack=func_stack+(self,))

self.parser = parser
return parser

def __call__(self, app=None, *args, **kwargs):
"""
Compatibility code so that we can pass outselves to argparse
as `func_handle`, above.
The call to handle() is not replaced, so older code can still
override it.
"""
return self.handle(app, *args, **kwargs)

def handle(self, app, *args, **kwargs):
"""
Handles the command with given app. Default behaviour is to call within
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

setup(
name='Flask-Script',
version='0.6.7',
version='0.7.0-dev',
url='http://github.com/techniq/flask-script',
license='BSD',
author='Dan Jacob',
Expand Down
36 changes: 34 additions & 2 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ def run(self, name):
print(name)


class CommandWithOptionalArg(Command):
'command with optional arg'

option_list = (
Option('-n','--name', required=False),
)

def run(self, name="NotGiven"):
print("OK name="+str(name))


class CommandWithOptions(Command):
'command with options'

Expand Down Expand Up @@ -173,13 +184,15 @@ def setup(self):
def test_with_default_commands(self):

manager = Manager(self.app)
manager.set_defaults()

assert 'runserver' in manager._commands
assert 'shell' in manager._commands

def test_without_default_commands(self):

manager = Manager(self.app, with_default_commands=False)
manager.set_defaults()

assert 'runserver' not in manager._commands
assert 'shell' not in manager._commands
Expand Down Expand Up @@ -395,8 +408,8 @@ def test_global_option_provided_before_and_after_command(self, capsys):

code = run('manage.py simple -c Development', manager.run)
out, err = capsys.readouterr()
assert code == 0
assert 'OK' in out
assert code == 2
assert 'OK' not in out

def test_global_option_value(self, capsys):

Expand Down Expand Up @@ -687,6 +700,24 @@ def test_submanager_has_options(self, capsys):
assert code == 0
assert 'OK' in out


def test_submanager_separate_options(self, capsys):

sub_manager = Manager(TestApp(verbose=True), with_default_commands=False)
sub_manager.add_command('opt', CommandWithOptionalArg())
sub_manager.add_option('-n', '--name', dest='name_sub', required=False)

manager = Manager(TestApp(verbose=True), with_default_commands=False)
manager.add_command('sub_manager', sub_manager)
manager.add_option('-n', '--name', dest='name_main', required=False)

code = run('manage.py -n MyMainName sub_manager -n MySubName opt -n MyName', manager.run)
out, err = capsys.readouterr()
assert code == 0
assert 'APP name_main=MyMainName' in out
assert 'APP name_sub=MySubName' in out
assert 'OK name=MyName' in out

def test_manager_usage_with_submanager(self, capsys):

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

manager = Manager()
manager.add_command('sub_manager', sub_manager)
manager.set_defaults()

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

0 comments on commit e9a5cf9

Please sign in to comment.