317 lines
11 KiB
Python
317 lines
11 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
r'''
|
||
|
werkzeug.script
|
||
|
~~~~~~~~~~~~~~~
|
||
|
|
||
|
.. admonition:: Deprecated Functionality
|
||
|
|
||
|
``werkzeug.script`` is deprecated without replacement functionality.
|
||
|
Python's command line support improved greatly with :mod:`argparse`
|
||
|
and a bunch of alternative modules.
|
||
|
|
||
|
Most of the time you have recurring tasks while writing an application
|
||
|
such as starting up an interactive python interpreter with some prefilled
|
||
|
imports, starting the development server, initializing the database or
|
||
|
something similar.
|
||
|
|
||
|
For that purpose werkzeug provides the `werkzeug.script` module which
|
||
|
helps you writing such scripts.
|
||
|
|
||
|
|
||
|
Basic Usage
|
||
|
-----------
|
||
|
|
||
|
The following snippet is roughly the same in every werkzeug script::
|
||
|
|
||
|
#!/usr/bin/env python
|
||
|
# -*- coding: utf-8 -*-
|
||
|
from werkzeug import script
|
||
|
|
||
|
# actions go here
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
script.run()
|
||
|
|
||
|
Starting this script now does nothing because no actions are defined.
|
||
|
An action is a function in the same module starting with ``"action_"``
|
||
|
which takes a number of arguments where every argument has a default. The
|
||
|
type of the default value specifies the type of the argument.
|
||
|
|
||
|
Arguments can then be passed by position or using ``--name=value`` from
|
||
|
the shell.
|
||
|
|
||
|
Because a runserver and shell command is pretty common there are two
|
||
|
factory functions that create such commands::
|
||
|
|
||
|
def make_app():
|
||
|
from yourapplication import YourApplication
|
||
|
return YourApplication(...)
|
||
|
|
||
|
action_runserver = script.make_runserver(make_app, use_reloader=True)
|
||
|
action_shell = script.make_shell(lambda: {'app': make_app()})
|
||
|
|
||
|
|
||
|
Using The Scripts
|
||
|
-----------------
|
||
|
|
||
|
The script from above can be used like this from the shell now:
|
||
|
|
||
|
.. sourcecode:: text
|
||
|
|
||
|
$ ./manage.py --help
|
||
|
$ ./manage.py runserver localhost 8080 --debugger --no-reloader
|
||
|
$ ./manage.py runserver -p 4000
|
||
|
$ ./manage.py shell
|
||
|
|
||
|
As you can see it's possible to pass parameters as positional arguments
|
||
|
or as named parameters, pretty much like Python function calls.
|
||
|
|
||
|
|
||
|
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||
|
:license: BSD, see LICENSE for more details.
|
||
|
'''
|
||
|
from __future__ import print_function
|
||
|
|
||
|
import sys
|
||
|
import inspect
|
||
|
import getopt
|
||
|
from os.path import basename
|
||
|
from werkzeug._compat import iteritems
|
||
|
|
||
|
|
||
|
argument_types = {
|
||
|
bool: 'boolean',
|
||
|
str: 'string',
|
||
|
int: 'integer',
|
||
|
float: 'float'
|
||
|
}
|
||
|
|
||
|
|
||
|
converters = {
|
||
|
'boolean': lambda x: x.lower() in ('1', 'true', 'yes', 'on'),
|
||
|
'string': str,
|
||
|
'integer': int,
|
||
|
'float': float
|
||
|
}
|
||
|
|
||
|
|
||
|
def run(namespace=None, action_prefix='action_', args=None):
|
||
|
"""Run the script. Participating actions are looked up in the caller's
|
||
|
namespace if no namespace is given, otherwise in the dict provided.
|
||
|
Only items that start with action_prefix are processed as actions. If
|
||
|
you want to use all items in the namespace provided as actions set
|
||
|
action_prefix to an empty string.
|
||
|
|
||
|
:param namespace: An optional dict where the functions are looked up in.
|
||
|
By default the local namespace of the caller is used.
|
||
|
:param action_prefix: The prefix for the functions. Everything else
|
||
|
is ignored.
|
||
|
:param args: the arguments for the function. If not specified
|
||
|
:data:`sys.argv` without the first argument is used.
|
||
|
"""
|
||
|
if namespace is None:
|
||
|
namespace = sys._getframe(1).f_locals
|
||
|
actions = find_actions(namespace, action_prefix)
|
||
|
|
||
|
if args is None:
|
||
|
args = sys.argv[1:]
|
||
|
if not args or args[0] in ('-h', '--help'):
|
||
|
return print_usage(actions)
|
||
|
elif args[0] not in actions:
|
||
|
fail('Unknown action \'%s\'' % args[0])
|
||
|
|
||
|
arguments = {}
|
||
|
types = {}
|
||
|
key_to_arg = {}
|
||
|
long_options = []
|
||
|
formatstring = ''
|
||
|
func, doc, arg_def = actions[args.pop(0)]
|
||
|
for idx, (arg, shortcut, default, option_type) in enumerate(arg_def):
|
||
|
real_arg = arg.replace('-', '_')
|
||
|
if shortcut:
|
||
|
formatstring += shortcut
|
||
|
if not isinstance(default, bool):
|
||
|
formatstring += ':'
|
||
|
key_to_arg['-' + shortcut] = real_arg
|
||
|
long_options.append(isinstance(default, bool) and arg or arg + '=')
|
||
|
key_to_arg['--' + arg] = real_arg
|
||
|
key_to_arg[idx] = real_arg
|
||
|
types[real_arg] = option_type
|
||
|
arguments[real_arg] = default
|
||
|
|
||
|
try:
|
||
|
optlist, posargs = getopt.gnu_getopt(args, formatstring, long_options)
|
||
|
except getopt.GetoptError as e:
|
||
|
fail(str(e))
|
||
|
|
||
|
specified_arguments = set()
|
||
|
for key, value in enumerate(posargs):
|
||
|
try:
|
||
|
arg = key_to_arg[key]
|
||
|
except IndexError:
|
||
|
fail('Too many parameters')
|
||
|
specified_arguments.add(arg)
|
||
|
try:
|
||
|
arguments[arg] = converters[types[arg]](value)
|
||
|
except ValueError:
|
||
|
fail('Invalid value for argument %s (%s): %s' % (key, arg, value))
|
||
|
|
||
|
for key, value in optlist:
|
||
|
arg = key_to_arg[key]
|
||
|
if arg in specified_arguments:
|
||
|
fail('Argument \'%s\' is specified twice' % arg)
|
||
|
if types[arg] == 'boolean':
|
||
|
if arg.startswith('no_'):
|
||
|
value = 'no'
|
||
|
else:
|
||
|
value = 'yes'
|
||
|
try:
|
||
|
arguments[arg] = converters[types[arg]](value)
|
||
|
except ValueError:
|
||
|
fail('Invalid value for \'%s\': %s' % (key, value))
|
||
|
|
||
|
newargs = {}
|
||
|
for k, v in iteritems(arguments):
|
||
|
newargs[k.startswith('no_') and k[3:] or k] = v
|
||
|
arguments = newargs
|
||
|
return func(**arguments)
|
||
|
|
||
|
|
||
|
def fail(message, code=-1):
|
||
|
"""Fail with an error."""
|
||
|
print('Error: %s' % message, file=sys.stderr)
|
||
|
sys.exit(code)
|
||
|
|
||
|
|
||
|
def find_actions(namespace, action_prefix):
|
||
|
"""Find all the actions in the namespace."""
|
||
|
actions = {}
|
||
|
for key, value in iteritems(namespace):
|
||
|
if key.startswith(action_prefix):
|
||
|
actions[key[len(action_prefix):]] = analyse_action(value)
|
||
|
return actions
|
||
|
|
||
|
|
||
|
def print_usage(actions):
|
||
|
"""Print the usage information. (Help screen)"""
|
||
|
actions = actions.items()
|
||
|
actions.sort()
|
||
|
print('usage: %s <action> [<options>]' % basename(sys.argv[0]))
|
||
|
print(' %s --help' % basename(sys.argv[0]))
|
||
|
print()
|
||
|
print('actions:')
|
||
|
for name, (func, doc, arguments) in actions:
|
||
|
print(' %s:' % name)
|
||
|
for line in doc.splitlines():
|
||
|
print(' %s' % line)
|
||
|
if arguments:
|
||
|
print()
|
||
|
for arg, shortcut, default, argtype in arguments:
|
||
|
if isinstance(default, bool):
|
||
|
print(' %s' % (
|
||
|
(shortcut and '-%s, ' % shortcut or '') + '--' + arg
|
||
|
))
|
||
|
else:
|
||
|
print(' %-30s%-10s%s' % (
|
||
|
(shortcut and '-%s, ' % shortcut or '') + '--' + arg,
|
||
|
argtype, default
|
||
|
))
|
||
|
print()
|
||
|
|
||
|
|
||
|
def analyse_action(func):
|
||
|
"""Analyse a function."""
|
||
|
description = inspect.getdoc(func) or 'undocumented action'
|
||
|
arguments = []
|
||
|
args, varargs, kwargs, defaults = inspect.getargspec(func)
|
||
|
if varargs or kwargs:
|
||
|
raise TypeError('variable length arguments for action not allowed.')
|
||
|
if len(args) != len(defaults or ()):
|
||
|
raise TypeError('not all arguments have proper definitions')
|
||
|
|
||
|
for idx, (arg, definition) in enumerate(zip(args, defaults or ())):
|
||
|
if arg.startswith('_'):
|
||
|
raise TypeError('arguments may not start with an underscore')
|
||
|
if not isinstance(definition, tuple):
|
||
|
shortcut = None
|
||
|
default = definition
|
||
|
else:
|
||
|
shortcut, default = definition
|
||
|
argument_type = argument_types[type(default)]
|
||
|
if isinstance(default, bool) and default is True:
|
||
|
arg = 'no-' + arg
|
||
|
arguments.append((arg.replace('_', '-'), shortcut,
|
||
|
default, argument_type))
|
||
|
return func, description, arguments
|
||
|
|
||
|
|
||
|
def make_shell(init_func=None, banner=None, use_ipython=True):
|
||
|
"""Returns an action callback that spawns a new interactive
|
||
|
python shell.
|
||
|
|
||
|
:param init_func: an optional initialization function that is
|
||
|
called before the shell is started. The return
|
||
|
value of this function is the initial namespace.
|
||
|
:param banner: the banner that is displayed before the shell. If
|
||
|
not specified a generic banner is used instead.
|
||
|
:param use_ipython: if set to `True` ipython is used if available.
|
||
|
"""
|
||
|
if banner is None:
|
||
|
banner = 'Interactive Werkzeug Shell'
|
||
|
if init_func is None:
|
||
|
init_func = dict
|
||
|
def action(ipython=use_ipython):
|
||
|
"""Start a new interactive python session."""
|
||
|
namespace = init_func()
|
||
|
if ipython:
|
||
|
try:
|
||
|
try:
|
||
|
from IPython.frontend.terminal.embed import InteractiveShellEmbed
|
||
|
sh = InteractiveShellEmbed(banner1=banner)
|
||
|
except ImportError:
|
||
|
from IPython.Shell import IPShellEmbed
|
||
|
sh = IPShellEmbed(banner=banner)
|
||
|
except ImportError:
|
||
|
pass
|
||
|
else:
|
||
|
sh(global_ns={}, local_ns=namespace)
|
||
|
return
|
||
|
from code import interact
|
||
|
interact(banner, local=namespace)
|
||
|
return action
|
||
|
|
||
|
|
||
|
def make_runserver(app_factory, hostname='localhost', port=5000,
|
||
|
use_reloader=False, use_debugger=False, use_evalex=True,
|
||
|
threaded=False, processes=1, static_files=None,
|
||
|
extra_files=None, ssl_context=None):
|
||
|
"""Returns an action callback that spawns a new development server.
|
||
|
|
||
|
.. versionadded:: 0.5
|
||
|
`static_files` and `extra_files` was added.
|
||
|
|
||
|
..versionadded:: 0.6.1
|
||
|
`ssl_context` was added.
|
||
|
|
||
|
:param app_factory: a function that returns a new WSGI application.
|
||
|
:param hostname: the default hostname the server should listen on.
|
||
|
:param port: the default port of the server.
|
||
|
:param use_reloader: the default setting for the reloader.
|
||
|
:param use_evalex: the default setting for the evalex flag of the debugger.
|
||
|
:param threaded: the default threading setting.
|
||
|
:param processes: the default number of processes to start.
|
||
|
:param static_files: optional dict of static files.
|
||
|
:param extra_files: optional list of extra files to track for reloading.
|
||
|
:param ssl_context: optional SSL context for running server in HTTPS mode.
|
||
|
"""
|
||
|
def action(hostname=('h', hostname), port=('p', port),
|
||
|
reloader=use_reloader, debugger=use_debugger,
|
||
|
evalex=use_evalex, threaded=threaded, processes=processes):
|
||
|
"""Start a new development server."""
|
||
|
from werkzeug.serving import run_simple
|
||
|
app = app_factory()
|
||
|
run_simple(hostname, port, app, reloader, debugger, evalex,
|
||
|
extra_files, 1, threaded, processes,
|
||
|
static_files=static_files, ssl_context=ssl_context)
|
||
|
return action
|