commands.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. # -*- coding: utf-8 -*-
  2. from __future__ import absolute_import,print_function
  3. import os
  4. import sys
  5. import code
  6. import warnings
  7. import string
  8. import inspect
  9. import argparse
  10. from flask import _request_ctx_stack
  11. from .cli import prompt, prompt_pass, prompt_bool, prompt_choices
  12. from ._compat import izip, text_type
  13. class InvalidCommand(Exception):
  14. """\
  15. This is a generic error for "bad" commands.
  16. It is not used in Flask-Script itself, but you should throw
  17. this error (or one derived from it) in your command handlers,
  18. and your main code should display this error's message without
  19. a stack trace.
  20. This way, we maintain interoperability if some other plug-in code
  21. supplies Flask-Script hooks.
  22. """
  23. pass
  24. class Group(object):
  25. """
  26. Stores argument groups and mutually exclusive groups for
  27. `ArgumentParser.add_argument_group <http://argparse.googlecode.com/svn/trunk/doc/other-methods.html#argument-groups>`
  28. or `ArgumentParser.add_mutually_exclusive_group <http://argparse.googlecode.com/svn/trunk/doc/other-methods.html#add_mutually_exclusive_group>`.
  29. Note: The title and description params cannot be used with the exclusive
  30. or required params.
  31. :param options: A list of Option classes to add to this group
  32. :param title: A string to use as the title of the argument group
  33. :param description: A string to use as the description of the argument
  34. group
  35. :param exclusive: A boolean indicating if this is an argument group or a
  36. mutually exclusive group
  37. :param required: A boolean indicating if this mutually exclusive group
  38. must have an option selected
  39. """
  40. def __init__(self, *options, **kwargs):
  41. self.option_list = options
  42. self.title = kwargs.pop("title", None)
  43. self.description = kwargs.pop("description", None)
  44. self.exclusive = kwargs.pop("exclusive", None)
  45. self.required = kwargs.pop("required", None)
  46. if ((self.title or self.description) and
  47. (self.required or self.exclusive)):
  48. raise TypeError("title and/or description cannot be used with "
  49. "required and/or exclusive.")
  50. super(Group, self).__init__(**kwargs)
  51. def get_options(self):
  52. """
  53. By default, returns self.option_list. Override if you
  54. need to do instance-specific configuration.
  55. """
  56. return self.option_list
  57. class Option(object):
  58. """
  59. Stores positional and optional arguments for `ArgumentParser.add_argument
  60. <http://argparse.googlecode.com/svn/trunk/doc/add_argument.html>`_.
  61. :param name_or_flags: Either a name or a list of option strings,
  62. e.g. foo or -f, --foo
  63. :param action: The basic type of action to be taken when this argument
  64. is encountered at the command-line.
  65. :param nargs: The number of command-line arguments that should be consumed.
  66. :param const: A constant value required by some action and nargs selections.
  67. :param default: The value produced if the argument is absent from
  68. the command-line.
  69. :param type: The type to which the command-line arg should be converted.
  70. :param choices: A container of the allowable values for the argument.
  71. :param required: Whether or not the command-line option may be omitted
  72. (optionals only).
  73. :param help: A brief description of what the argument does.
  74. :param metavar: A name for the argument in usage messages.
  75. :param dest: The name of the attribute to be added to the object
  76. returned by parse_args().
  77. """
  78. def __init__(self, *args, **kwargs):
  79. self.args = args
  80. self.kwargs = kwargs
  81. class Command(object):
  82. """
  83. Base class for creating commands.
  84. :param func: Initialize this command by introspecting the function.
  85. """
  86. option_list = ()
  87. help_args = None
  88. def __init__(self, func=None):
  89. if func is None:
  90. if not self.option_list:
  91. self.option_list = []
  92. return
  93. args, varargs, keywords, defaults = inspect.getargspec(func)
  94. if inspect.ismethod(func):
  95. args = args[1:]
  96. options = []
  97. # first arg is always "app" : ignore
  98. defaults = defaults or []
  99. kwargs = dict(izip(*[reversed(l) for l in (args, defaults)]))
  100. for arg in args:
  101. if arg in kwargs:
  102. default = kwargs[arg]
  103. if isinstance(default, bool):
  104. options.append(Option('-%s' % arg[0],
  105. '--%s' % arg,
  106. action="store_true",
  107. dest=arg,
  108. required=False,
  109. default=default))
  110. else:
  111. options.append(Option('-%s' % arg[0],
  112. '--%s' % arg,
  113. dest=arg,
  114. type=text_type,
  115. required=False,
  116. default=default))
  117. else:
  118. options.append(Option(arg, type=text_type))
  119. self.run = func
  120. self.__doc__ = func.__doc__
  121. self.option_list = options
  122. @property
  123. def description(self):
  124. description = self.__doc__ or ''
  125. return description.strip()
  126. def add_option(self, option):
  127. """
  128. Adds Option to option list.
  129. """
  130. self.option_list.append(option)
  131. def get_options(self):
  132. """
  133. By default, returns self.option_list. Override if you
  134. need to do instance-specific configuration.
  135. """
  136. return self.option_list
  137. def create_parser(self, *args, **kwargs):
  138. func_stack = kwargs.pop('func_stack',())
  139. parent = kwargs.pop('parent',None)
  140. parser = argparse.ArgumentParser(*args, add_help=False, **kwargs)
  141. help_args = self.help_args
  142. while help_args is None and parent is not None:
  143. help_args = parent.help_args
  144. parent = getattr(parent,'parent',None)
  145. if help_args:
  146. from flask_script import add_help
  147. add_help(parser,help_args)
  148. for option in self.get_options():
  149. if isinstance(option, Group):
  150. if option.exclusive:
  151. group = parser.add_mutually_exclusive_group(
  152. required=option.required,
  153. )
  154. else:
  155. group = parser.add_argument_group(
  156. title=option.title,
  157. description=option.description,
  158. )
  159. for opt in option.get_options():
  160. group.add_argument(*opt.args, **opt.kwargs)
  161. else:
  162. parser.add_argument(*option.args, **option.kwargs)
  163. parser.set_defaults(func_stack=func_stack+(self,))
  164. self.parser = parser
  165. self.parent = parent
  166. return parser
  167. def __call__(self, app=None, *args, **kwargs):
  168. """
  169. Handles the command with the given app.
  170. Default behaviour is to call ``self.run`` within a test request context.
  171. """
  172. with app.test_request_context():
  173. return self.run(*args, **kwargs)
  174. def run(self):
  175. """
  176. Runs a command. This must be implemented by the subclass. Should take
  177. arguments as configured by the Command options.
  178. """
  179. raise NotImplementedError
  180. class Shell(Command):
  181. """
  182. Runs a Python shell inside Flask application context.
  183. :param banner: banner appearing at top of shell when started
  184. :param make_context: a callable returning a dict of variables
  185. used in the shell namespace. By default
  186. returns a dict consisting of just the app.
  187. :param use_bpython: use BPython shell if available, ignore if not.
  188. The BPython shell can be turned off in command
  189. line by passing the **--no-bpython** flag.
  190. :param use_ipython: use IPython shell if available, ignore if not.
  191. The IPython shell can be turned off in command
  192. line by passing the **--no-ipython** flag.
  193. """
  194. banner = ''
  195. help = description = 'Runs a Python shell inside Flask application context.'
  196. def __init__(self, banner=None, make_context=None, use_ipython=True,
  197. use_bpython=True):
  198. self.banner = banner or self.banner
  199. self.use_ipython = use_ipython
  200. self.use_bpython = use_bpython
  201. if make_context is None:
  202. make_context = lambda: dict(app=_request_ctx_stack.top.app)
  203. self.make_context = make_context
  204. def get_options(self):
  205. return (
  206. Option('--no-ipython',
  207. action="store_true",
  208. dest='no_ipython',
  209. default=not(self.use_ipython),
  210. help="Do not use the BPython shell"),
  211. Option('--no-bpython',
  212. action="store_true",
  213. dest='no_bpython',
  214. default=not(self.use_bpython),
  215. help="Do not use the IPython shell"),
  216. )
  217. def get_context(self):
  218. """
  219. Returns a dict of context variables added to the shell namespace.
  220. """
  221. return self.make_context()
  222. def run(self, no_ipython, no_bpython):
  223. """
  224. Runs the shell. If no_bpython is False or use_bpython is True, then
  225. a BPython shell is run (if installed). Else, if no_ipython is False or
  226. use_python is True then a IPython shell is run (if installed).
  227. """
  228. context = self.get_context()
  229. if not no_bpython:
  230. # Try BPython
  231. try:
  232. from bpython import embed
  233. embed(banner=self.banner, locals_=context)
  234. return
  235. except ImportError:
  236. pass
  237. if not no_ipython:
  238. # Try IPython
  239. try:
  240. try:
  241. # 0.10.x
  242. from IPython.Shell import IPShellEmbed
  243. ipshell = IPShellEmbed(banner=self.banner)
  244. ipshell(global_ns=dict(), local_ns=context)
  245. except ImportError:
  246. # 0.12+
  247. from IPython import embed
  248. embed(banner1=self.banner, user_ns=context)
  249. return
  250. except ImportError:
  251. pass
  252. # Use basic python shell
  253. code.interact(self.banner, local=context)
  254. class Server(Command):
  255. """
  256. Runs the Flask development server i.e. app.run()
  257. :param host: server host
  258. :param port: server port
  259. :param use_debugger: Flag whether to default to using the Werkzeug debugger.
  260. This can be overriden in the command line
  261. by passing the **-d** or **-D** flag.
  262. Defaults to False, for security.
  263. :param use_reloader: Flag whether to use the auto-reloader.
  264. Default to True when debugging.
  265. This can be overriden in the command line by
  266. passing the **-r**/**-R** flag.
  267. :param threaded: should the process handle each request in a separate
  268. thread?
  269. :param processes: number of processes to spawn
  270. :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.)
  271. :param options: :func:`werkzeug.run_simple` options.
  272. """
  273. help = description = 'Runs the Flask development server i.e. app.run()'
  274. def __init__(self, host='127.0.0.1', port=5000, use_debugger=None,
  275. use_reloader=None, threaded=False, processes=1,
  276. passthrough_errors=False, **options):
  277. self.port = port
  278. self.host = host
  279. self.use_debugger = use_debugger
  280. self.use_reloader = use_reloader if use_reloader is not None else use_debugger
  281. self.server_options = options
  282. self.threaded = threaded
  283. self.processes = processes
  284. self.passthrough_errors = passthrough_errors
  285. def get_options(self):
  286. options = (
  287. Option('-h', '--host',
  288. dest='host',
  289. default=self.host),
  290. Option('-p', '--port',
  291. dest='port',
  292. type=int,
  293. default=self.port),
  294. Option('--threaded',
  295. dest='threaded',
  296. action='store_true',
  297. default=self.threaded),
  298. Option('--processes',
  299. dest='processes',
  300. type=int,
  301. default=self.processes),
  302. Option('--passthrough-errors',
  303. action='store_true',
  304. dest='passthrough_errors',
  305. default=self.passthrough_errors),
  306. Option('-d', '--debug',
  307. action='store_true',
  308. dest='use_debugger',
  309. help='enable the Werkzeug debugger (DO NOT use in production code)',
  310. default=self.use_debugger),
  311. Option('-D', '--no-debug',
  312. action='store_false',
  313. dest='use_debugger',
  314. help='disable the Werkzeug debugger',
  315. default=self.use_debugger),
  316. Option('-r', '--reload',
  317. action='store_true',
  318. dest='use_reloader',
  319. help='monitor Python files for changes (not 100% safe for production use)',
  320. default=self.use_reloader),
  321. Option('-R', '--no-reload',
  322. action='store_false',
  323. dest='use_reloader',
  324. help='do not monitor Python files for changes',
  325. default=self.use_reloader),
  326. )
  327. return options
  328. def __call__(self, app, host, port, use_debugger, use_reloader,
  329. threaded, processes, passthrough_errors):
  330. # we don't need to run the server in request context
  331. # so just run it directly
  332. if use_debugger is None:
  333. use_debugger = app.debug
  334. if use_debugger is None:
  335. use_debugger = True
  336. if sys.stderr.isatty():
  337. print("Debugging is on. DANGER: Do not allow random users to connect to this server.", file=sys.stderr)
  338. if use_reloader is None:
  339. use_reloader = app.debug
  340. app.run(host=host,
  341. port=port,
  342. debug=use_debugger,
  343. use_debugger=use_debugger,
  344. use_reloader=use_reloader,
  345. threaded=threaded,
  346. processes=processes,
  347. passthrough_errors=passthrough_errors,
  348. **self.server_options)
  349. class Clean(Command):
  350. "Remove *.pyc and *.pyo files recursively starting at current directory"
  351. def run(self):
  352. for dirpath, dirnames, filenames in os.walk('.'):
  353. for filename in filenames:
  354. if filename.endswith('.pyc') or filename.endswith('.pyo'):
  355. full_pathname = os.path.join(dirpath, filename)
  356. print('Removing %s' % full_pathname)
  357. os.remove(full_pathname)
  358. class ShowUrls(Command):
  359. """
  360. Displays all of the url matching routes for the project
  361. """
  362. def __init__(self, order='rule'):
  363. self.order = order
  364. def get_options(self):
  365. return (
  366. Option('url',
  367. nargs='?',
  368. help='Url to test (ex. /static/image.png)'),
  369. Option('--order',
  370. dest='order',
  371. default=self.order,
  372. help='Property on Rule to order by (default: %s)' % self.order)
  373. )
  374. return options
  375. def run(self, url, order):
  376. from flask import current_app
  377. from werkzeug.exceptions import NotFound, MethodNotAllowed
  378. rows = []
  379. column_length = 0
  380. column_headers = ('Rule', 'Endpoint', 'Arguments')
  381. if url:
  382. try:
  383. rule, arguments = current_app.url_map \
  384. .bind('localhost') \
  385. .match(url, return_rule=True)
  386. rows.append((rule.rule, rule.endpoint, arguments))
  387. column_length = 3
  388. except (NotFound, MethodNotAllowed) as e:
  389. rows.append(("<%s>" % e, None, None))
  390. column_length = 1
  391. else:
  392. rules = sorted(current_app.url_map.iter_rules(), key=lambda rule: getattr(rule, order))
  393. for rule in rules:
  394. rows.append((rule.rule, rule.endpoint, None))
  395. column_length = 2
  396. str_template = ''
  397. table_width = 0
  398. if column_length >= 1:
  399. max_rule_length = max(len(r[0]) for r in rows)
  400. max_rule_length = max_rule_length if max_rule_length > 4 else 4
  401. str_template += '%-' + str(max_rule_length) + 's'
  402. table_width += max_rule_length
  403. if column_length >= 2:
  404. max_endpoint_length = max(len(str(r[1])) for r in rows)
  405. # max_endpoint_length = max(rows, key=len)
  406. max_endpoint_length = max_endpoint_length if max_endpoint_length > 8 else 8
  407. str_template += ' %-' + str(max_endpoint_length) + 's'
  408. table_width += 2 + max_endpoint_length
  409. if column_length >= 3:
  410. max_arguments_length = max(len(str(r[2])) for r in rows)
  411. max_arguments_length = max_arguments_length if max_arguments_length > 9 else 9
  412. str_template += ' %-' + str(max_arguments_length) + 's'
  413. table_width += 2 + max_arguments_length
  414. print(str_template % (column_headers[:column_length]))
  415. print('-' * table_width)
  416. for row in rows:
  417. print(str_template % row[:column_length])