# -*- 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 ` or `ArgumentParser.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 `_. :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])