Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Support hierachical commands through sub-managers. closes #39

  • Loading branch information...
commit c08c9b6f8445bb899cfe07a30e185b074099474a 1 parent dd8c834
@techniq techniq authored
Showing with 256 additions and 76 deletions.
  1. +80 −0 docs/index.rst
  2. +94 −72 flask_script.py
  3. +1 −1  setup.py
  4. +81 −3 tests.py
View
80 docs/index.rst
@@ -404,6 +404,86 @@ to the ``Manager`` constructor these commands will not be loaded::
manager = Manager(app, with_default_commands=False)
+Sub-Managers
+-----------
+A Sub-Manager is an instance of ``Manager`` added as a command to another Manager
+
+To create a submanager::
+
+ sub_manager = Manager()
+
+ 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
+
+*New in version 0.5.0.*
+
+Note to extension developers
+----------------------------
+Extension developers can easily create convenient sub-manager instance within their extensions to make it easy for a user to consume all the available commands of an extension.
+
+Here is an example how a database extension could provide (ex. database.py)::
+ manager = Manager("Perform database operations")
+
+ @manager.command
+ def drop():
+ "Drops database tables"
+ if prompt_bool("Are you sure you want to lose all your data"):
+ db.drop_all()
+
+
+ @manager.command
+ def create(default_data=True, sample_data=False):
+ "Creates database tables from sqlalchemy models"
+ db.create_all()
+ populate(default_data, sample_data)
+
+
+ @manager.command
+ def recreate(default_data=True, sample_data=False):
+ "Recreates database tables (same as issuing 'drop' and then 'create')"
+ drop()
+ create(default_data, sample_data)
+
+
+ @manager.command
+ def populate(default_data=False, sample_data=False):
+ "Populate database with default data"
+ from fixtures import dbfixture
+
+ if default_data:
+ from fixtures.default_data import all
+ default_data = dbfixture.data(*all)
+ default_data.setup()
+
+ if sample_data:
+ from fixtures.sample_data import all
+ sample_data = dbfixture.data(*all)
+ sample_data.setup()
+
+
+Then the user can register the sub-manager to their primary Manager (within manage.py)::
+ manager = Manager(app)
+
+ from flask.ext.database import manager as database_manager
+ manager.add_command("database", database_manager)
+
+The commands will then be available::
+ > python manage.py database
+
+ Please provide a command:
+
+ Perform database operations
+ create Creates database tables from sqlalchemy models
+ drop Drops database tables
+ populate Populate database with default data
+ recreate Recreates database tables (same as issuing 'drop' and then 'create')
Accessing local proxies
-----------------------
View
166 flask_script.py
@@ -397,7 +397,6 @@ class InvalidCommand(Exception):
class Manager(object):
-
"""
Controller class for handling a set of commands.
@@ -426,18 +425,23 @@ def run(self):
by default.
"""
- def __init__(self, app, with_default_commands=True, usage=None):
+ def __init__(self, app=None, with_default_commands=None, usage=None):
self.app = app
self._commands = dict()
self._options = list()
- if with_default_commands:
+ # 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.parent = None
+
+
def add_default_commands(self):
"""
Adds the shell and runserver default commands. To override these
@@ -447,30 +451,6 @@ def add_default_commands(self):
self.add_command("shell", Shell())
self.add_command("runserver", Server())
- def create_app(self, **kwargs):
-
- if isinstance(self.app, Flask):
- return self.app
-
- return self.app(**kwargs)
-
- def create_parser(self, prog):
-
- """
- Creates an ArgumentParser instance from options returned
- by get_options(), and a subparser for the given command.
- """
-
- prog = os.path.basename(prog)
- parser = argparse.ArgumentParser(prog=prog)
- for option in self.get_options():
- parser.add_argument(*option.args, **option.kwargs)
-
- return parser
-
- def get_options(self):
- return self._options
-
def add_option(self, *args, **kwargs):
"""
Adds an application-wide option. This is useful if you want to set
@@ -505,9 +485,52 @@ def create_app(config=None):
self._options.append(Option(*args, **kwargs))
+ def create_app(self, **kwargs):
+ # Sub-manager, defer to parent Manager
+ if self.parent:
+ return self.parent.create_app(**kwargs)
+
+ if isinstance(self.app, Flask):
+ return self.app
+
+ return self.app(**kwargs)
+
+ def create_parser(self, prog):
+
+ """
+ Creates an ArgumentParser instance from options returned
+ by get_options(), and a subparser for the given command.
+ """
+
+ prog = os.path.basename(prog)
+ parser = argparse.ArgumentParser(prog=prog)
+ for option in self.get_options():
+ parser.add_argument(*option.args, **option.kwargs)
+
+ return parser
+
+ def get_options(self):
+ if self.parent:
+ return self.parent._options
+
+ return self._options
+
+ def add_command(self, name, command):
+
+ """
+ Adds command to registry.
+
+ :param command: Command instance
+ """
+
+ if isinstance(command, Manager):
+ command.parent = self
+
+ self._commands[name] = command
+
def command(self, func):
"""
- Adds a command function to the registry.
+ Decorator to add a command function to the registry.
:param func: command function.Arguments depend on the
options.
@@ -555,25 +578,6 @@ def command(self, func):
return func
- def shell(self, func):
- """
- Decorator that wraps function in shell command. This is equivalent to::
-
- def _make_context(app):
- return dict(app=app)
-
- manager.add_command("shell", Shell(make_context=_make_context))
-
- The decorated function should take a single "app" argument, and return
- a dict.
-
- For more sophisticated usage use the Shell class.
- """
-
- self.add_command('shell', Shell(make_context=func))
-
- return func
-
def option(self, *args, **kwargs):
"""
@@ -607,15 +611,24 @@ def decorate(func):
return func
return decorate
- def add_command(self, name, command):
-
+ def shell(self, func):
"""
- Adds command to registry.
+ Decorator that wraps function in shell command. This is equivalent to::
- :param command: Command instance
+ def _make_context(app):
+ return dict(app=app)
+
+ manager.add_command("shell", Shell(make_context=_make_context))
+
+ The decorated function should take a single "app" argument, and return
+ a dict.
+
+ For more sophisticated usage use the Shell class.
"""
- self._commands[name] = command
+ self.add_command('shell', Shell(make_context=func))
+
+ return func
def get_usage(self):
@@ -633,7 +646,12 @@ def get_usage(self):
for name, command in sorted(self._commands.iteritems()):
usage = name
- description = command.description or ''
+
+ if isinstance(command, Manager):
+ description = command.usage or ''
+ else:
+ description = command.description or ''
+
usage = format % (name, description)
rv.append(usage)
@@ -656,27 +674,32 @@ def handle(self, prog, name, args=None):
except KeyError:
raise InvalidCommand, "Command %s not found" % name
- help_args = ('-h', '--help')
+ if isinstance(command, Manager):
+ # Run sub-manager, stripping first argument
+ sys.argv = sys.argv[1:]
+ command.run()
+ else:
+ help_args = ('-h', '--help')
- # remove -h from args if present, and add to remaining args
- app_args = [a for a in args if a not in help_args]
+ # remove -h/--help from args if present, and add to remaining args
+ app_args = [a for a in args if a not in help_args]
- app_parser = self.create_parser(prog)
- app_namespace, remaining_args = app_parser.parse_known_args(app_args)
- app = self.create_app(**app_namespace.__dict__)
+ app_parser = self.create_parser(prog)
+ app_namespace, remaining_args = app_parser.parse_known_args(app_args)
+ app = self.create_app(**app_namespace.__dict__)
- for arg in help_args:
- if arg in args:
- remaining_args.append(arg)
+ for arg in help_args:
+ if arg in args:
+ remaining_args.append(arg)
- command_parser = command.create_parser(prog + " " + name)
- if getattr(command, 'capture_all_args', False):
- command_namespace, unparsed_args = \
- command_parser.parse_known_args(remaining_args)
- positional_args = [unparsed_args]
- else:
- command_namespace = command_parser.parse_args(remaining_args)
- positional_args = []
+ command_parser = command.create_parser(prog + " " + name)
+ if getattr(command, 'capture_all_args', False):
+ command_namespace, unparsed_args = \
+ command_parser.parse_known_args(remaining_args)
+ positional_args = [unparsed_args]
+ else:
+ command_namespace = command_parser.parse_args(remaining_args)
+ positional_args = []
return command.handle(app, *positional_args, **command_namespace.__dict__)
@@ -697,14 +720,13 @@ def run(self, commands=None, default_command=None):
self._commands.update(commands)
try:
-
try:
command = sys.argv[1]
except IndexError:
command = default_command
if command is None:
- raise InvalidCommand, "Please provide a command"
+ raise InvalidCommand, "Please provide a command:"
result = self.handle(sys.argv[0], command, sys.argv[2:])
View
2  setup.py
@@ -29,7 +29,7 @@
setup(
name='Flask-Script',
- version='0.4.0',
+ version='0.5.0',
url='http://github.com/techniq/flask-script',
license='BSD',
author='Dan Jacob',
View
84 tests.py
@@ -352,7 +352,6 @@ def test_run_catch_all(self):
assert "['pos1', 'pos2', '--bar']" in sys.stdout.getvalue()
def test_run_bad_options(self):
-
manager = Manager(self.app)
manager.add_command("simple", CommandWithOptions())
sys.argv = ["manage.py", "simple", "--foo=bar"]
@@ -366,12 +365,10 @@ def test_run_bad_options(self):
sys.stderr = sys_stderr_orig
def test_init_with_flask_instance(self):
-
manager = Manager(self.app)
assert callable(manager.app)
def test_init_with_callable(self):
-
manager = Manager(lambda: app)
assert callable(manager.app)
@@ -396,3 +393,84 @@ def test_run_with_default_command(self):
except SystemExit, e:
assert e.code == 0
assert 'OK' in sys.stdout.getvalue()
+
+
+class TestSubManager(unittest.TestCase):
+
+ TESTING = True
+
+ def setUp(self):
+
+ self.app = Flask(__name__)
+ self.app.config.from_object(self)
+
+ def test_add_submanager(self):
+
+ sub_manager = Manager()
+
+ manager = Manager(self.app)
+ manager.add_command("sub_manager", sub_manager)
+
+ assert isinstance(manager._commands['sub_manager'], Manager)
+ assert sub_manager.parent == manager
+ assert sub_manager.get_options() == manager.get_options()
+
+ def test_run_submanager_command(self):
+
+ sub_manager = Manager()
+ sub_manager.add_command("simple", SimpleCommand())
+
+ manager = Manager(self.app)
+ manager.add_command("sub_manager", sub_manager)
+
+ sys.argv = ["manage.py", "sub_manager", "simple"]
+
+ try:
+ manager.run()
+ except SystemExit, e:
+ assert e.code == 0
+
+ assert 'OK' in sys.stdout.getvalue()
+
+ def test_manager_usage_with_submanager(self):
+
+ sub_manager = Manager(usage="Example sub-manager")
+
+ manager = Manager(self.app)
+ manager.add_command("sub_manager", sub_manager)
+
+ sys.argv = ["manage.py"]
+
+ try:
+ manager.run()
+ except SystemExit, e:
+ assert e.code == 1
+
+ assert 'sub_manager Example sub-manager' in sys.stdout.getvalue()
+
+ def test_submanager_usage(self):
+
+ sub_manager = Manager(usage="Example sub-manager")
+ sub_manager.add_command("simple", SimpleCommand())
+
+ manager = Manager(self.app)
+ manager.add_command("sub_manager", sub_manager)
+
+ sys.argv = ["manage.py", "sub_manager"]
+
+ try:
+ manager.run()
+ except SystemExit, e:
+ assert e.code == 1
+
+ assert "simple simple command" in sys.stdout.getvalue()
+
+ def test_submanager_no_default_commands(self):
+
+ sub_manager = Manager()
+
+ manager = Manager()
+ manager.add_command("sub_manager", sub_manager)
+
+ assert 'runserver' not in sub_manager._commands
+ assert 'shell' not in sub_manager._commands
Please sign in to comment.
Something went wrong with that request. Please try again.