518 lines
18 KiB
Python
518 lines
18 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
from __future__ import absolute_import
|
||
|
|
||
|
import os
|
||
|
import code
|
||
|
import warnings
|
||
|
import string
|
||
|
import inspect
|
||
|
|
||
|
import argparse
|
||
|
|
||
|
from flask import _request_ctx_stack
|
||
|
|
||
|
from .cli import prompt, prompt_pass, prompt_bool, prompt_choices
|
||
|
from ._compat import izip, text_type
|
||
|
|
||
|
|
||
|
class InvalidCommand(Exception):
|
||
|
"""\
|
||
|
This is a generic error for "bad" commands.
|
||
|
It is not used in Flask-Script itself, but you should throw
|
||
|
this error (or one derived from it) in your command handlers,
|
||
|
and your main code should display this error's message without
|
||
|
a stack trace.
|
||
|
|
||
|
This way, we maintain interoperability if some other plug-in code
|
||
|
supplies Flask-Script hooks.
|
||
|
"""
|
||
|
pass
|
||
|
|
||
|
class Group(object):
|
||
|
"""
|
||
|
Stores argument groups and mutually exclusive groups for
|
||
|
`ArgumentParser.add_argument_group <http://argparse.googlecode.com/svn/trunk/doc/other-methods.html#argument-groups>`
|
||
|
or `ArgumentParser.add_mutually_exclusive_group <http://argparse.googlecode.com/svn/trunk/doc/other-methods.html#add_mutually_exclusive_group>`.
|
||
|
|
||
|
Note: The title and description params cannot be used with the exclusive
|
||
|
or required params.
|
||
|
|
||
|
:param options: A list of Option classes to add to this group
|
||
|
:param title: A string to use as the title of the argument group
|
||
|
:param description: A string to use as the description of the argument
|
||
|
group
|
||
|
:param exclusive: A boolean indicating if this is an argument group or a
|
||
|
mutually exclusive group
|
||
|
:param required: A boolean indicating if this mutually exclusive group
|
||
|
must have an option selected
|
||
|
"""
|
||
|
|
||
|
def __init__(self, *options, **kwargs):
|
||
|
self.option_list = options
|
||
|
|
||
|
self.title = kwargs.pop("title", None)
|
||
|
self.description = kwargs.pop("description", None)
|
||
|
self.exclusive = kwargs.pop("exclusive", None)
|
||
|
self.required = kwargs.pop("required", None)
|
||
|
|
||
|
if ((self.title or self.description) and
|
||
|
(self.required or self.exclusive)):
|
||
|
raise TypeError("title and/or description cannot be used with "
|
||
|
"required and/or exclusive.")
|
||
|
|
||
|
super(Group, self).__init__(**kwargs)
|
||
|
|
||
|
def get_options(self):
|
||
|
"""
|
||
|
By default, returns self.option_list. Override if you
|
||
|
need to do instance-specific configuration.
|
||
|
"""
|
||
|
return self.option_list
|
||
|
|
||
|
|
||
|
class Option(object):
|
||
|
"""
|
||
|
Stores positional and optional arguments for `ArgumentParser.add_argument
|
||
|
<http://argparse.googlecode.com/svn/trunk/doc/add_argument.html>`_.
|
||
|
|
||
|
:param name_or_flags: Either a name or a list of option strings,
|
||
|
e.g. foo or -f, --foo
|
||
|
:param action: The basic type of action to be taken when this argument
|
||
|
is encountered at the command-line.
|
||
|
:param nargs: The number of command-line arguments that should be consumed.
|
||
|
:param const: A constant value required by some action and nargs selections.
|
||
|
:param default: The value produced if the argument is absent from
|
||
|
the command-line.
|
||
|
:param type: The type to which the command-line arg should be converted.
|
||
|
:param choices: A container of the allowable values for the argument.
|
||
|
:param required: Whether or not the command-line option may be omitted
|
||
|
(optionals only).
|
||
|
:param help: A brief description of what the argument does.
|
||
|
:param metavar: A name for the argument in usage messages.
|
||
|
:param dest: The name of the attribute to be added to the object
|
||
|
returned by parse_args().
|
||
|
"""
|
||
|
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
self.args = args
|
||
|
self.kwargs = kwargs
|
||
|
|
||
|
|
||
|
class Command(object):
|
||
|
"""
|
||
|
Base class for creating commands.
|
||
|
|
||
|
:param func: Initialize this command by introspecting the function.
|
||
|
"""
|
||
|
|
||
|
option_list = ()
|
||
|
help_args = None
|
||
|
|
||
|
def __init__(self, func=None):
|
||
|
if func is None:
|
||
|
if not self.option_list:
|
||
|
self.option_list = []
|
||
|
return
|
||
|
|
||
|
args, varargs, keywords, defaults = inspect.getargspec(func)
|
||
|
if inspect.ismethod(func):
|
||
|
args = args[1:]
|
||
|
|
||
|
options = []
|
||
|
|
||
|
# first arg is always "app" : ignore
|
||
|
|
||
|
defaults = defaults or []
|
||
|
kwargs = dict(izip(*[reversed(l) for l in (args, defaults)]))
|
||
|
|
||
|
for arg in args:
|
||
|
|
||
|
if arg in kwargs:
|
||
|
|
||
|
default = kwargs[arg]
|
||
|
|
||
|
if isinstance(default, bool):
|
||
|
options.append(Option('-%s' % arg[0],
|
||
|
'--%s' % arg,
|
||
|
action="store_true",
|
||
|
dest=arg,
|
||
|
required=False,
|
||
|
default=default))
|
||
|
else:
|
||
|
options.append(Option('-%s' % arg[0],
|
||
|
'--%s' % arg,
|
||
|
dest=arg,
|
||
|
type=text_type,
|
||
|
required=False,
|
||
|
default=default))
|
||
|
|
||
|
else:
|
||
|
options.append(Option(arg, type=text_type))
|
||
|
|
||
|
self.run = func
|
||
|
self.__doc__ = func.__doc__
|
||
|
self.option_list = options
|
||
|
|
||
|
@property
|
||
|
def description(self):
|
||
|
description = self.__doc__ or ''
|
||
|
return description.strip()
|
||
|
|
||
|
def add_option(self, option):
|
||
|
"""
|
||
|
Adds Option to option list.
|
||
|
"""
|
||
|
self.option_list.append(option)
|
||
|
|
||
|
def get_options(self):
|
||
|
"""
|
||
|
By default, returns self.option_list. Override if you
|
||
|
need to do instance-specific configuration.
|
||
|
"""
|
||
|
return self.option_list
|
||
|
|
||
|
def create_parser(self, *args, **kwargs):
|
||
|
func_stack = kwargs.pop('func_stack',())
|
||
|
parent = kwargs.pop('parent',None)
|
||
|
parser = argparse.ArgumentParser(*args, add_help=False, **kwargs)
|
||
|
help_args = self.help_args
|
||
|
while help_args is None and parent is not None:
|
||
|
help_args = parent.help_args
|
||
|
parent = getattr(parent,'parent',None)
|
||
|
|
||
|
if help_args:
|
||
|
from flask_script import add_help
|
||
|
add_help(parser,help_args)
|
||
|
|
||
|
for option in self.get_options():
|
||
|
if isinstance(option, Group):
|
||
|
if option.exclusive:
|
||
|
group = parser.add_mutually_exclusive_group(
|
||
|
required=option.required,
|
||
|
)
|
||
|
else:
|
||
|
group = parser.add_argument_group(
|
||
|
title=option.title,
|
||
|
description=option.description,
|
||
|
)
|
||
|
for opt in option.get_options():
|
||
|
group.add_argument(*opt.args, **opt.kwargs)
|
||
|
else:
|
||
|
parser.add_argument(*option.args, **option.kwargs)
|
||
|
|
||
|
parser.set_defaults(func_stack=func_stack+(self,))
|
||
|
|
||
|
self.parser = parser
|
||
|
self.parent = parent
|
||
|
return parser
|
||
|
|
||
|
def __call__(self, app=None, *args, **kwargs):
|
||
|
"""
|
||
|
Handles the command with the given app.
|
||
|
Default behaviour is to call ``self.run`` within a test request context.
|
||
|
"""
|
||
|
with app.test_request_context():
|
||
|
return self.run(*args, **kwargs)
|
||
|
|
||
|
def run(self):
|
||
|
"""
|
||
|
Runs a command. This must be implemented by the subclass. Should take
|
||
|
arguments as configured by the Command options.
|
||
|
"""
|
||
|
raise NotImplementedError
|
||
|
|
||
|
class Shell(Command):
|
||
|
"""
|
||
|
Runs a Python shell inside Flask application context.
|
||
|
|
||
|
:param banner: banner appearing at top of shell when started
|
||
|
:param make_context: a callable returning a dict of variables
|
||
|
used in the shell namespace. By default
|
||
|
returns a dict consisting of just the app.
|
||
|
:param use_bpython: use BPython shell if available, ignore if not.
|
||
|
The BPython shell can be turned off in command
|
||
|
line by passing the **--no-bpython** flag.
|
||
|
:param use_ipython: use IPython shell if available, ignore if not.
|
||
|
The IPython shell can be turned off in command
|
||
|
line by passing the **--no-ipython** flag.
|
||
|
"""
|
||
|
|
||
|
banner = ''
|
||
|
|
||
|
help = description = 'Runs a Python shell inside Flask application context.'
|
||
|
|
||
|
def __init__(self, banner=None, make_context=None, use_ipython=True,
|
||
|
use_bpython=True):
|
||
|
|
||
|
self.banner = banner or self.banner
|
||
|
self.use_ipython = use_ipython
|
||
|
self.use_bpython = use_bpython
|
||
|
|
||
|
if make_context is None:
|
||
|
make_context = lambda: dict(app=_request_ctx_stack.top.app)
|
||
|
|
||
|
self.make_context = make_context
|
||
|
|
||
|
def get_options(self):
|
||
|
return (
|
||
|
Option('--no-ipython',
|
||
|
action="store_true",
|
||
|
dest='no_ipython',
|
||
|
default=not(self.use_ipython),
|
||
|
help="Do not use the BPython shell"),
|
||
|
Option('--no-bpython',
|
||
|
action="store_true",
|
||
|
dest='no_bpython',
|
||
|
default=not(self.use_bpython),
|
||
|
help="Do not use the IPython shell"),
|
||
|
)
|
||
|
|
||
|
def get_context(self):
|
||
|
"""
|
||
|
Returns a dict of context variables added to the shell namespace.
|
||
|
"""
|
||
|
return self.make_context()
|
||
|
|
||
|
def run(self, no_ipython, no_bpython):
|
||
|
"""
|
||
|
Runs the shell. If no_bpython is False or use_bpython is True, then
|
||
|
a BPython shell is run (if installed). Else, if no_ipython is False or
|
||
|
use_python is True then a IPython shell is run (if installed).
|
||
|
"""
|
||
|
|
||
|
context = self.get_context()
|
||
|
|
||
|
if not no_bpython:
|
||
|
# Try BPython
|
||
|
try:
|
||
|
from bpython import embed
|
||
|
embed(banner=self.banner, locals_=context)
|
||
|
return
|
||
|
except ImportError:
|
||
|
pass
|
||
|
|
||
|
if not no_ipython:
|
||
|
# Try IPython
|
||
|
try:
|
||
|
try:
|
||
|
# 0.10.x
|
||
|
from IPython.Shell import IPShellEmbed
|
||
|
ipshell = IPShellEmbed(banner=self.banner)
|
||
|
ipshell(global_ns=dict(), local_ns=context)
|
||
|
except ImportError:
|
||
|
# 0.12+
|
||
|
from IPython import embed
|
||
|
embed(banner1=self.banner, user_ns=context)
|
||
|
return
|
||
|
except ImportError:
|
||
|
pass
|
||
|
|
||
|
# Use basic python shell
|
||
|
code.interact(self.banner, local=context)
|
||
|
|
||
|
|
||
|
class Server(Command):
|
||
|
"""
|
||
|
Runs the Flask development server i.e. app.run()
|
||
|
|
||
|
:param host: server host
|
||
|
:param port: server port
|
||
|
:param use_debugger: if False, will no longer use Werkzeug debugger.
|
||
|
This can be overriden in the command line
|
||
|
by passing the **-d** flag.
|
||
|
:param use_reloader: if False, will no longer use auto-reloader.
|
||
|
This can be overriden in the command line by
|
||
|
passing the **-r** flag.
|
||
|
:param threaded: should the process handle each request in a separate
|
||
|
thread?
|
||
|
:param processes: number of processes to spawn
|
||
|
:param passthrough_errors: disable the error catching. This means that the server will die on errors but it can be useful to hook debuggers in (pdb etc.)
|
||
|
:param options: :func:`werkzeug.run_simple` options.
|
||
|
"""
|
||
|
|
||
|
help = description = 'Runs the Flask development server i.e. app.run()'
|
||
|
|
||
|
def __init__(self, host='127.0.0.1', port=5000, use_debugger=True,
|
||
|
use_reloader=True, threaded=False, processes=1,
|
||
|
passthrough_errors=False, **options):
|
||
|
|
||
|
self.port = port
|
||
|
self.host = host
|
||
|
self.use_debugger = use_debugger
|
||
|
self.use_reloader = use_reloader
|
||
|
self.server_options = options
|
||
|
self.threaded = threaded
|
||
|
self.processes = processes
|
||
|
self.passthrough_errors = passthrough_errors
|
||
|
|
||
|
def get_options(self):
|
||
|
|
||
|
options = (
|
||
|
Option('-h', '--host',
|
||
|
dest='host',
|
||
|
default=self.host),
|
||
|
|
||
|
Option('-p', '--port',
|
||
|
dest='port',
|
||
|
type=int,
|
||
|
default=self.port),
|
||
|
|
||
|
Option('--threaded',
|
||
|
dest='threaded',
|
||
|
action='store_true',
|
||
|
default=self.threaded),
|
||
|
|
||
|
Option('--processes',
|
||
|
dest='processes',
|
||
|
type=int,
|
||
|
default=self.processes),
|
||
|
|
||
|
Option('--passthrough-errors',
|
||
|
action='store_true',
|
||
|
dest='passthrough_errors',
|
||
|
default=self.passthrough_errors),
|
||
|
)
|
||
|
|
||
|
if self.use_debugger:
|
||
|
options += (Option('-d', '--debug',
|
||
|
action='store_true',
|
||
|
dest='use_debugger',
|
||
|
help="(no-op for compatibility)"),)
|
||
|
options += (Option('-D', '--no-debug',
|
||
|
action='store_false',
|
||
|
dest='use_debugger',
|
||
|
default=self.use_debugger),)
|
||
|
|
||
|
else:
|
||
|
options += (Option('-d', '--debug',
|
||
|
action='store_true',
|
||
|
dest='use_debugger',
|
||
|
default=self.use_debugger),)
|
||
|
options += (Option('-D', '--no-debug',
|
||
|
action='store_false',
|
||
|
dest='use_debugger',
|
||
|
help="(no-op for compatibility)"),)
|
||
|
|
||
|
if self.use_reloader:
|
||
|
options += (Option('-r', '--reload',
|
||
|
action='store_true',
|
||
|
dest='use_reloader',
|
||
|
help="(no-op for compatibility)"),)
|
||
|
options += (Option('-R', '--no-reload',
|
||
|
action='store_false',
|
||
|
dest='use_reloader',
|
||
|
default=self.use_reloader),)
|
||
|
|
||
|
else:
|
||
|
options += (Option('-r', '--reload',
|
||
|
action='store_true',
|
||
|
dest='use_reloader',
|
||
|
default=self.use_reloader),)
|
||
|
options += (Option('-R', '--no-reload',
|
||
|
action='store_false',
|
||
|
dest='use_reloader',
|
||
|
help="(no-op for compatibility)"),)
|
||
|
|
||
|
return options
|
||
|
|
||
|
def __call__(self, app, host, port, use_debugger, use_reloader,
|
||
|
threaded, processes, passthrough_errors):
|
||
|
# we don't need to run the server in request context
|
||
|
# so just run it directly
|
||
|
|
||
|
app.run(host=host,
|
||
|
port=port,
|
||
|
debug=use_debugger,
|
||
|
use_debugger=use_debugger,
|
||
|
use_reloader=use_reloader,
|
||
|
threaded=threaded,
|
||
|
processes=processes,
|
||
|
passthrough_errors=passthrough_errors,
|
||
|
**self.server_options)
|
||
|
|
||
|
|
||
|
class Clean(Command):
|
||
|
"Remove *.pyc and *.pyo files recursively starting at current directory"
|
||
|
def run(self):
|
||
|
for dirpath, dirnames, filenames in os.walk('.'):
|
||
|
for filename in filenames:
|
||
|
if filename.endswith('.pyc') or filename.endswith('.pyo'):
|
||
|
full_pathname = os.path.join(dirpath, filename)
|
||
|
print('Removing %s' % full_pathname)
|
||
|
os.remove(full_pathname)
|
||
|
|
||
|
|
||
|
class ShowUrls(Command):
|
||
|
"""
|
||
|
Displays all of the url matching routes for the project
|
||
|
"""
|
||
|
def __init__(self, order='rule'):
|
||
|
self.order = order
|
||
|
|
||
|
def get_options(self):
|
||
|
return (
|
||
|
Option('url',
|
||
|
nargs='?',
|
||
|
help='Url to test (ex. /static/image.png)'),
|
||
|
Option('--order',
|
||
|
dest='order',
|
||
|
default=self.order,
|
||
|
help='Property on Rule to order by (default: %s)' % self.order)
|
||
|
)
|
||
|
|
||
|
return options
|
||
|
|
||
|
def run(self, url, order):
|
||
|
from flask import current_app
|
||
|
from werkzeug.exceptions import NotFound, MethodNotAllowed
|
||
|
|
||
|
rows = []
|
||
|
column_length = 0
|
||
|
column_headers = ('Rule', 'Endpoint', 'Arguments')
|
||
|
|
||
|
if url:
|
||
|
try:
|
||
|
rule, arguments = current_app.url_map \
|
||
|
.bind('localhost') \
|
||
|
.match(url, return_rule=True)
|
||
|
rows.append((rule.rule, rule.endpoint, arguments))
|
||
|
column_length = 3
|
||
|
except (NotFound, MethodNotAllowed) as e:
|
||
|
rows.append(("<%s>" % e, None, None))
|
||
|
column_length = 1
|
||
|
else:
|
||
|
rules = sorted(current_app.url_map.iter_rules(), key=lambda rule: getattr(rule, order))
|
||
|
for rule in rules:
|
||
|
rows.append((rule.rule, rule.endpoint, None))
|
||
|
column_length = 2
|
||
|
|
||
|
str_template = ''
|
||
|
table_width = 0
|
||
|
|
||
|
if column_length >= 1:
|
||
|
max_rule_length = max(len(r[0]) for r in rows)
|
||
|
max_rule_length = max_rule_length if max_rule_length > 4 else 4
|
||
|
str_template += '%-' + str(max_rule_length) + 's'
|
||
|
table_width += max_rule_length
|
||
|
|
||
|
if column_length >= 2:
|
||
|
max_endpoint_length = max(len(str(r[1])) for r in rows)
|
||
|
# max_endpoint_length = max(rows, key=len)
|
||
|
max_endpoint_length = max_endpoint_length if max_endpoint_length > 8 else 8
|
||
|
str_template += ' %-' + str(max_endpoint_length) + 's'
|
||
|
table_width += 2 + max_endpoint_length
|
||
|
|
||
|
if column_length >= 3:
|
||
|
max_arguments_length = max(len(str(r[2])) for r in rows)
|
||
|
max_arguments_length = max_arguments_length if max_arguments_length > 9 else 9
|
||
|
str_template += ' %-' + str(max_arguments_length) + 's'
|
||
|
table_width += 2 + max_arguments_length
|
||
|
|
||
|
print(str_template % (column_headers[:column_length]))
|
||
|
print('-' * table_width)
|
||
|
|
||
|
for row in rows:
|
||
|
print(str_template % row[:column_length])
|
||
|
|
||
|
|