├── examples ├── prod.cfg └── manage.py ├── requirements.txt ├── docs ├── _static │ ├── flask-script.png │ └── index.html ├── _themes │ ├── flask │ │ ├── theme.conf │ │ └── static │ │ │ └── flasky.css_t │ ├── flask_small │ │ ├── theme.conf │ │ ├── layout.html │ │ └── static │ │ │ └── flasky.css_t │ ├── README │ └── flask_theme_support.py ├── make.bat ├── Makefile ├── conf.py └── index.rst ├── MANIFEST.in ├── .gitignore ├── .travis.yml ├── tox.ini ├── README.rst ├── LICENSE ├── setup.py ├── CHANGES ├── flask_script ├── cli.py ├── _compat.py ├── __init__.py └── commands.py └── tests.py /examples/prod.cfg: -------------------------------------------------------------------------------- 1 | DEBUG=False 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask>=0.10 2 | -------------------------------------------------------------------------------- /docs/_static/flask-script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smurfix/flask-script/HEAD/docs/_static/flask-script.png -------------------------------------------------------------------------------- /docs/_themes/flask/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE tests.py 2 | recursive-include docs * 3 | recursive-exclude docs *.pyc 4 | recursive-exclude docs *.pyo 5 | prune docs/_build 6 | prune docs/_themes/.git 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | _build/ 3 | *.cfg 4 | *.coverage 5 | *.db 6 | dist/ 7 | *.egg-info 8 | *.log 9 | *.orig 10 | *.pyc 11 | *.swo 12 | *.swp 13 | *.tox 14 | *.zip 15 | *.sublime-* 16 | .env 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - '2.7' 5 | - '3.4' 6 | - '3.5' 7 | - '3.6' 8 | install: 9 | - pip install tox-travis 10 | script: 11 | - tox 12 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | nosidebar = true 5 | pygments_style = flask_theme_support.FlaskyStyle 6 | 7 | [options] 8 | index_logo = '' 9 | index_logo_height = 120px 10 | github_fork = '' 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = 8 | {py27,py34,py35,py36}-{lowest,release,devel} 9 | 10 | [testenv] 11 | deps = 12 | pytest 13 | 14 | lowest: Flask>=0.10, <0.11 15 | release: Flask 16 | devel: git+https://github.com/mitsuhiko/flask.git 17 | commands = py.test tests.py 18 | -------------------------------------------------------------------------------- /docs/_static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flask-Script documentation has moved... 5 | 6 | 7 | 8 | 9 | Flask-Script documentation is now maintained at Read the Docs. 10 | If your browser does not automatically redirect you, please click here. 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "basic/layout.html" %} 2 | {% block header %} 3 | {{ super() }} 4 | {% if pagename == 'index' %} 5 |
6 | {% endif %} 7 | {% endblock %} 8 | {% block footer %} 9 | {% if pagename == 'index' %} 10 |
11 | {% endif %} 12 | {% endblock %} 13 | {# do not display relbars #} 14 | {% block relbar1 %}{% endblock %} 15 | {% block relbar2 %} 16 | {% if theme_github_fork %} 17 | Fork me on GitHub 19 | {% endif %} 20 | {% endblock %} 21 | {% block sidebar1 %}{% endblock %} 22 | {% block sidebar2 %}{% endblock %} 23 | -------------------------------------------------------------------------------- /docs/_themes/README: -------------------------------------------------------------------------------- 1 | Flask Sphinx Styles 2 | =================== 3 | 4 | This repository contains sphinx styles for Flask and Flask related 5 | projects. To use this style in your Sphinx documentation, follow 6 | this guide: 7 | 8 | 1. put this folder as _themes into your docs folder. Alternatively 9 | you can also use git submodules to check out the contents there. 10 | 2. add this to your conf.py: 11 | 12 | sys.path.append(os.path.abspath('_themes')) 13 | html_theme_path = ['_themes'] 14 | html_theme = 'flask' 15 | 16 | The following themes exist: 17 | 18 | - 'flask' - the standard flask documentation theme for large 19 | projects 20 | - 'flask_small' - small one-page theme. Intended to be used by 21 | very small addon libraries for flask. 22 | 23 | The following options exist for the flask_small theme: 24 | 25 | [options] 26 | index_logo = '' filename of a picture in _static 27 | to be used as replacement for the 28 | h1 in the index.rst file. 29 | index_logo_height = 120px height of the index logo 30 | github_fork = '' repository name on github for the 31 | "fork me" badge 32 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Deprecated 2 | ------------ 3 | 4 | While the maintainers are willing to merge PR's, they are not actively developing features. As of Flask 0.11, Flask includes `a built-in CLI tool`__, and that may fit your needs better. 5 | 6 | __ http://flask.pocoo.org/docs/latest/cli/ 7 | 8 | 9 | Flask-Script 10 | ============== 11 | 12 | .. image:: https://travis-ci.org/smurfix/flask-script.svg?branch=master 13 | :target: https://travis-ci.org/smurfix/flask-script 14 | 15 | .. image:: https://img.shields.io/pypi/v/flask-script.svg 16 | :target: http://flask-script.readthedocs.org/en/latest/ 17 | :alt: Latest Version 18 | 19 | .. image:: https://img.shields.io/pypi/pyversions/flask-script.svg 20 | :target: https://pypi.python.org/pypi/flask-script/ 21 | :alt: Supported Python versions 22 | 23 | .. image:: https://img.shields.io/pypi/l/flask-script.svg 24 | :target: https://github.com/smurfix/flask-script/blob/master/LICENSE 25 | :alt: License 26 | 27 | A set of utilities for use with the Flask framework which provide 28 | decorators, classes and helpers for writing your own script commands. 29 | 30 | Useful for creating command-line scripts, cronjobs etc outside your 31 | web application. 32 | 33 | 34 | Resources 35 | --------- 36 | 37 | - `Documentation `_ 38 | - `Issue Tracker `_ 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 by Dan Jacob. 2 | Copyright (c) 2014 by Matthias Urlichs. 3 | 4 | Some rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the following 15 | disclaimer in the documentation and/or other materials provided 16 | with the distribution. 17 | 18 | * The names of the contributors may not be used to endorse or 19 | promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /examples/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # PYTHON_ARGCOMPLETE_OK 3 | 4 | import pprint 5 | 6 | from flask import Flask, current_app 7 | from flask_script import Manager, prompt_choices, Server 8 | from flask_script.commands import ShowUrls, Clean 9 | 10 | 11 | def create_app(config=None): 12 | app = Flask(__name__) 13 | app.debug = False 14 | print "CONFIG", config 15 | 16 | app.config.from_envvar('APP_CONFIG', silent=True) 17 | 18 | @app.route("/") 19 | def index(): 20 | # deliberate error, test debug working 21 | assert False, "oops" 22 | 23 | return app 24 | 25 | manager = Manager(create_app) 26 | 27 | 28 | @manager.command 29 | def dumpconfig(): 30 | "Dumps config" 31 | pprint.pprint(current_app.config) 32 | 33 | 34 | @manager.command 35 | def output(name): 36 | "print something" 37 | print name 38 | print type(name) 39 | 40 | 41 | @manager.command 42 | def outputplus(name, url=None): 43 | "print name and url" 44 | print name, url 45 | 46 | 47 | @manager.command 48 | def getrolesimple(): 49 | 50 | choices = ("member", "moderator", "admin") 51 | 52 | role = prompt_choices("role", choices=choices, default="member") 53 | print "ROLE:", role 54 | 55 | 56 | @manager.command 57 | def getrole(): 58 | 59 | choices = ( 60 | (1, "member"), 61 | (2, "moderator"), 62 | (3, "admin"), 63 | ) 64 | 65 | role = prompt_choices("role", choices=choices, resolve=int, default=1) 66 | print "ROLE:", role 67 | 68 | 69 | @manager.option('-n', '--name', dest='name', help="your name") 70 | @manager.option('-u', '--url', dest='url', help="your url") 71 | def optional(name, url): 72 | "print name and url" 73 | print name, url 74 | 75 | manager.add_option("-c", "--config", 76 | dest="config", 77 | help="config file", 78 | required=False) 79 | 80 | manager.add_command("runservernoreload", Server(use_reloader=False)) 81 | manager.add_command("urls", ShowUrls()) 82 | manager.add_command("clean", Clean()) 83 | 84 | if __name__ == "__main__": 85 | manager.run() 86 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flask-Script 3 | -------------- 4 | 5 | Flask support for writing external scripts. 6 | 7 | Links 8 | ````` 9 | 10 | * `documentation `_ 11 | 12 | 13 | """ 14 | import sys 15 | from setuptools import setup 16 | 17 | version='2.0.6' 18 | 19 | # Hack to prevent stupid TypeError: 'NoneType' object is not callable error on 20 | # exit of python setup.py test # in multiprocessing/util.py _exit_function when 21 | # running python setup.py test (see 22 | # https://github.com/pypa/virtualenv/pull/259) 23 | try: 24 | import multiprocessing 25 | except ImportError: 26 | pass 27 | 28 | install_requires = ['Flask'] 29 | 30 | setup( 31 | name='Flask-Script', 32 | version=version, 33 | url='http://github.com/smurfix/flask-script', 34 | download_url = 'https://github.com/smurfix/flask-script/tarball/v'+version, 35 | license='BSD', 36 | author='Dan Jacob', 37 | author_email='danjac354@gmail.com', 38 | maintainer='Matthias Urlichs', 39 | maintainer_email='matthias@urlichs.de', 40 | description='Scripting support for Flask', 41 | long_description=__doc__, 42 | packages=[ 43 | 'flask_script' 44 | ], 45 | zip_safe=False, 46 | install_requires=install_requires, 47 | tests_require=[ 48 | 'pytest', 49 | ], 50 | platforms='any', 51 | classifiers=[ 52 | 'Development Status :: 5 - Production/Stable', 53 | 'Environment :: Web Environment', 54 | 'Intended Audience :: Developers', 55 | 'License :: OSI Approved :: BSD License', 56 | 'Operating System :: OS Independent', 57 | 'Programming Language :: Python', 58 | 'Programming Language :: Python :: 2', 59 | 'Programming Language :: Python :: 2.7', 60 | 'Programming Language :: Python :: 3', 61 | 'Programming Language :: Python :: 3.4', 62 | 'Programming Language :: Python :: 3.5', 63 | 'Programming Language :: Python :: 3.6', 64 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 65 | 'Topic :: Software Development :: Libraries :: Python Modules' 66 | ] 67 | ) 68 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Flask-Script Changelog 2 | ====================== 3 | 4 | Here you can see the full list of changes between each Flask-Script release. 5 | 6 | Version 0.6.7 7 | ----------------- 8 | 9 | Released on February 16, 2014 10 | 11 | - Expose app instance in a command commands (manage.app). #83 12 | - Show full help for submanagers if called without arguments. #85 13 | - Fix ShowUrls command conflict. #88 14 | 15 | Version 0.6.6 16 | ----------------- 17 | 18 | Released on December 6, 2013 19 | 20 | - Fix global being passed after command by not expliciting checking for the 'parents' argument. 21 | 22 | Version 0.6.5 23 | ----------------- 24 | 25 | Released on December 5, 2013 26 | 27 | - Change warning from UserWarning to DeprecationWarning so it is ignored by default 28 | 29 | Version 0.6.4 30 | ----------------- 31 | 32 | Released on December 5, 2013 33 | 34 | - Only pass `parents` argument if a command's `create_parser` accepts it. Workaround for #71 35 | 36 | Version 0.6.3 37 | ----------------- 38 | 39 | Released on November 11, 2013 40 | 41 | - Separate usage into usage/help/description 42 | - Allow for command auto detection 43 | 44 | Version 0.6.2 45 | ----------------- 46 | 47 | Released on August 10, 2013 48 | 49 | - FIXED: 0.6.1 fails to embed ipython at all 50 | 51 | Version 0.6.1 52 | ----------------- 53 | 54 | Released on August 9, 2013 55 | 56 | - FIXED: IPython Shell embedding fails after upgrade to IPython 1.0 57 | 58 | Version 0.6.0 59 | ------------- 60 | 61 | Released on August 7, 2013. 62 | 63 | - Drop support for Python 2.5 64 | - Support Python 2.6/2.7 and >= 3.3 using same source code. 65 | Import necessary compatibility code from flask._compat module of current 66 | Flask repo code. 67 | - Use proper argparse subparsers 68 | - Tab completion using `argcomplete` 69 | - Remove question marks from automatically being appended to prompt_bool and prompt_choices 70 | - FIXED: ipython with disabled bpython 71 | - FIXED: debug parameter no longer passed in from flask_script 72 | 73 | Version 0.5.3 74 | ------------- 75 | 76 | Released on January 9, 2013. 77 | 78 | - Fix nasty bug in Clean command that deletes all files 79 | 80 | Version 0.5.2 81 | ------------- 82 | 83 | Released on December 26, 2012. 84 | 85 | - Change from module to package and refactor project structure 86 | - Add BPython shell support 87 | - Add Clean and ShowUrls commands 88 | - Add Group, a simple way to group arguments 89 | 90 | Version 0.5.1 91 | ------------- 92 | 93 | Released on October 2nd, 2012. 94 | 95 | - Fixed an issue where debug settings were being overridden if present in config. 96 | - Expose 'passthrough_errors' on Server to disable error catching (useful to hook debuggers in (ex. pdb)) 97 | 98 | Version 0.5.0 99 | ------------- 100 | 101 | Released on September 22, 2012. 102 | 103 | - Sub-Manager support (see: http://flask-script.readthedocs.org/en/latest/index.html#sub-managers) 104 | -------------------------------------------------------------------------------- /flask_script/cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import getpass 4 | from ._compat import string_types, input 5 | 6 | 7 | def prompt(name, default=None): 8 | """ 9 | Grab user input from command line. 10 | 11 | :param name: prompt text 12 | :param default: default value if no input provided. 13 | """ 14 | 15 | prompt = name + (default and ' [%s]' % default or '') 16 | prompt += name.endswith('?') and ' ' or ': ' 17 | while True: 18 | rv = input(prompt) 19 | if rv: 20 | return rv 21 | if default is not None: 22 | return default 23 | 24 | 25 | def prompt_pass(name, default=None): 26 | """ 27 | Grabs hidden (password) input from command line. 28 | 29 | :param name: prompt text 30 | :param default: default value if no input provided. 31 | """ 32 | 33 | prompt = name + (default and ' [%s]' % default or '') 34 | prompt += name.endswith('?') and ' ' or ': ' 35 | while True: 36 | rv = getpass.getpass(prompt) 37 | if rv: 38 | return rv 39 | if default is not None: 40 | return default 41 | 42 | 43 | def prompt_bool(name, default=False, yes_choices=None, no_choices=None): 44 | """ 45 | Grabs user input from command line and converts to boolean 46 | value. 47 | 48 | :param name: prompt text 49 | :param default: default value if no input provided. 50 | :param yes_choices: default 'y', 'yes', '1', 'on', 'true', 't' 51 | :param no_choices: default 'n', 'no', '0', 'off', 'false', 'f' 52 | """ 53 | 54 | yes_choices = yes_choices or ('y', 'yes', '1', 'on', 'true', 't') 55 | no_choices = no_choices or ('n', 'no', '0', 'off', 'false', 'f') 56 | 57 | while True: 58 | rv = prompt(name, default and yes_choices[0] or no_choices[0]) 59 | if not rv: 60 | return default 61 | if rv.lower() in yes_choices: 62 | return True 63 | elif rv.lower() in no_choices: 64 | return False 65 | 66 | 67 | def prompt_choices(name, choices, default=None, no_choice=('none',)): 68 | """ 69 | Grabs user input from command line from set of provided choices. 70 | 71 | :param name: prompt text 72 | :param choices: list or tuple of available choices. Choices may be 73 | single strings or (key, value) tuples. 74 | :param default: default value if no input provided. 75 | :param no_choice: acceptable list of strings for "null choice" 76 | """ 77 | 78 | _choices = [] 79 | options = [] 80 | 81 | for choice in choices: 82 | if isinstance(choice, string_types): 83 | options.append(choice) 84 | else: 85 | options.append("%s [%s]" % (choice[1], choice[0])) 86 | choice = choice[0] 87 | _choices.append(choice) 88 | 89 | while True: 90 | rv = prompt(name + ' - (%s)' % ', '.join(options), default).lower() 91 | if rv in no_choice: 92 | return None 93 | if rv in _choices or rv == default: 94 | return rv 95 | -------------------------------------------------------------------------------- /flask_script/_compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flask_script._compat 4 | ~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Some py2/py3 compatibility support based on a stripped down 7 | version of six so we don't have to depend on a specific version 8 | of it. 9 | 10 | :copyright: (c) 2013 by Armin Ronacher. 11 | :license: BSD, see LICENSE for more details. 12 | """ 13 | import sys 14 | 15 | PY2 = sys.version_info[0] == 2 16 | PYPY = hasattr(sys, 'pypy_translation_info') 17 | _identity = lambda x: x 18 | 19 | 20 | if not PY2: 21 | unichr = chr 22 | range_type = range 23 | text_type = str 24 | string_types = (str, ) 25 | integer_types = (int, ) 26 | 27 | iterkeys = lambda d: iter(d.keys()) 28 | itervalues = lambda d: iter(d.values()) 29 | iteritems = lambda d: iter(d.items()) 30 | 31 | import pickle 32 | from io import BytesIO, StringIO 33 | NativeStringIO = StringIO 34 | 35 | def reraise(tp, value, tb=None): 36 | if value.__traceback__ is not tb: 37 | raise value.with_traceback(tb) 38 | raise value 39 | 40 | ifilter = filter 41 | imap = map 42 | izip = zip 43 | intern = sys.intern 44 | 45 | implements_iterator = _identity 46 | implements_to_string = _identity 47 | encode_filename = _identity 48 | get_next = lambda x: x.__next__ 49 | 50 | input = input 51 | 52 | else: 53 | unichr = unichr 54 | text_type = unicode 55 | range_type = xrange 56 | string_types = (str, unicode) 57 | integer_types = (int, long) 58 | 59 | iterkeys = lambda d: d.iterkeys() 60 | itervalues = lambda d: d.itervalues() 61 | iteritems = lambda d: d.iteritems() 62 | 63 | import cPickle as pickle 64 | from cStringIO import StringIO as BytesIO, StringIO 65 | NativeStringIO = BytesIO 66 | 67 | exec('def reraise(tp, value, tb=None):\n raise tp, value, tb') 68 | 69 | from itertools import imap, izip, ifilter 70 | intern = intern 71 | 72 | def implements_iterator(cls): 73 | cls.next = cls.__next__ 74 | del cls.__next__ 75 | return cls 76 | 77 | def implements_to_string(cls): 78 | cls.__unicode__ = cls.__str__ 79 | cls.__str__ = lambda x: x.__unicode__().encode('utf-8') 80 | return cls 81 | 82 | get_next = lambda x: x.next 83 | 84 | def encode_filename(filename): 85 | if isinstance(filename, unicode): 86 | return filename.encode('utf-8') 87 | return filename 88 | 89 | input = raw_input 90 | 91 | 92 | def with_metaclass(meta, *bases): 93 | # This requires a bit of explanation: the basic idea is to make a 94 | # dummy metaclass for one level of class instantiation that replaces 95 | # itself with the actual metaclass. Because of internal type checks 96 | # we also need to make sure that we downgrade the custom metaclass 97 | # for one level to something closer to type (that's why __call__ and 98 | # __init__ comes back from type etc.). 99 | # 100 | # This has the advantage over six.with_metaclass in that it does not 101 | # introduce dummy classes into the final MRO. 102 | class metaclass(meta): 103 | __call__ = type.__call__ 104 | __init__ = type.__init__ 105 | def __new__(cls, name, this_bases, d): 106 | if this_bases is None: 107 | return type.__new__(cls, name, (), d) 108 | return meta(name, bases, d) 109 | return metaclass('temporary_class', None, {}) 110 | 111 | 112 | try: 113 | from urllib.parse import quote_from_bytes as url_quote 114 | except ImportError: 115 | from urllib import quote as url_quote 116 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 47 | goto end 48 | ) 49 | 50 | if "%1" == "dirhtml" ( 51 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 52 | echo. 53 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 54 | goto end 55 | ) 56 | 57 | if "%1" == "singlehtml" ( 58 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 59 | echo. 60 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 61 | goto end 62 | ) 63 | 64 | if "%1" == "pickle" ( 65 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 66 | echo. 67 | echo.Build finished; now you can process the pickle files. 68 | goto end 69 | ) 70 | 71 | if "%1" == "json" ( 72 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 73 | echo. 74 | echo.Build finished; now you can process the JSON files. 75 | goto end 76 | ) 77 | 78 | if "%1" == "htmlhelp" ( 79 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 80 | echo. 81 | echo.Build finished; now you can run HTML Help Workshop with the ^ 82 | .hhp project file in %BUILDDIR%/htmlhelp. 83 | goto end 84 | ) 85 | 86 | if "%1" == "qthelp" ( 87 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 88 | echo. 89 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 90 | .qhcp project file in %BUILDDIR%/qthelp, like this: 91 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\flask-unittest.qhcp 92 | echo.To view the help file: 93 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\flask-unittest.ghc 94 | goto end 95 | ) 96 | 97 | if "%1" == "devhelp" ( 98 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 99 | echo. 100 | echo.Build finished. 101 | goto end 102 | ) 103 | 104 | if "%1" == "epub" ( 105 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 106 | echo. 107 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 108 | goto end 109 | ) 110 | 111 | if "%1" == "latex" ( 112 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 113 | echo. 114 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 115 | goto end 116 | ) 117 | 118 | if "%1" == "text" ( 119 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 120 | echo. 121 | echo.Build finished. The text files are in %BUILDDIR%/text. 122 | goto end 123 | ) 124 | 125 | if "%1" == "man" ( 126 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 127 | echo. 128 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 129 | goto end 130 | ) 131 | 132 | if "%1" == "changes" ( 133 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 134 | echo. 135 | echo.The overview file is in %BUILDDIR%/changes. 136 | goto end 137 | ) 138 | 139 | if "%1" == "linkcheck" ( 140 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 141 | echo. 142 | echo.Link check complete; look for any errors in the above output ^ 143 | or in %BUILDDIR%/linkcheck/output.txt. 144 | goto end 145 | ) 146 | 147 | if "%1" == "doctest" ( 148 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 149 | echo. 150 | echo.Testing of doctests in the sources finished, look at the ^ 151 | results in %BUILDDIR%/doctest/output.txt. 152 | goto end 153 | ) 154 | 155 | :end 156 | -------------------------------------------------------------------------------- /docs/_themes/flask_theme_support.py: -------------------------------------------------------------------------------- 1 | # flasky extensions. flasky pygments style based on tango style 2 | from pygments.style import Style 3 | from pygments.token import Keyword, Name, Comment, String, Error, \ 4 | Number, Operator, Generic, Whitespace, Punctuation, Other, Literal 5 | 6 | 7 | class FlaskyStyle(Style): 8 | background_color = "#f8f8f8" 9 | default_style = "" 10 | 11 | styles = { 12 | # No corresponding class for the following: 13 | #Text: "", # class: '' 14 | Whitespace: "underline #f8f8f8", # class: 'w' 15 | Error: "#a40000 border:#ef2929", # class: 'err' 16 | Other: "#000000", # class 'x' 17 | 18 | Comment: "italic #8f5902", # class: 'c' 19 | Comment.Preproc: "noitalic", # class: 'cp' 20 | 21 | Keyword: "bold #004461", # class: 'k' 22 | Keyword.Constant: "bold #004461", # class: 'kc' 23 | Keyword.Declaration: "bold #004461", # class: 'kd' 24 | Keyword.Namespace: "bold #004461", # class: 'kn' 25 | Keyword.Pseudo: "bold #004461", # class: 'kp' 26 | Keyword.Reserved: "bold #004461", # class: 'kr' 27 | Keyword.Type: "bold #004461", # class: 'kt' 28 | 29 | Operator: "#582800", # class: 'o' 30 | Operator.Word: "bold #004461", # class: 'ow' - like keywords 31 | 32 | Punctuation: "bold #000000", # class: 'p' 33 | 34 | # because special names such as Name.Class, Name.Function, etc. 35 | # are not recognized as such later in the parsing, we choose them 36 | # to look the same as ordinary variables. 37 | Name: "#000000", # class: 'n' 38 | Name.Attribute: "#c4a000", # class: 'na' - to be revised 39 | Name.Builtin: "#004461", # class: 'nb' 40 | Name.Builtin.Pseudo: "#3465a4", # class: 'bp' 41 | Name.Class: "#000000", # class: 'nc' - to be revised 42 | Name.Constant: "#000000", # class: 'no' - to be revised 43 | Name.Decorator: "#888", # class: 'nd' - to be revised 44 | Name.Entity: "#ce5c00", # class: 'ni' 45 | Name.Exception: "bold #cc0000", # class: 'ne' 46 | Name.Function: "#000000", # class: 'nf' 47 | Name.Property: "#000000", # class: 'py' 48 | Name.Label: "#f57900", # class: 'nl' 49 | Name.Namespace: "#000000", # class: 'nn' - to be revised 50 | Name.Other: "#000000", # class: 'nx' 51 | Name.Tag: "bold #004461", # class: 'nt' - like a keyword 52 | Name.Variable: "#000000", # class: 'nv' - to be revised 53 | Name.Variable.Class: "#000000", # class: 'vc' - to be revised 54 | Name.Variable.Global: "#000000", # class: 'vg' - to be revised 55 | Name.Variable.Instance: "#000000", # class: 'vi' - to be revised 56 | 57 | Number: "#990000", # class: 'm' 58 | 59 | Literal: "#000000", # class: 'l' 60 | Literal.Date: "#000000", # class: 'ld' 61 | 62 | String: "#4e9a06", # class: 's' 63 | String.Backtick: "#4e9a06", # class: 'sb' 64 | String.Char: "#4e9a06", # class: 'sc' 65 | String.Doc: "italic #8f5902", # class: 'sd' - like a comment 66 | String.Double: "#4e9a06", # class: 's2' 67 | String.Escape: "#4e9a06", # class: 'se' 68 | String.Heredoc: "#4e9a06", # class: 'sh' 69 | String.Interpol: "#4e9a06", # class: 'si' 70 | String.Other: "#4e9a06", # class: 'sx' 71 | String.Regex: "#4e9a06", # class: 'sr' 72 | String.Single: "#4e9a06", # class: 's1' 73 | String.Symbol: "#4e9a06", # class: 'ss' 74 | 75 | Generic: "#000000", # class: 'g' 76 | Generic.Deleted: "#a40000", # class: 'gd' 77 | Generic.Emph: "italic #000000", # class: 'ge' 78 | Generic.Error: "#ef2929", # class: 'gr' 79 | Generic.Heading: "bold #000080", # class: 'gh' 80 | Generic.Inserted: "#00A000", # class: 'gi' 81 | Generic.Output: "#888", # class: 'go' 82 | Generic.Prompt: "#745334", # class: 'gp' 83 | Generic.Strong: "bold #000000", # class: 'gs' 84 | Generic.Subheading: "bold #800080", # class: 'gu' 85 | Generic.Traceback: "bold #a40000", # class: 'gt' 86 | } 87 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/flask-unittest.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/flask-unittest.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/flask-unittest" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/flask-unittest" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- flasky theme based on nature theme. 6 | * 7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Georgia', serif; 18 | font-size: 17px; 19 | color: #000; 20 | background: white; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.documentwrapper { 26 | float: left; 27 | width: 100%; 28 | } 29 | 30 | div.bodywrapper { 31 | margin: 40px auto 0 auto; 32 | width: 700px; 33 | } 34 | 35 | hr { 36 | border: 1px solid #B1B4B6; 37 | } 38 | 39 | div.body { 40 | background-color: #ffffff; 41 | color: #3E4349; 42 | padding: 0 30px 30px 30px; 43 | } 44 | 45 | img.floatingflask { 46 | padding: 0 0 10px 10px; 47 | float: right; 48 | } 49 | 50 | div.footer { 51 | text-align: right; 52 | color: #888; 53 | padding: 10px; 54 | font-size: 14px; 55 | width: 650px; 56 | margin: 0 auto 40px auto; 57 | } 58 | 59 | div.footer a { 60 | color: #888; 61 | text-decoration: underline; 62 | } 63 | 64 | div.related { 65 | line-height: 32px; 66 | color: #888; 67 | } 68 | 69 | div.related ul { 70 | padding: 0 0 0 10px; 71 | } 72 | 73 | div.related a { 74 | color: #444; 75 | } 76 | 77 | /* -- body styles ----------------------------------------------------------- */ 78 | 79 | a { 80 | color: #004B6B; 81 | text-decoration: underline; 82 | } 83 | 84 | a:hover { 85 | color: #6D4100; 86 | text-decoration: underline; 87 | } 88 | 89 | div.body { 90 | padding-bottom: 40px; /* saved for footer */ 91 | } 92 | 93 | div.body h1, 94 | div.body h2, 95 | div.body h3, 96 | div.body h4, 97 | div.body h5, 98 | div.body h6 { 99 | font-family: 'Garamond', 'Georgia', serif; 100 | font-weight: normal; 101 | margin: 30px 0px 10px 0px; 102 | padding: 0; 103 | } 104 | 105 | {% if theme_index_logo %} 106 | div.indexwrapper h1 { 107 | text-indent: -999999px; 108 | background: url({{ theme_index_logo }}) no-repeat center center; 109 | height: {{ theme_index_logo_height }}; 110 | } 111 | {% endif %} 112 | 113 | div.body h2 { font-size: 180%; } 114 | div.body h3 { font-size: 150%; } 115 | div.body h4 { font-size: 130%; } 116 | div.body h5 { font-size: 100%; } 117 | div.body h6 { font-size: 100%; } 118 | 119 | a.headerlink { 120 | color: white; 121 | padding: 0 4px; 122 | text-decoration: none; 123 | } 124 | 125 | a.headerlink:hover { 126 | color: #444; 127 | background: #eaeaea; 128 | } 129 | 130 | div.body p, div.body dd, div.body li { 131 | line-height: 1.4em; 132 | } 133 | 134 | div.admonition { 135 | background: #fafafa; 136 | margin: 20px -30px; 137 | padding: 10px 30px; 138 | border-top: 1px solid #ccc; 139 | border-bottom: 1px solid #ccc; 140 | } 141 | 142 | div.admonition p.admonition-title { 143 | font-family: 'Garamond', 'Georgia', serif; 144 | font-weight: normal; 145 | font-size: 24px; 146 | margin: 0 0 10px 0; 147 | padding: 0; 148 | line-height: 1; 149 | } 150 | 151 | div.admonition p.last { 152 | margin-bottom: 0; 153 | } 154 | 155 | div.highlight{ 156 | background-color: white; 157 | } 158 | 159 | dt:target, .highlight { 160 | background: #FAF3E8; 161 | } 162 | 163 | div.note { 164 | background-color: #eee; 165 | border: 1px solid #ccc; 166 | } 167 | 168 | div.seealso { 169 | background-color: #ffc; 170 | border: 1px solid #ff6; 171 | } 172 | 173 | div.topic { 174 | background-color: #eee; 175 | } 176 | 177 | div.warning { 178 | background-color: #ffe4e4; 179 | border: 1px solid #f66; 180 | } 181 | 182 | p.admonition-title { 183 | display: inline; 184 | } 185 | 186 | p.admonition-title:after { 187 | content: ":"; 188 | } 189 | 190 | pre, tt { 191 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 192 | font-size: 0.85em; 193 | } 194 | 195 | img.screenshot { 196 | } 197 | 198 | tt.descname, tt.descclassname { 199 | font-size: 0.95em; 200 | } 201 | 202 | tt.descname { 203 | padding-right: 0.08em; 204 | } 205 | 206 | img.screenshot { 207 | -moz-box-shadow: 2px 2px 4px #eee; 208 | -webkit-box-shadow: 2px 2px 4px #eee; 209 | box-shadow: 2px 2px 4px #eee; 210 | } 211 | 212 | table.docutils { 213 | border: 1px solid #888; 214 | -moz-box-shadow: 2px 2px 4px #eee; 215 | -webkit-box-shadow: 2px 2px 4px #eee; 216 | box-shadow: 2px 2px 4px #eee; 217 | } 218 | 219 | table.docutils td, table.docutils th { 220 | border: 1px solid #888; 221 | padding: 0.25em 0.7em; 222 | } 223 | 224 | table.field-list, table.footnote { 225 | border: none; 226 | -moz-box-shadow: none; 227 | -webkit-box-shadow: none; 228 | box-shadow: none; 229 | } 230 | 231 | table.footnote { 232 | margin: 15px 0; 233 | width: 100%; 234 | border: 1px solid #eee; 235 | } 236 | 237 | table.field-list th { 238 | padding: 0 0.8em 0 0; 239 | } 240 | 241 | table.field-list td { 242 | padding: 0; 243 | } 244 | 245 | table.footnote td { 246 | padding: 0.5em; 247 | } 248 | 249 | dl { 250 | margin: 0; 251 | padding: 0; 252 | } 253 | 254 | dl dd { 255 | margin-left: 30px; 256 | } 257 | 258 | pre { 259 | padding: 0; 260 | margin: 15px -30px; 261 | padding: 8px; 262 | line-height: 1.3em; 263 | padding: 7px 30px; 264 | background: #eee; 265 | border-radius: 2px; 266 | -moz-border-radius: 2px; 267 | -webkit-border-radius: 2px; 268 | } 269 | 270 | dl pre { 271 | margin-left: -60px; 272 | padding-left: 60px; 273 | } 274 | 275 | tt { 276 | background-color: #ecf0f3; 277 | color: #222; 278 | /* padding: 1px 2px; */ 279 | } 280 | 281 | tt.xref, a tt { 282 | background-color: #FBFBFB; 283 | } 284 | 285 | a:hover tt { 286 | background: #EEE; 287 | } 288 | -------------------------------------------------------------------------------- /docs/_themes/flask/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- flasky theme based on nature theme. 6 | * 7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Georgia', serif; 18 | font-size: 17px; 19 | background-color: #ddd; 20 | color: #000; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.document { 26 | background: #fafafa; 27 | } 28 | 29 | div.documentwrapper { 30 | float: left; 31 | width: 100%; 32 | } 33 | 34 | div.bodywrapper { 35 | margin: 0 0 0 230px; 36 | } 37 | 38 | hr { 39 | border: 1px solid #B1B4B6; 40 | } 41 | 42 | div.body { 43 | background-color: #ffffff; 44 | color: #3E4349; 45 | padding: 0 30px 30px 30px; 46 | min-height: 34em; 47 | } 48 | 49 | img.floatingflask { 50 | padding: 0 0 10px 10px; 51 | float: right; 52 | } 53 | 54 | div.footer { 55 | position: absolute; 56 | right: 0; 57 | margin-top: -70px; 58 | text-align: right; 59 | color: #888; 60 | padding: 10px; 61 | font-size: 14px; 62 | } 63 | 64 | div.footer a { 65 | color: #888; 66 | text-decoration: underline; 67 | } 68 | 69 | div.related { 70 | line-height: 32px; 71 | color: #888; 72 | } 73 | 74 | div.related ul { 75 | padding: 0 0 0 10px; 76 | } 77 | 78 | div.related a { 79 | color: #444; 80 | } 81 | 82 | div.sphinxsidebar { 83 | font-size: 14px; 84 | line-height: 1.5; 85 | } 86 | 87 | div.sphinxsidebarwrapper { 88 | padding: 0 20px; 89 | } 90 | 91 | div.sphinxsidebarwrapper p.logo { 92 | padding: 20px 0 10px 0; 93 | margin: 0; 94 | text-align: center; 95 | } 96 | 97 | div.sphinxsidebar h3, 98 | div.sphinxsidebar h4 { 99 | font-family: 'Garamond', 'Georgia', serif; 100 | color: #222; 101 | font-size: 24px; 102 | font-weight: normal; 103 | margin: 20px 0 5px 0; 104 | padding: 0; 105 | } 106 | 107 | div.sphinxsidebar h4 { 108 | font-size: 20px; 109 | } 110 | 111 | div.sphinxsidebar h3 a { 112 | color: #444; 113 | } 114 | 115 | div.sphinxsidebar p { 116 | color: #555; 117 | margin: 10px 0; 118 | } 119 | 120 | div.sphinxsidebar ul { 121 | margin: 10px 0; 122 | padding: 0; 123 | color: #000; 124 | } 125 | 126 | div.sphinxsidebar a { 127 | color: #444; 128 | text-decoration: none; 129 | } 130 | 131 | div.sphinxsidebar a:hover { 132 | text-decoration: underline; 133 | } 134 | 135 | div.sphinxsidebar input { 136 | border: 1px solid #ccc; 137 | font-family: 'Georgia', serif; 138 | font-size: 1em; 139 | } 140 | 141 | /* -- body styles ----------------------------------------------------------- */ 142 | 143 | a { 144 | color: #004B6B; 145 | text-decoration: underline; 146 | } 147 | 148 | a:hover { 149 | color: #6D4100; 150 | text-decoration: underline; 151 | } 152 | 153 | div.body { 154 | padding-bottom: 40px; /* saved for footer */ 155 | } 156 | 157 | div.body h1, 158 | div.body h2, 159 | div.body h3, 160 | div.body h4, 161 | div.body h5, 162 | div.body h6 { 163 | font-family: 'Garamond', 'Georgia', serif; 164 | font-weight: normal; 165 | margin: 30px 0px 10px 0px; 166 | padding: 0; 167 | } 168 | 169 | div.body h1 { margin-top: 0; padding-top: 20px; font-size: 240%; } 170 | div.body h2 { font-size: 180%; } 171 | div.body h3 { font-size: 150%; } 172 | div.body h4 { font-size: 130%; } 173 | div.body h5 { font-size: 100%; } 174 | div.body h6 { font-size: 100%; } 175 | 176 | a.headerlink { 177 | color: white; 178 | padding: 0 4px; 179 | text-decoration: none; 180 | } 181 | 182 | a.headerlink:hover { 183 | color: #444; 184 | background: #eaeaea; 185 | } 186 | 187 | div.body p, div.body dd, div.body li { 188 | line-height: 1.4em; 189 | } 190 | 191 | div.admonition { 192 | background: #fafafa; 193 | margin: 20px -30px; 194 | padding: 10px 30px; 195 | border-top: 1px solid #ccc; 196 | border-bottom: 1px solid #ccc; 197 | } 198 | 199 | div.admonition p.admonition-title { 200 | font-family: 'Garamond', 'Georgia', serif; 201 | font-weight: normal; 202 | font-size: 24px; 203 | margin: 0 0 10px 0; 204 | padding: 0; 205 | line-height: 1; 206 | } 207 | 208 | div.admonition p.last { 209 | margin-bottom: 0; 210 | } 211 | 212 | div.highlight{ 213 | background-color: white; 214 | } 215 | 216 | dt:target, .highlight { 217 | background: #FAF3E8; 218 | } 219 | 220 | div.note { 221 | background-color: #eee; 222 | border: 1px solid #ccc; 223 | } 224 | 225 | div.seealso { 226 | background-color: #ffc; 227 | border: 1px solid #ff6; 228 | } 229 | 230 | div.topic { 231 | background-color: #eee; 232 | } 233 | 234 | div.warning { 235 | background-color: #ffe4e4; 236 | border: 1px solid #f66; 237 | } 238 | 239 | p.admonition-title { 240 | display: inline; 241 | } 242 | 243 | p.admonition-title:after { 244 | content: ":"; 245 | } 246 | 247 | pre, tt { 248 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 249 | font-size: 0.9em; 250 | } 251 | 252 | img.screenshot { 253 | } 254 | 255 | tt.descname, tt.descclassname { 256 | font-size: 0.95em; 257 | } 258 | 259 | tt.descname { 260 | padding-right: 0.08em; 261 | } 262 | 263 | img.screenshot { 264 | -moz-box-shadow: 2px 2px 4px #eee; 265 | -webkit-box-shadow: 2px 2px 4px #eee; 266 | box-shadow: 2px 2px 4px #eee; 267 | } 268 | 269 | table.docutils { 270 | border: 1px solid #888; 271 | -moz-box-shadow: 2px 2px 4px #eee; 272 | -webkit-box-shadow: 2px 2px 4px #eee; 273 | box-shadow: 2px 2px 4px #eee; 274 | } 275 | 276 | table.docutils td, table.docutils th { 277 | border: 1px solid #888; 278 | padding: 0.25em 0.7em; 279 | } 280 | 281 | table.field-list, table.footnote { 282 | border: none; 283 | -moz-box-shadow: none; 284 | -webkit-box-shadow: none; 285 | box-shadow: none; 286 | } 287 | 288 | table.footnote { 289 | margin: 15px 0; 290 | width: 100%; 291 | border: 1px solid #eee; 292 | } 293 | 294 | table.field-list th { 295 | padding: 0 0.8em 0 0; 296 | } 297 | 298 | table.field-list td { 299 | padding: 0; 300 | } 301 | 302 | table.footnote td { 303 | padding: 0.5em; 304 | } 305 | 306 | dl { 307 | margin: 0; 308 | padding: 0; 309 | } 310 | 311 | dl dd { 312 | margin-left: 30px; 313 | } 314 | 315 | pre { 316 | background: #eee; 317 | padding: 7px 30px; 318 | margin: 15px -30px; 319 | line-height: 1.3em; 320 | } 321 | 322 | dl pre { 323 | margin-left: -60px; 324 | padding-left: 60px; 325 | } 326 | 327 | dl dl pre { 328 | margin-left: -90px; 329 | padding-left: 90px; 330 | } 331 | 332 | tt { 333 | background-color: #ecf0f3; 334 | color: #222; 335 | /* padding: 1px 2px; */ 336 | } 337 | 338 | tt.xref, a tt { 339 | background-color: #FBFBFB; 340 | text-decoration: none!important; 341 | } 342 | 343 | a:hover tt { 344 | background: #EEE; 345 | } 346 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # flask-script documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Jun 23 08:26:41 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('..')) 20 | sys.path.append(os.path.abspath('_themes')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'Flask-Script' 45 | copyright = u'2010, Dan Jacob' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = '0.4' 53 | # The full version, including alpha/beta/rc tags. 54 | release = '0.4.0' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ['_build'] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | #pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | html_theme = 'flask_small' 96 | 97 | html_theme_options = { 98 | 'index_logo': 'flask-script.png', 99 | 'github_fork': 'smurfix/flask-script' 100 | } 101 | # Theme options are theme-specific and customize the look and feel of a theme 102 | # further. For a list of options available for each theme, see the 103 | # documentation. 104 | #html_theme_options = {} 105 | 106 | # Add any paths that contain custom themes here, relative to this directory. 107 | html_theme_path = ['_themes'] 108 | 109 | # The name for this set of Sphinx documents. If None, it defaults to 110 | # " v documentation". 111 | #html_title = None 112 | 113 | # A shorter title for the navigation bar. Default is the same as html_title. 114 | #html_short_title = None 115 | 116 | # The name of an image file (relative to this directory) to place at the top 117 | # of the sidebar. 118 | #html_logo = None 119 | 120 | # The name of an image file (within the static path) to use as favicon of the 121 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 122 | # pixels large. 123 | #html_favicon = None 124 | 125 | # Add any paths that contain custom static files (such as style sheets) here, 126 | # relative to this directory. They are copied after the builtin static files, 127 | # so a file named "default.css" will overwrite the builtin "default.css". 128 | html_static_path = ['_static'] 129 | 130 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 131 | # using the given strftime format. 132 | #html_last_updated_fmt = '%b %d, %Y' 133 | 134 | # If true, SmartyPants will be used to convert quotes and dashes to 135 | # typographically correct entities. 136 | #html_use_smartypants = True 137 | 138 | # Custom sidebar templates, maps document names to template names. 139 | #html_sidebars = {} 140 | 141 | # Additional templates that should be rendered to pages, maps page names to 142 | # template names. 143 | #html_additional_pages = {} 144 | 145 | # If false, no module index is generated. 146 | #html_domain_indices = True 147 | 148 | # If false, no index is generated. 149 | #html_use_index = True 150 | 151 | # If true, the index is split into individual pages for each letter. 152 | #html_split_index = False 153 | 154 | # If true, links to the reST sources are added to the pages. 155 | #html_show_sourcelink = True 156 | 157 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 158 | #html_show_sphinx = True 159 | 160 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 161 | #html_show_copyright = True 162 | 163 | # If true, an OpenSearch description file will be output, and all pages will 164 | # contain a tag referring to it. The value of this option must be the 165 | # base URL from which the finished HTML is served. 166 | #html_use_opensearch = '' 167 | 168 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 169 | #html_file_suffix = '' 170 | 171 | # Output file base name for HTML help builder. 172 | htmlhelp_basename = 'flask-scriptdoc' 173 | 174 | 175 | # -- Options for LaTeX output -------------------------------------------------- 176 | 177 | # The paper size ('letter' or 'a4'). 178 | #latex_paper_size = 'letter' 179 | 180 | # The font size ('10pt', '11pt' or '12pt'). 181 | #latex_font_size = '10pt' 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'flask-script.tex', u'Flask-Script Documentation', 187 | u'Dan Jacob', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Additional stuff for the LaTeX preamble. 205 | #latex_preamble = '' 206 | 207 | # Documents to append as an appendix to all manuals. 208 | #latex_appendices = [] 209 | 210 | # If false, no module index is generated. 211 | #latex_domain_indices = True 212 | 213 | 214 | # -- Options for manual page output -------------------------------------------- 215 | 216 | # One entry per manual page. List of tuples 217 | # (source start file, name, description, authors, manual section). 218 | man_pages = [ 219 | ('index', 'flask-script', u'Flask-Script Documentation', 220 | [u'Dan Jacob'], 1) 221 | ] 222 | -------------------------------------------------------------------------------- /flask_script/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | import os 5 | import re 6 | import sys 7 | import types 8 | import warnings 9 | from gettext import gettext as _ 10 | from collections import OrderedDict 11 | 12 | import argparse 13 | 14 | from flask import Flask 15 | from flask._compat import text_type 16 | 17 | from ._compat import iteritems 18 | from .commands import Group, Option, Command, Server, Shell 19 | from .cli import prompt, prompt_pass, prompt_bool, prompt_choices 20 | 21 | __all__ = ["Command", "Shell", "Server", "Manager", "Group", "Option", 22 | "prompt", "prompt_pass", "prompt_bool", "prompt_choices"] 23 | 24 | safe_actions = (argparse._StoreAction, 25 | argparse._StoreConstAction, 26 | argparse._StoreTrueAction, 27 | argparse._StoreFalseAction, 28 | argparse._AppendAction, 29 | argparse._AppendConstAction, 30 | argparse._CountAction) 31 | 32 | 33 | try: 34 | import argcomplete 35 | ARGCOMPLETE_IMPORTED = True 36 | except ImportError: 37 | ARGCOMPLETE_IMPORTED = False 38 | 39 | def add_help(parser, help_args): 40 | if not help_args: 41 | return 42 | parser.add_argument(*help_args, 43 | action='help', default=argparse.SUPPRESS, help=_('show this help message and exit')) 44 | 45 | class Manager(object): 46 | """ 47 | Controller class for handling a set of commands. 48 | 49 | Typical usage:: 50 | 51 | class Print(Command): 52 | 53 | def run(self): 54 | print "hello" 55 | 56 | app = Flask(__name__) 57 | 58 | manager = Manager(app) 59 | manager.add_command("print", Print()) 60 | 61 | if __name__ == "__main__": 62 | manager.run() 63 | 64 | On command line:: 65 | 66 | python manage.py print 67 | > hello 68 | 69 | :param app: Flask instance, or callable returning a Flask instance. 70 | :param with_default_commands: load commands **runserver** and **shell** 71 | by default. 72 | :param disable_argcomplete: disable automatic loading of argcomplete. 73 | 74 | """ 75 | help_args = ('-?','--help') 76 | 77 | def __init__(self, app=None, with_default_commands=None, usage=None, 78 | help=None, description=None, disable_argcomplete=False): 79 | 80 | self.app = app 81 | 82 | self.subparser_kwargs = dict() 83 | 84 | self._commands = OrderedDict() 85 | self._options = list() 86 | 87 | self.usage = usage 88 | self.help = help if help is not None else usage 89 | self.description = description if description is not None else usage 90 | self.disable_argcomplete = disable_argcomplete 91 | self.with_default_commands = with_default_commands 92 | 93 | self.parent = None 94 | 95 | def add_default_commands(self): 96 | """ 97 | Adds the shell and runserver default commands. To override these, 98 | simply add your own equivalents using add_command or decorators. 99 | """ 100 | 101 | if "shell" not in self._commands: 102 | self.add_command("shell", Shell()) 103 | if "runserver" not in self._commands: 104 | self.add_command("runserver", Server()) 105 | 106 | def add_option(self, *args, **kwargs): 107 | """ 108 | Adds a global option. This is useful if you want to set variables 109 | applying to the application setup, rather than individual commands. 110 | 111 | For this to work, the manager must be initialized with a factory 112 | function rather than a Flask instance. Otherwise any options you set 113 | will be ignored. 114 | 115 | The arguments are then passed to your function, e.g.:: 116 | 117 | def create_my_app(config=None): 118 | app = Flask(__name__) 119 | if config: 120 | app.config.from_pyfile(config) 121 | 122 | return app 123 | 124 | manager = Manager(create_my_app) 125 | manager.add_option("-c", "--config", dest="config", required=False) 126 | @manager.command 127 | def mycommand(app): 128 | app.do_something() 129 | 130 | and are invoked like this:: 131 | 132 | > python manage.py -c dev.cfg mycommand 133 | 134 | Any manager options passed on the command line will not be passed to 135 | the command. 136 | 137 | Arguments for this function are the same as for the Option class. 138 | """ 139 | 140 | self._options.append(Option(*args, **kwargs)) 141 | 142 | def __call__(self, app=None, **kwargs): 143 | """ 144 | This procedure is called with the App instance (if this is a 145 | sub-Manager) and any options. 146 | 147 | If your sub-Manager does not override this, any values for options will get lost. 148 | """ 149 | if app is None: 150 | app = self.app 151 | if app is None: 152 | raise Exception("There is no app here. This is unlikely to work.") 153 | 154 | if isinstance(app, Flask): 155 | if kwargs: 156 | warnings.warn("Options will be ignored.") 157 | return app 158 | 159 | app = app(**kwargs) 160 | self.app = app 161 | return app 162 | 163 | def create_app(self, *args, **kwargs): 164 | warnings.warn("create_app() is deprecated; use __call__().", warnings.DeprecationWarning) 165 | return self(*args,**kwargs) 166 | 167 | def create_parser(self, prog, func_stack=(), parent=None): 168 | """ 169 | Creates an ArgumentParser instance from options returned 170 | by get_options(), and subparser for the given commands. 171 | """ 172 | prog = os.path.basename(prog) 173 | func_stack=func_stack+(self,) 174 | 175 | options_parser = argparse.ArgumentParser(add_help=False) 176 | for option in self.get_options(): 177 | options_parser.add_argument(*option.args, **option.kwargs) 178 | 179 | parser = argparse.ArgumentParser(prog=prog, usage=self.usage, 180 | description=self.description, 181 | parents=[options_parser], 182 | add_help=False) 183 | add_help(parser, self.help_args) 184 | 185 | self._patch_argparser(parser) 186 | 187 | subparsers = parser.add_subparsers(**self.subparser_kwargs) 188 | 189 | for name, command in self._commands.items(): 190 | usage = getattr(command, 'usage', None) 191 | help = getattr(command, 'help', None) 192 | if help is None: help = command.__doc__ 193 | description = getattr(command, 'description', None) 194 | if description is None: description = command.__doc__ 195 | 196 | command_parser = command.create_parser(name, func_stack=func_stack, parent=self) 197 | 198 | subparser = subparsers.add_parser(name, usage=usage, help=help, 199 | description=description, 200 | parents=[command_parser], 201 | add_help=False) 202 | 203 | if isinstance(command, Manager): 204 | self._patch_argparser(subparser) 205 | 206 | ## enable autocomplete only for parent parser when argcomplete is 207 | ## imported and it is NOT disabled in constructor 208 | if parent is None and ARGCOMPLETE_IMPORTED \ 209 | and not self.disable_argcomplete: 210 | argcomplete.autocomplete(parser, always_complete_options=True) 211 | 212 | self.parser = parser 213 | return parser 214 | 215 | # def foo(self, app, *args, **kwargs): 216 | # print(args) 217 | 218 | def _patch_argparser(self, parser): 219 | """ 220 | Patches the parser to print the full help if no arguments are supplied 221 | """ 222 | 223 | def _parse_known_args(self, arg_strings, *args, **kw): 224 | if not arg_strings: 225 | self.print_help() 226 | self.exit(2) 227 | 228 | return self._parse_known_args2(arg_strings, *args, **kw) 229 | 230 | parser._parse_known_args2 = parser._parse_known_args 231 | parser._parse_known_args = types.MethodType(_parse_known_args, parser) 232 | 233 | def get_options(self): 234 | return self._options 235 | 236 | def add_command(self, *args, **kwargs): 237 | """ 238 | Adds command to registry. 239 | 240 | :param command: Command instance 241 | :param name: Name of the command (optional) 242 | :param namespace: Namespace of the command (optional; pass as kwarg) 243 | """ 244 | 245 | if len(args) == 1: 246 | command = args[0] 247 | name = None 248 | 249 | else: 250 | name, command = args 251 | 252 | if name is None: 253 | if hasattr(command, 'name'): 254 | name = command.name 255 | 256 | else: 257 | name = type(command).__name__.lower() 258 | name = re.sub(r'command$', '', name) 259 | 260 | if isinstance(command, Manager): 261 | command.parent = self 262 | 263 | if isinstance(command, type): 264 | command = command() 265 | 266 | namespace = kwargs.get('namespace') 267 | if not namespace: 268 | namespace = getattr(command, 'namespace', None) 269 | 270 | if namespace: 271 | if namespace not in self._commands: 272 | self.add_command(namespace, Manager()) 273 | 274 | self._commands[namespace]._commands[name] = command 275 | 276 | else: 277 | self._commands[name] = command 278 | 279 | def command(self, func): 280 | """ 281 | Decorator to add a command function to the registry. 282 | 283 | :param func: command function.Arguments depend on the 284 | options. 285 | 286 | """ 287 | 288 | command = Command(func) 289 | self.add_command(func.__name__, command) 290 | 291 | return func 292 | 293 | def option(self, *args, **kwargs): 294 | """ 295 | Decorator to add an option to a function. Automatically registers the 296 | function - do not use together with ``@command``. You can add as many 297 | ``@option`` calls as you like, for example:: 298 | 299 | @option('-n', '--name', dest='name') 300 | @option('-u', '--url', dest='url') 301 | def hello(name, url): 302 | print "hello", name, url 303 | 304 | Takes the same arguments as the ``Option`` constructor. 305 | """ 306 | 307 | option = Option(*args, **kwargs) 308 | 309 | def decorate(func): 310 | name = func.__name__ 311 | 312 | if name not in self._commands: 313 | 314 | command = Command() 315 | command.run = func 316 | command.__doc__ = func.__doc__ 317 | command.option_list = [] 318 | 319 | self.add_command(name, command) 320 | 321 | self._commands[name].option_list.append(option) 322 | return func 323 | return decorate 324 | 325 | def shell(self, func): 326 | """ 327 | Decorator that wraps function in shell command. This is equivalent to:: 328 | 329 | def _make_context(app): 330 | return dict(app=app) 331 | 332 | manager.add_command("shell", Shell(make_context=_make_context)) 333 | 334 | The decorated function should take a single "app" argument, and return 335 | a dict. 336 | 337 | For more sophisticated usage use the Shell class. 338 | """ 339 | 340 | self.add_command('shell', Shell(make_context=func)) 341 | 342 | return func 343 | 344 | def set_defaults(self): 345 | if self.with_default_commands is None: 346 | self.with_default_commands = self.parent is None 347 | if self.with_default_commands: 348 | self.add_default_commands() 349 | self.with_default_commands = False 350 | 351 | def handle(self, prog, args=None): 352 | self.set_defaults() 353 | app_parser = self.create_parser(prog) 354 | 355 | args = list(args or []) 356 | app_namespace, remaining_args = app_parser.parse_known_args(args) 357 | 358 | # get the handle function and remove it from parsed options 359 | kwargs = app_namespace.__dict__ 360 | func_stack = kwargs.pop('func_stack', None) 361 | if not func_stack: 362 | app_parser.error('too few arguments') 363 | 364 | last_func = func_stack[-1] 365 | if remaining_args and not getattr(last_func, 'capture_all_args', False): 366 | app_parser.error('too many arguments') 367 | 368 | args = [] 369 | for handle in func_stack: 370 | 371 | # get only safe config options 372 | config_keys = [action.dest for action in handle.parser._actions 373 | if handle is last_func or action.__class__ in safe_actions] 374 | 375 | # pass only safe app config keys 376 | config = dict((k, v) for k, v in iteritems(kwargs) 377 | if k in config_keys) 378 | 379 | # remove application config keys from handle kwargs 380 | kwargs = dict((k, v) for k, v in iteritems(kwargs) 381 | if k not in config_keys) 382 | 383 | if handle is last_func and getattr(last_func, 'capture_all_args', False): 384 | args.append(remaining_args) 385 | try: 386 | res = handle(*args, **config) 387 | except TypeError as err: 388 | err.args = ("{0}: {1}".format(handle,str(err)),) 389 | raise 390 | 391 | args = [res] 392 | 393 | assert not kwargs 394 | return res 395 | 396 | def run(self, commands=None, default_command=None): 397 | """ 398 | Prepares manager to receive command line input. Usually run 399 | inside "if __name__ == "__main__" block in a Python script. 400 | 401 | :param commands: optional dict of commands. Appended to any commands 402 | added using add_command(). 403 | 404 | :param default_command: name of default command to run if no 405 | arguments passed. 406 | """ 407 | 408 | if commands: 409 | self._commands.update(commands) 410 | 411 | # Make sure all of this is Unicode 412 | argv = list(text_type(arg) for arg in sys.argv) 413 | if default_command is not None and len(argv) == 1: 414 | argv.append(default_command) 415 | 416 | try: 417 | result = self.handle(argv[0], argv[1:]) 418 | except SystemExit as e: 419 | result = e.code 420 | 421 | sys.exit(result or 0) 422 | -------------------------------------------------------------------------------- /flask_script/commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import,print_function 3 | 4 | import os 5 | import sys 6 | import code 7 | import warnings 8 | import string 9 | import inspect 10 | 11 | import argparse 12 | 13 | from flask import _request_ctx_stack 14 | 15 | from .cli import prompt, prompt_pass, prompt_bool, prompt_choices 16 | from ._compat import izip, text_type 17 | 18 | 19 | class InvalidCommand(Exception): 20 | """\ 21 | This is a generic error for "bad" commands. 22 | It is not used in Flask-Script itself, but you should throw 23 | this error (or one derived from it) in your command handlers, 24 | and your main code should display this error's message without 25 | a stack trace. 26 | 27 | This way, we maintain interoperability if some other plug-in code 28 | supplies Flask-Script hooks. 29 | """ 30 | pass 31 | 32 | class Group(object): 33 | """ 34 | Stores argument groups and mutually exclusive groups for 35 | `ArgumentParser.add_argument_group ` 36 | or `ArgumentParser.add_mutually_exclusive_group `. 37 | 38 | Note: The title and description params cannot be used with the exclusive 39 | or required params. 40 | 41 | :param options: A list of Option classes to add to this group 42 | :param title: A string to use as the title of the argument group 43 | :param description: A string to use as the description of the argument 44 | group 45 | :param exclusive: A boolean indicating if this is an argument group or a 46 | mutually exclusive group 47 | :param required: A boolean indicating if this mutually exclusive group 48 | must have an option selected 49 | """ 50 | 51 | def __init__(self, *options, **kwargs): 52 | self.option_list = options 53 | 54 | self.title = kwargs.pop("title", None) 55 | self.description = kwargs.pop("description", None) 56 | self.exclusive = kwargs.pop("exclusive", None) 57 | self.required = kwargs.pop("required", None) 58 | 59 | if ((self.title or self.description) and 60 | (self.required or self.exclusive)): 61 | raise TypeError("title and/or description cannot be used with " 62 | "required and/or exclusive.") 63 | 64 | super(Group, self).__init__(**kwargs) 65 | 66 | def get_options(self): 67 | """ 68 | By default, returns self.option_list. Override if you 69 | need to do instance-specific configuration. 70 | """ 71 | return self.option_list 72 | 73 | 74 | class Option(object): 75 | """ 76 | Stores positional and optional arguments for `ArgumentParser.add_argument 77 | `_. 78 | 79 | :param name_or_flags: Either a name or a list of option strings, 80 | e.g. foo or -f, --foo 81 | :param action: The basic type of action to be taken when this argument 82 | is encountered at the command-line. 83 | :param nargs: The number of command-line arguments that should be consumed. 84 | :param const: A constant value required by some action and nargs selections. 85 | :param default: The value produced if the argument is absent from 86 | the command-line. 87 | :param type: The type to which the command-line arg should be converted. 88 | :param choices: A container of the allowable values for the argument. 89 | :param required: Whether or not the command-line option may be omitted 90 | (optionals only). 91 | :param help: A brief description of what the argument does. 92 | :param metavar: A name for the argument in usage messages. 93 | :param dest: The name of the attribute to be added to the object 94 | returned by parse_args(). 95 | """ 96 | 97 | def __init__(self, *args, **kwargs): 98 | self.args = args 99 | self.kwargs = kwargs 100 | 101 | 102 | class Command(object): 103 | """ 104 | Base class for creating commands. 105 | 106 | :param func: Initialize this command by introspecting the function. 107 | """ 108 | 109 | option_list = () 110 | help_args = None 111 | 112 | def __init__(self, func=None): 113 | if func is None: 114 | if not self.option_list: 115 | self.option_list = [] 116 | return 117 | 118 | args, varargs, keywords, defaults = inspect.getargspec(func) 119 | if inspect.ismethod(func): 120 | args = args[1:] 121 | 122 | options = [] 123 | 124 | # first arg is always "app" : ignore 125 | 126 | defaults = defaults or [] 127 | kwargs = dict(izip(*[reversed(l) for l in (args, defaults)])) 128 | 129 | for arg in args: 130 | 131 | if arg in kwargs: 132 | 133 | default = kwargs[arg] 134 | 135 | if isinstance(default, bool): 136 | options.append(Option('-%s' % arg[0], 137 | '--%s' % arg, 138 | action="store_true", 139 | dest=arg, 140 | required=False, 141 | default=default)) 142 | else: 143 | options.append(Option('-%s' % arg[0], 144 | '--%s' % arg, 145 | dest=arg, 146 | type=text_type, 147 | required=False, 148 | default=default)) 149 | 150 | else: 151 | options.append(Option(arg, type=text_type)) 152 | 153 | self.run = func 154 | self.__doc__ = func.__doc__ 155 | self.option_list = options 156 | 157 | @property 158 | def description(self): 159 | description = self.__doc__ or '' 160 | return description.strip() 161 | 162 | def add_option(self, option): 163 | """ 164 | Adds Option to option list. 165 | """ 166 | self.option_list.append(option) 167 | 168 | def get_options(self): 169 | """ 170 | By default, returns self.option_list. Override if you 171 | need to do instance-specific configuration. 172 | """ 173 | return self.option_list 174 | 175 | def create_parser(self, *args, **kwargs): 176 | func_stack = kwargs.pop('func_stack',()) 177 | parent = kwargs.pop('parent',None) 178 | parser = argparse.ArgumentParser(*args, add_help=False, **kwargs) 179 | help_args = self.help_args 180 | while help_args is None and parent is not None: 181 | help_args = parent.help_args 182 | parent = getattr(parent,'parent',None) 183 | 184 | if help_args: 185 | from flask_script import add_help 186 | add_help(parser,help_args) 187 | 188 | for option in self.get_options(): 189 | if isinstance(option, Group): 190 | if option.exclusive: 191 | group = parser.add_mutually_exclusive_group( 192 | required=option.required, 193 | ) 194 | else: 195 | group = parser.add_argument_group( 196 | title=option.title, 197 | description=option.description, 198 | ) 199 | for opt in option.get_options(): 200 | group.add_argument(*opt.args, **opt.kwargs) 201 | else: 202 | parser.add_argument(*option.args, **option.kwargs) 203 | 204 | parser.set_defaults(func_stack=func_stack+(self,)) 205 | 206 | self.parser = parser 207 | self.parent = parent 208 | return parser 209 | 210 | def __call__(self, app=None, *args, **kwargs): 211 | """ 212 | Handles the command with the given app. 213 | Default behaviour is to call ``self.run`` within a test request context. 214 | """ 215 | with app.test_request_context(): 216 | return self.run(*args, **kwargs) 217 | 218 | def run(self): 219 | """ 220 | Runs a command. This must be implemented by the subclass. Should take 221 | arguments as configured by the Command options. 222 | """ 223 | raise NotImplementedError 224 | 225 | class Shell(Command): 226 | """ 227 | Runs a Python shell inside Flask application context. 228 | 229 | :param banner: banner appearing at top of shell when started 230 | :param make_context: a callable returning a dict of variables 231 | used in the shell namespace. By default 232 | returns a dict consisting of just the app. 233 | :param use_ptipython: use PtIPython shell if available, ignore if not. 234 | The PtIPython shell can be turned off in command 235 | line by passing the **--no-ptipython** flag. 236 | :param use_ptpython: use PtPython shell if available, ignore if not. 237 | The PtPython shell can be turned off in command 238 | line by passing the **--no-ptpython** flag. 239 | :param use_bpython: use BPython shell if available, ignore if not. 240 | The BPython shell can be turned off in command 241 | line by passing the **--no-bpython** flag. 242 | :param use_ipython: use IPython shell if available, ignore if not. 243 | The IPython shell can be turned off in command 244 | line by passing the **--no-ipython** flag. 245 | """ 246 | 247 | banner = '' 248 | 249 | help = description = 'Runs a Python shell inside Flask application context.' 250 | 251 | def __init__(self, banner=None, make_context=None, use_ipython=True, 252 | use_bpython=True, use_ptipython=True, use_ptpython=True): 253 | 254 | self.banner = banner or self.banner 255 | self.use_ipython = use_ipython 256 | self.use_bpython = use_bpython 257 | self.use_ptipython = use_ptipython 258 | self.use_ptpython = use_ptpython 259 | 260 | if make_context is None: 261 | make_context = lambda: dict(app=_request_ctx_stack.top.app) 262 | 263 | self.make_context = make_context 264 | 265 | def get_options(self): 266 | return ( 267 | Option('--no-ipython', 268 | action="store_true", 269 | dest='no_ipython', 270 | default=not(self.use_ipython), 271 | help="Do not use the IPython shell"), 272 | Option('--no-bpython', 273 | action="store_true", 274 | dest='no_bpython', 275 | default=not(self.use_bpython), 276 | help="Do not use the BPython shell"), 277 | Option('--no-ptipython', 278 | action="store_true", 279 | dest='no_ptipython', 280 | default=not(self.use_ptipython), 281 | help="Do not use the PtIPython shell"), 282 | Option('--no-ptpython', 283 | action="store_true", 284 | dest='no_ptpython', 285 | default=not(self.use_ptpython), 286 | help="Do not use the PtPython shell"), 287 | ) 288 | 289 | def get_context(self): 290 | """ 291 | Returns a dict of context variables added to the shell namespace. 292 | """ 293 | return self.make_context() 294 | 295 | def run(self, no_ipython, no_bpython, no_ptipython, no_ptpython): 296 | """ 297 | Runs the shell. 298 | If no_ptipython is False or use_ptipython is True, then a PtIPython shell is run (if installed). 299 | If no_ptpython is False or use_ptpython is True, then a PtPython shell is run (if installed). 300 | If no_bpython is False or use_bpython is True, then a BPython shell is run (if installed). 301 | If no_ipython is False or use_python is True then a IPython shell is run (if installed). 302 | """ 303 | 304 | context = self.get_context() 305 | 306 | if not no_ptipython: 307 | # Try PtIPython 308 | try: 309 | from ptpython.ipython import embed 310 | history_filename = os.path.expanduser('~/.ptpython_history') 311 | embed(banner1=self.banner, user_ns=context, history_filename=history_filename) 312 | return 313 | except ImportError: 314 | pass 315 | 316 | if not no_ptpython: 317 | # Try PtPython 318 | try: 319 | from ptpython.repl import embed 320 | history_filename = os.path.expanduser('~/.ptpython_history') 321 | embed(globals=context, history_filename=history_filename) 322 | return 323 | except ImportError: 324 | pass 325 | 326 | if not no_bpython: 327 | # Try BPython 328 | try: 329 | from bpython import embed 330 | embed(banner=self.banner, locals_=context) 331 | return 332 | except ImportError: 333 | pass 334 | 335 | if not no_ipython: 336 | # Try IPython 337 | try: 338 | from IPython import embed 339 | embed(banner1=self.banner, user_ns=context) 340 | return 341 | except ImportError: 342 | pass 343 | 344 | # Use basic python shell 345 | code.interact(self.banner, local=context) 346 | 347 | 348 | class Server(Command): 349 | """ 350 | Runs the Flask development server i.e. app.run() 351 | 352 | :param host: server host 353 | :param port: server port 354 | :param use_debugger: Flag whether to default to using the Werkzeug debugger. 355 | This can be overriden in the command line 356 | by passing the **-d** or **-D** flag. 357 | Defaults to False, for security. 358 | 359 | :param use_reloader: Flag whether to use the auto-reloader. 360 | Default to True when debugging. 361 | This can be overriden in the command line by 362 | passing the **-r**/**-R** flag. 363 | :param threaded: should the process handle each request in a separate 364 | thread? 365 | :param processes: number of processes to spawn 366 | :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.) 367 | :param ssl_crt: path to ssl certificate file 368 | :param ssl_key: path to ssl key file 369 | :param options: :func:`werkzeug.run_simple` options. 370 | """ 371 | 372 | help = description = 'Runs the Flask development server i.e. app.run()' 373 | 374 | def __init__(self, host='127.0.0.1', port=5000, use_debugger=None, 375 | use_reloader=None, threaded=False, processes=1, 376 | passthrough_errors=False, ssl_crt=None, ssl_key=None, **options): 377 | 378 | self.port = port 379 | self.host = host 380 | self.use_debugger = use_debugger 381 | self.use_reloader = use_reloader if use_reloader is not None else use_debugger 382 | self.server_options = options 383 | self.threaded = threaded 384 | self.processes = processes 385 | self.passthrough_errors = passthrough_errors 386 | self.ssl_crt = ssl_crt 387 | self.ssl_key = ssl_key 388 | 389 | def get_options(self): 390 | 391 | options = ( 392 | Option('-h', '--host', 393 | dest='host', 394 | default=self.host), 395 | 396 | Option('-p', '--port', 397 | dest='port', 398 | type=int, 399 | default=self.port), 400 | 401 | Option('--threaded', 402 | dest='threaded', 403 | action='store_true', 404 | default=self.threaded), 405 | 406 | Option('--processes', 407 | dest='processes', 408 | type=int, 409 | default=self.processes), 410 | 411 | Option('--passthrough-errors', 412 | action='store_true', 413 | dest='passthrough_errors', 414 | default=self.passthrough_errors), 415 | 416 | Option('-d', '--debug', 417 | action='store_true', 418 | dest='use_debugger', 419 | help='enable the Werkzeug debugger (DO NOT use in production code)', 420 | default=self.use_debugger), 421 | Option('-D', '--no-debug', 422 | action='store_false', 423 | dest='use_debugger', 424 | help='disable the Werkzeug debugger', 425 | default=self.use_debugger), 426 | 427 | Option('-r', '--reload', 428 | action='store_true', 429 | dest='use_reloader', 430 | help='monitor Python files for changes (not 100%% safe for production use)', 431 | default=self.use_reloader), 432 | Option('-R', '--no-reload', 433 | action='store_false', 434 | dest='use_reloader', 435 | help='do not monitor Python files for changes', 436 | default=self.use_reloader), 437 | Option('--ssl-crt', 438 | dest='ssl_crt', 439 | type=str, 440 | help='Path to ssl certificate', 441 | default=self.ssl_crt), 442 | Option('--ssl-key', 443 | dest='ssl_key', 444 | type=str, 445 | help='Path to ssl key', 446 | default=self.ssl_key), 447 | ) 448 | 449 | return options 450 | 451 | def __call__(self, app, host, port, use_debugger, use_reloader, 452 | threaded, processes, passthrough_errors, ssl_crt, ssl_key): 453 | # we don't need to run the server in request context 454 | # so just run it directly 455 | 456 | if use_debugger is None: 457 | use_debugger = app.debug 458 | if use_debugger is None: 459 | use_debugger = True 460 | if sys.stderr.isatty(): 461 | print("Debugging is on. DANGER: Do not allow random users to connect to this server.", file=sys.stderr) 462 | if use_reloader is None: 463 | use_reloader = use_debugger 464 | 465 | if None in [ssl_crt, ssl_key]: 466 | ssl_context = None 467 | else: 468 | ssl_context = (ssl_crt, ssl_key) 469 | 470 | app.run(host=host, 471 | port=port, 472 | debug=use_debugger, 473 | use_debugger=use_debugger, 474 | use_reloader=use_reloader, 475 | threaded=threaded, 476 | processes=processes, 477 | passthrough_errors=passthrough_errors, 478 | ssl_context=ssl_context, 479 | **self.server_options) 480 | 481 | 482 | class Clean(Command): 483 | "Remove *.pyc and *.pyo files recursively starting at current directory" 484 | def run(self): 485 | for dirpath, dirnames, filenames in os.walk('.'): 486 | for filename in filenames: 487 | if filename.endswith('.pyc') or filename.endswith('.pyo'): 488 | full_pathname = os.path.join(dirpath, filename) 489 | print('Removing %s' % full_pathname) 490 | os.remove(full_pathname) 491 | 492 | 493 | class ShowUrls(Command): 494 | """ 495 | Displays all of the url matching routes for the project 496 | """ 497 | def __init__(self, order='rule'): 498 | self.order = order 499 | 500 | def get_options(self): 501 | return ( 502 | Option('url', 503 | nargs='?', 504 | help='Url to test (ex. /static/image.png)'), 505 | Option('--order', 506 | dest='order', 507 | default=self.order, 508 | help='Property on Rule to order by (default: %s)' % self.order) 509 | ) 510 | 511 | def run(self, url, order): 512 | from flask import current_app 513 | from werkzeug.exceptions import NotFound, MethodNotAllowed 514 | 515 | rows = [] 516 | column_length = 0 517 | column_headers = ('Rule', 'Endpoint', 'Arguments') 518 | 519 | if url: 520 | try: 521 | rule, arguments = current_app.url_map \ 522 | .bind('localhost') \ 523 | .match(url, return_rule=True) 524 | rows.append((rule.rule, rule.endpoint, arguments)) 525 | column_length = 3 526 | except (NotFound, MethodNotAllowed) as e: 527 | rows.append(("<%s>" % e, None, None)) 528 | column_length = 1 529 | else: 530 | rules = sorted(current_app.url_map.iter_rules(), key=lambda rule: getattr(rule, order)) 531 | for rule in rules: 532 | rows.append((rule.rule, rule.endpoint, None)) 533 | column_length = 2 534 | 535 | str_template = '' 536 | table_width = 0 537 | 538 | if column_length >= 1: 539 | max_rule_length = max(len(r[0]) for r in rows) 540 | max_rule_length = max_rule_length if max_rule_length > 4 else 4 541 | str_template += '%-' + str(max_rule_length) + 's' 542 | table_width += max_rule_length 543 | 544 | if column_length >= 2: 545 | max_endpoint_length = max(len(str(r[1])) for r in rows) 546 | # max_endpoint_length = max(rows, key=len) 547 | max_endpoint_length = max_endpoint_length if max_endpoint_length > 8 else 8 548 | str_template += ' %-' + str(max_endpoint_length) + 's' 549 | table_width += 2 + max_endpoint_length 550 | 551 | if column_length >= 3: 552 | max_arguments_length = max(len(str(r[2])) for r in rows) 553 | max_arguments_length = max_arguments_length if max_arguments_length > 9 else 9 554 | str_template += ' %-' + str(max_arguments_length) + 's' 555 | table_width += 2 + max_arguments_length 556 | 557 | print(str_template % (column_headers[:column_length])) 558 | print('-' * table_width) 559 | 560 | for row in rows: 561 | print(str_template % row[:column_length]) 562 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Flask-Script 2 | ====================================== 3 | 4 | .. module:: Flask-Script 5 | 6 | 7 | .. warning:: 8 | 9 | While the maintainers are willing to merge PR's, they are not actively developing features. As of Flask 0.11, Flask includes `a built-in CLI tool`__, and that may fit your needs better. 10 | :+1: 2 11 | 12 | 13 | __ http://flask.pocoo.org/docs/latest/cli/ 14 | 15 | 16 | The **Flask-Script** extension provides support for writing external scripts in Flask. This includes running a development server, a customised Python shell, scripts to set up your database, cronjobs, and other command-line tasks that belong outside the web application itself. 17 | 18 | **Flask-Script** works in a similar way to Flask itself. You define and add commands that can be called from the command line to a ``Manager`` instance:: 19 | 20 | # manage.py 21 | 22 | from flask_script import Manager 23 | 24 | from myapp import app 25 | 26 | manager = Manager(app) 27 | 28 | @manager.command 29 | def hello(): 30 | print "hello" 31 | 32 | if __name__ == "__main__": 33 | manager.run() 34 | 35 | Once you define your script commands, you can then run them on the command line:: 36 | 37 | python manage.py hello 38 | > hello 39 | 40 | Source code and issue tracking at `GitHub`_. 41 | 42 | Installing Flask-Script 43 | ------------------------ 44 | 45 | Install with **pip** and **easy_install**:: 46 | 47 | pip install Flask-Script 48 | 49 | or download the latest version from version control:: 50 | 51 | git clone https://github.com/smurfix/flask-script.git 52 | cd flask-script 53 | python setup.py develop 54 | 55 | If you are using **virtualenv**, it is assumed that you are installing **Flask-Script** 56 | in the same virtualenv as your Flask application(s). 57 | 58 | Creating and running commands 59 | ----------------------------- 60 | 61 | The first step is to create a Python module to run your script commands in. You can call it 62 | anything you like, for our examples we'll call it ``manage.py``. 63 | 64 | You don't have to place all your commands in the same file; for example, in a larger project 65 | with lots of commands you might want to split them into a number of files with related commands. 66 | 67 | In your ``manage.py`` file you have to create a ``Manager`` instance. The ``Manager`` class 68 | keeps track of all the commands and handles how they are called from the command line:: 69 | 70 | from flask_script import Manager 71 | 72 | app = Flask(__name__) 73 | # configure your app 74 | 75 | manager = Manager(app) 76 | 77 | if __name__ == "__main__": 78 | manager.run() 79 | 80 | Calling ``manager.run()`` prepares your ``Manager`` instance to receive input from the command line. 81 | 82 | The ``Manager`` requires a single argument, a **Flask** instance. This may also be a function or other callable 83 | that returns a **Flask** instance instead, if you want to use a factory pattern. 84 | 85 | The next step is to create and add your commands. There are three methods for creating commands: 86 | 87 | * subclassing the ``Command`` class 88 | * using the ``@command`` decorator 89 | * using the ``@option`` decorator 90 | 91 | To take a very simple example, we want to create a ``hello`` command that just prints out "hello world". It 92 | doesn't take any arguments so is very straightforward:: 93 | 94 | from flask_script import Command 95 | 96 | class Hello(Command): 97 | "prints hello world" 98 | 99 | def run(self): 100 | print "hello world" 101 | 102 | Now the command needs to be added to our ``Manager`` instance, like the one created above:: 103 | 104 | manager.add_command('hello', Hello()) 105 | 106 | This of course needs to be called before ``manager.run``. Now in our command line:: 107 | 108 | python manage.py hello 109 | > hello world 110 | 111 | You can also pass the ``Command`` instance in a dict to ``manager.run()``:: 112 | 113 | manager.run({'hello' : Hello()}) 114 | 115 | The ``Command`` class must define a ``run`` method. The positional and optional arguments 116 | depend on the command-line arguments you pass to the ``Command`` (see below). 117 | 118 | To get a list of available commands and their descriptions, just run with no command:: 119 | 120 | python manage.py 121 | 122 | To get help text for a particular command:: 123 | 124 | python manage.py runserver -? 125 | 126 | This will print usage plus the docstring of the ``Command``. 127 | 128 | This first method is probably the most flexible, but it's also the most verbose. For simpler commands you can use 129 | the ``@command`` decorator, which belongs to the ``Manager`` instance:: 130 | 131 | @manager.command 132 | def hello(): 133 | "Just say hello" 134 | print "hello" 135 | 136 | Commands created this way are run in exactly the same way as those created with the ``Command`` class:: 137 | 138 | python manage.py hello 139 | > hello 140 | 141 | As with the ``Command`` class, the docstring you use for the function will appear when you run with the ``-?`` or ``--help`` option:: 142 | 143 | python manage.py -? 144 | > Just say hello 145 | 146 | Finally, the ``@option`` decorator, again belonging to ``Manager`` can be used when you want more sophisticated 147 | control over your commands:: 148 | 149 | @manager.option('-n', '--name', help='Your name') 150 | def hello(name): 151 | print "hello", name 152 | 153 | The ``@option`` decorator is explained in more detail below. 154 | 155 | *New in version 2.0* 156 | 157 | Help was previously available with ``--help`` and ``-h``. This had a couple 158 | of less-than-ideal consequences, among them the inability to use ``-h`` as 159 | a shortcut for ``--host`` or similar options. 160 | 161 | *New in version 2.0.2* 162 | 163 | If you want to restore the original meaning of ``-h``, set your manager's 164 | ``help_args`` attribute to a list of argument strings you want to be 165 | considered helpful:: 166 | 167 | manager = Manager() 168 | manager.help_args = ('-h', '-?', '--help') 169 | 170 | You can override this list in sub-commands and -managers:: 171 | 172 | def talker(host='localhost'): 173 | pass 174 | ccmd = ConnectCmd(talker) 175 | ccmd.help_args = ('-?', '--help') 176 | manager.add_command("connect", ccmd) 177 | manager.run() 178 | 179 | so that ``manager -h`` prints help, while ``manager connect -h fubar.example.com`` 180 | connects to a remote host. 181 | 182 | Adding arguments to commands 183 | ---------------------------- 184 | 185 | Most commands take a number of named or positional arguments that you pass in the command line. 186 | 187 | Taking the above examples, rather than just print "hello world" we would like to be able to print some 188 | arbitrary name, like this:: 189 | 190 | python manage.py hello --name=Joe 191 | hello Joe 192 | 193 | or alternatively:: 194 | 195 | python manage.py hello -n Joe 196 | 197 | To facilitate this you use the ``option_list`` attribute of the ``Command`` class:: 198 | 199 | from flask_script import Command, Manager, Option 200 | 201 | class Hello(Command): 202 | 203 | option_list = ( 204 | Option('--name', '-n', dest='name'), 205 | ) 206 | 207 | def run(self, name): 208 | print "hello %s" % name 209 | 210 | Positional and optional arguments are stored as ``Option`` instances - see the :ref:`api` below for details. 211 | 212 | Alternatively, you can define a ``get_options`` method for your ``Command`` class. This is useful if you want to be able 213 | to return options at runtime based on for example per-instance attributes:: 214 | 215 | class Hello(Command): 216 | 217 | def __init__(self, default_name='Joe'): 218 | self.default_name=default_name 219 | 220 | def get_options(self): 221 | return [ 222 | Option('-n', '--name', dest='name', default=self.default_name), 223 | ] 224 | 225 | def run(self, name): 226 | print "hello", name 227 | 228 | If you are using the ``@command`` decorator, it's much easier - the options are extracted automatically from your function arguments. This is an example of a positional argument:: 229 | 230 | @manager.command 231 | def hello(name): 232 | print "hello", name 233 | 234 | You then invoke this on the command line like so:: 235 | 236 | > python manage.py hello Joe 237 | hello Joe 238 | 239 | Or you can do optional arguments:: 240 | 241 | @manager.command 242 | def hello(name="Fred") 243 | print "hello", name 244 | 245 | These can be called like so:: 246 | 247 | > python manage.py hello --name=Joe 248 | hello Joe 249 | 250 | alternatively:: 251 | 252 | > python manage.py hello -n Joe 253 | hello Joe 254 | 255 | The short form ``-n`` is formed from the first letter of the argument, so "name" > "-n". Therefore it's a good idea for your 256 | optional argument variable names to begin with different letters. 257 | 258 | *New in version 2.0* 259 | 260 | Note also that if your optional argument is a boolean, for example:: 261 | 262 | @manager.command 263 | def verify(verified=False): 264 | """ 265 | Checks if verified 266 | """ 267 | print "VERIFIED?", "YES" if verified else "NO" 268 | 269 | You can just call it like this:: 270 | 271 | > python manage.py verify 272 | VERIFIED? NO 273 | 274 | > python manage.py verify -v 275 | VERIFIED? YES 276 | 277 | > python manage.py verify --verified 278 | VERIFIED? YES 279 | 280 | The ``@command`` decorator is fine for simple operations, but often you need the flexibility. For more sophisticated options it's better to use the ``@option`` decorator:: 281 | 282 | @manager.option('-n', '--name', dest='name', default='joe') 283 | def hello(name): 284 | print "hello", name 285 | 286 | You can add as many options as you want:: 287 | 288 | @manager.option('-n', '--name', dest='name', default='joe') 289 | @manager.option('-u', '--url', dest='url', default=None) 290 | def hello(name, url): 291 | if url is None: 292 | print "hello", name 293 | else: 294 | print "hello", name, "from", url 295 | 296 | This can be called like so:: 297 | 298 | > python manage.py hello -n Joe -u reddit.com 299 | hello Joe from reddit.com 300 | 301 | or alternatively:: 302 | 303 | > python manage.py hello --name=Joe --url=reddit.com 304 | hello Joe from reddit.com 305 | 306 | Adding options to the manager 307 | ----------------------------- 308 | 309 | Options can also be passed to the ``Manager`` instance. This is allows you to set up options that are passed to the application rather 310 | than a single command. For example, you might want to have a flag to set the configuration file for your application. Suppose you create 311 | your application with a factory function:: 312 | 313 | def create_app(config=None): 314 | 315 | app = Flask(__name__) 316 | if config is not None: 317 | app.config.from_pyfile(config) 318 | # configure your app... 319 | return app 320 | 321 | You want to be able to define the ``config`` argument on the command line - for example, if you have a command to set up your database, you 322 | most certainly want to use different configuration files for production and development. 323 | 324 | In order to pass that ``config`` argument, use the ``add_option()`` method of your ``Manager`` instance. It takes the same arguments 325 | as ``Option``:: 326 | 327 | manager.add_option('-c', '--config', dest='config', required=False) 328 | 329 | As with any other **Flask-Script** configuration you can call this anywhere in your script module, but it must be called before your ``manager.run()`` call. 330 | 331 | Suppose you have this command:: 332 | 333 | @manager.command 334 | def hello(name): 335 | uppercase = app.config.get('USE_UPPERCASE', False) 336 | if uppercase: 337 | name = name.upper() 338 | print "hello", name 339 | 340 | You can now run the following:: 341 | 342 | > python manage.py -c dev.cfg hello joe 343 | hello JOE 344 | 345 | Assuming the ``USE_UPPERCASE`` setting is **True** in your dev.cfg file. 346 | 347 | Notice also that the "config" option is **not** passed to the command. In 348 | fact, this usage:: 349 | 350 | > python manage.py hello joe -c dev.cfg 351 | 352 | will show an error message because the ``-c`` option does not belong to the 353 | ``hello`` command. 354 | 355 | You can attach same-named options to different levels; this allows you to 356 | add an option to your app setup code without checking whether it conflicts with 357 | a command:: 358 | 359 | @manager.option('-n', '--name', dest='name', default='joe') 360 | @manager.option('-c', '--clue', dest='clue', default='clue') 361 | def hello(name, clue): 362 | uppercase = app.config.get('USE_UPPERCASE', False) 363 | if uppercase: 364 | name = name.upper() 365 | clue = clue.upper() 366 | print "hello {0}, get a {1}!".format(name, clue) 367 | 368 | > python manage.py -c dev.cfg hello -c cookie -n frank 369 | hello FRANK, get a COOKIE! 370 | 371 | Note that the destination variables (command arguments, corresponding to 372 | ``dest`` values) must still be different; this is a limitation of Python's 373 | argument parser. 374 | 375 | In order for manager options to work you must pass a factory function, rather than a Flask instance, to your 376 | ``Manager`` constructor. A simple but complete example is available in `this gist `_. 377 | 378 | *New in version 2.0* 379 | 380 | Before version 2, options and command names could be interspersed freely. 381 | The author decided to discontinue this practice for a number of reasons; 382 | the problem with the most impact was that it was not possible to do:: 383 | 384 | > python manage.py connect -d DEST 385 | > python manage.py import -d DIR 386 | 387 | as these options collided. 388 | 389 | Getting user input 390 | ------------------ 391 | 392 | **Flask-Script** comes with a set of helper functions for grabbing user input from the command line. For example:: 393 | 394 | from flask_script import Manager, prompt_bool 395 | 396 | from myapp import app 397 | from myapp.models import db 398 | 399 | manager = Manager(app) 400 | 401 | @manager.command 402 | def dropdb(): 403 | if prompt_bool( 404 | "Are you sure you want to lose all your data"): 405 | db.drop_all() 406 | 407 | It then runs like this:: 408 | 409 | > python manage.py dropdb 410 | Are you sure you want to lose all your data ? [N] 411 | 412 | See the :ref:`api` below for details on the various prompt functions. 413 | 414 | Default commands 415 | ---------------- 416 | 417 | runserver 418 | +++++++++ 419 | 420 | **Flask-Script** has a couple of ready commands you can add and customise: ``Server`` and ``Shell``. 421 | 422 | The ``Server`` command runs the **Flask** development server.:: 423 | 424 | from flask_script import Server, Manager 425 | from myapp import create_app 426 | 427 | manager = Manager(create_app) 428 | manager.add_command("runserver", Server()) 429 | 430 | if __name__ == "__main__": 431 | manager.run() 432 | 433 | and then run the command:: 434 | 435 | python manage.py runserver 436 | 437 | The ``Server`` command has a number of command-line arguments - run ``python manage.py runserver -?`` for details on these. You can redefine the defaults in the constructor:: 438 | 439 | server = Server(host="0.0.0.0", port=9000) 440 | 441 | Needless to say the development server is not intended for production use. 442 | 443 | *New in version 2.0.5* 444 | 445 | The most common use-case for ``runserver`` is to run a debug server for 446 | investigating problems. Therefore the default, if it is *not* set in the 447 | configuration file, is to enable debugging and auto-reloading. 448 | 449 | Unfortunately, Flask currently (as of May 2014) defaults to set the DEBUG 450 | configuration parameter to ``False``. Until this is changed, you can 451 | safely add ``DEBUG=None`` to your Flask configuration. Flask-Script's 452 | ``runserver`` will then turn on debugging, but everything else will treat 453 | it as being turned off. 454 | 455 | To prevent misunderstandings -- after all, debug mode is a serious security 456 | hole --, a warning is printed when Flask-Script treats a ``None`` default 457 | value as if it were set to ``True``. You can turn on debugging explicitly 458 | to get rid of this warning. 459 | 460 | shell 461 | +++++ 462 | 463 | The ``Shell`` command starts a Python shell. You can pass in a ``make_context`` argument, which must be a ``callable`` returning a ``dict``. By default, this is just a dict returning the your Flask application instance:: 464 | 465 | from flask_script import Shell, Manager 466 | 467 | from myapp import app 468 | from myapp import models 469 | from myapp.models import db 470 | 471 | def _make_context(): 472 | return dict(app=app, db=db, models=models) 473 | 474 | manager = Manager(create_app) 475 | manager.add_command("shell", Shell(make_context=_make_context)) 476 | 477 | This is handy if you want to include a bunch of defaults in your shell to save typing lots of ``import`` statements. 478 | 479 | The ``Shell`` command will use `IPython `_ if it is installed, otherwise it defaults to the standard Python shell. You can disable this behaviour in two ways: by passing the ``use_ipython`` argument to the ``Shell`` constructor, or passing the flag ``--no-ipython`` in the command line:: 480 | 481 | shell = Shell(use_ipython=False) 482 | 483 | There is also a ``shell`` decorator which you can use with a context function:: 484 | 485 | @manager.shell 486 | def make_shell_context(): 487 | return dict(app=app, db=db, models=models) 488 | 489 | This enables a ``shell`` command with the defaults enabled:: 490 | 491 | > python manage.py shell 492 | 493 | The default commands ``shell`` and ``runserver`` are included by default, with the default options for these commands. If you wish to 494 | replace them with different commands simply override with ``add_command()`` or the decorators. If you pass ``with_default_commands=False`` 495 | to the ``Manager`` constructor these commands will not be loaded:: 496 | 497 | manager = Manager(app, with_default_commands=False) 498 | 499 | Sub-Managers 500 | ------------ 501 | A Sub-Manager is an instance of ``Manager`` added as a command to another Manager 502 | 503 | To create a submanager:: 504 | 505 | def sub_opts(app, **kwargs): 506 | pass 507 | sub_manager = Manager(sub_opts) 508 | 509 | manager = Manager(self.app) 510 | manager.add_command("sub_manager", sub_manager) 511 | 512 | If you attach options to the sub_manager, the ``sub_opts`` procedure will 513 | receive their values. Your application is passed in ``app`` for 514 | convenience. 515 | 516 | If ``sub_opts`` returns a value other than ``None``, this value will replace 517 | the ``app`` value that's passed on. This way, you can implement a 518 | sub-manager which replaces the whole app. One use case is to create a 519 | separate administrative application for improved security:: 520 | 521 | def gen_admin(app, **kwargs): 522 | from myweb.admin import MyAdminApp 523 | ## easiest but possibly incomplete way to copy your settings 524 | return MyAdminApp(config=app.config, **kwargs) 525 | sub_manager = Manager(gen_admin) 526 | 527 | manager = Manager(MyApp) 528 | manager.add_command("admin", sub_manager) 529 | 530 | > python manage.py runserver 531 | [ starts your normal server ] 532 | > python manage.py admin runserver 533 | [ starts an administrative server ] 534 | 535 | You can cascade sub-managers, i.e. add one sub-manager to another. 536 | 537 | A sub-manager does not get default commands added to itself (by default) 538 | 539 | *New in version 0.5.0.* 540 | 541 | Note to extension developers 542 | ---------------------------- 543 | 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. 544 | 545 | Here is an example how a database extension could provide (ex. database.py):: 546 | 547 | manager = Manager(usage="Perform database operations") 548 | 549 | @manager.command 550 | def drop(): 551 | "Drops database tables" 552 | if prompt_bool("Are you sure you want to lose all your data"): 553 | db.drop_all() 554 | 555 | 556 | @manager.command 557 | def create(default_data=True, sample_data=False): 558 | "Creates database tables from sqlalchemy models" 559 | db.create_all() 560 | populate(default_data, sample_data) 561 | 562 | 563 | @manager.command 564 | def recreate(default_data=True, sample_data=False): 565 | "Recreates database tables (same as issuing 'drop' and then 'create')" 566 | drop() 567 | create(default_data, sample_data) 568 | 569 | 570 | @manager.command 571 | def populate(default_data=False, sample_data=False): 572 | "Populate database with default data" 573 | from fixtures import dbfixture 574 | 575 | if default_data: 576 | from fixtures.default_data import all 577 | default_data = dbfixture.data(*all) 578 | default_data.setup() 579 | 580 | if sample_data: 581 | from fixtures.sample_data import all 582 | sample_data = dbfixture.data(*all) 583 | sample_data.setup() 584 | 585 | 586 | Then the user can register the sub-manager to their primary Manager (within manage.py):: 587 | 588 | manager = Manager(app) 589 | 590 | from flask_database import manager as database_manager 591 | manager.add_command("database", database_manager) 592 | 593 | The commands will then be available:: 594 | 595 | > python manage.py database 596 | 597 | Please provide a command: 598 | 599 | Perform database operations 600 | create Creates database tables from sqlalchemy models 601 | drop Drops database tables 602 | populate Populate database with default data 603 | recreate Recreates database tables (same as issuing 'drop' and then 'create') 604 | 605 | Error handling 606 | -------------- 607 | 608 | Users do not like to see stack traces, but developers want them for bug reports. 609 | 610 | Therefore, ``flask_script.commands`` provides an `InvalidCommand` error 611 | class which is not supposed to print a stack trace when reported. 612 | 613 | In your command handler:: 614 | 615 | from flask_script.commands import InvalidCommand 616 | 617 | [… if some command verification fails …] 618 | class MyCommand(Command): 619 | def run(self, foo=None, bar=None): 620 | if foo and bar: 621 | raise InvalidCommand("Options foo and bar are incompatible") 622 | 623 | In your main loop:: 624 | 625 | try: 626 | MyManager().run() 627 | except InvalidCommand as err: 628 | print(err, file=sys.stderr) 629 | sys.exit(1) 630 | 631 | This way, you maintain interoperability if some plug-in code supplies 632 | Flask-Script hooks you'd like to use, or vice versa. 633 | 634 | Accessing local proxies 635 | ----------------------- 636 | 637 | The ``Manager`` runs the command inside a `Flask test context `_. This means that you can access request-local proxies where appropriate, such as ``current_app``, which may be used by extensions. 638 | 639 | .. _api: 640 | 641 | API 642 | --- 643 | 644 | .. module:: flask_script 645 | 646 | .. autoclass:: Manager 647 | :members: run, add_option, add_command, command, option, shell 648 | 649 | .. autoclass:: Command 650 | :members: run, get_options 651 | 652 | .. autoclass:: Shell 653 | 654 | .. autoclass:: Server 655 | 656 | .. autoclass:: Option 657 | 658 | .. autoclass:: Group 659 | 660 | .. autofunction:: prompt 661 | 662 | .. autofunction:: prompt_bool 663 | 664 | .. autofunction:: prompt_pass 665 | 666 | .. autofunction:: prompt_choices 667 | 668 | .. _Flask: http://flask.pocoo.org 669 | .. _GitHub: http://github.com/smurfix/flask-script 670 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | import sys 5 | import unittest 6 | 7 | from flask import Flask 8 | from flask_script._compat import StringIO, text_type 9 | from flask_script import Command, Manager, Option, prompt, prompt_bool, prompt_choices 10 | 11 | from pytest import raises 12 | 13 | 14 | class Catcher(object): 15 | """Helper decorator to test raw_input.""" 16 | ## see: http://stackoverflow.com/questions/13480632/python-stringio-selectively-place-data-into-stdin 17 | 18 | def __init__(self, handler): 19 | self.handler = handler 20 | self.inputs = [] 21 | 22 | def __enter__(self): 23 | self.__stdin = sys.stdin 24 | self.__stdout = sys.stdout 25 | sys.stdin = self 26 | sys.stdout = self 27 | 28 | def __exit__(self, type, value, traceback): 29 | sys.stdin = self.__stdin 30 | sys.stdout = self.__stdout 31 | 32 | def write(self, value): 33 | self.__stdout.write(value) 34 | result = self.handler(value) 35 | if result is not None: 36 | self.inputs.append(result) 37 | 38 | def readline(self): 39 | return self.inputs.pop() 40 | 41 | def getvalue(self): 42 | return self.__stdout.getvalue() 43 | 44 | def truncate(self, pos): 45 | return self.__stdout.truncate(pos) 46 | 47 | 48 | def run(command_line, manager_run): 49 | ''' 50 | Runs a manager command line, returns exit code 51 | ''' 52 | sys.argv = command_line.split() 53 | exit_code = None 54 | try: 55 | manager_run() 56 | except SystemExit as e: 57 | exit_code = e.code 58 | 59 | return exit_code 60 | 61 | 62 | class SimpleCommand(Command): 63 | 'simple command' 64 | 65 | def run(self): 66 | print('OK') 67 | 68 | 69 | class NamedCommand(Command): 70 | 'named command' 71 | 72 | def run(self): 73 | print('OK') 74 | 75 | 76 | class ExplicitNamedCommand(Command): 77 | 'named command' 78 | 79 | name = 'named' 80 | 81 | def run(self): 82 | print('OK') 83 | 84 | 85 | class NamespacedCommand(Command): 86 | 'namespaced command' 87 | 88 | namespace = 'ns' 89 | 90 | def run(self): 91 | print('OK') 92 | 93 | 94 | class CommandWithArgs(Command): 95 | 'command with args' 96 | 97 | option_list = ( 98 | Option('name'), 99 | ) 100 | 101 | def run(self, name): 102 | print(name) 103 | 104 | 105 | class CommandWithOptionalArg(Command): 106 | 'command with optional arg' 107 | 108 | option_list = ( 109 | Option('-n','--name', required=False), 110 | ) 111 | 112 | def run(self, name="NotGiven"): 113 | print("OK name="+str(name)) 114 | 115 | 116 | class CommandWithOptions(Command): 117 | 'command with options' 118 | 119 | option_list = ( 120 | Option('-n', '--name', 121 | help='name to pass in', 122 | dest='name'), 123 | ) 124 | 125 | def run(self, name): 126 | print(name) 127 | 128 | 129 | class CommandWithDynamicOptions(Command): 130 | 'command with options' 131 | 132 | def __init__(self, default_name='Joe'): 133 | self.default_name = default_name 134 | 135 | def get_options(self): 136 | 137 | return ( 138 | Option('-n', '--name', 139 | help='name to pass in', 140 | dest='name', 141 | default=self.default_name), 142 | ) 143 | 144 | def run(self, name): 145 | print(name) 146 | 147 | 148 | class CommandWithCatchAll(Command): 149 | 'command with catch all args' 150 | 151 | capture_all_args = True 152 | 153 | def get_options(self): 154 | return (Option('--foo', dest='foo', 155 | action='store_true'),) 156 | 157 | def run(self, remaining_args, foo): 158 | print(remaining_args) 159 | 160 | 161 | class EmptyContext(object): 162 | def __enter__(self): 163 | pass 164 | def __exit__(self, a,b,c): 165 | pass 166 | 167 | class AppForTesting(object): 168 | def __init__(self, verbose=False): 169 | self.verbose = verbose 170 | def test_request_context(self): 171 | return EmptyContext() 172 | def __call__(self,**kw): 173 | if self.verbose: 174 | print("APP "+" ".join("%s=%s" % (k,v) for k,v in kw.items())) 175 | return self 176 | 177 | 178 | class TestManager: 179 | 180 | def setup(self): 181 | 182 | self.app = AppForTesting() 183 | 184 | def test_with_default_commands(self): 185 | 186 | manager = Manager(self.app) 187 | manager.set_defaults() 188 | 189 | assert 'runserver' in manager._commands 190 | assert 'shell' in manager._commands 191 | 192 | def test_without_default_commands(self): 193 | 194 | manager = Manager(self.app, with_default_commands=False) 195 | manager.set_defaults() 196 | 197 | assert 'runserver' not in manager._commands 198 | assert 'shell' not in manager._commands 199 | 200 | def test_add_command(self): 201 | 202 | manager = Manager(self.app) 203 | manager.add_command('simple', SimpleCommand()) 204 | 205 | assert isinstance(manager._commands['simple'], SimpleCommand) 206 | 207 | def test_add_named_command(self): 208 | 209 | manager = Manager(self.app) 210 | manager.add_command(NamedCommand()) 211 | 212 | assert 'named' in manager._commands 213 | assert isinstance(manager._commands['named'], NamedCommand) 214 | 215 | def test_add_explicit_named_command(self): 216 | 217 | manager = Manager(self.app) 218 | manager.add_command(ExplicitNamedCommand()) 219 | 220 | name = ExplicitNamedCommand.name 221 | assert name in manager._commands 222 | assert isinstance(manager._commands[name], ExplicitNamedCommand) 223 | 224 | def test_add_namespaced_command(self): 225 | 226 | manager = Manager(self.app) 227 | manager.add_command('one', NamespacedCommand()) 228 | manager.add_command('two', NamespacedCommand()) 229 | 230 | assert 'ns' in manager._commands 231 | assert isinstance(manager._commands['ns'], Manager) 232 | ns = manager._commands['ns'] 233 | assert isinstance(ns._commands['one'], NamespacedCommand) 234 | assert isinstance(ns._commands['two'], NamespacedCommand) 235 | 236 | def test_add_namespaced_simple_command(self): 237 | 238 | manager = Manager(self.app) 239 | manager.add_command('hello', SimpleCommand(), namespace='ns') 240 | manager.add_command('world', SimpleCommand(), namespace='ns') 241 | 242 | assert 'ns' in manager._commands 243 | assert isinstance(manager._commands['ns'], Manager) 244 | ns = manager._commands['ns'] 245 | assert isinstance(ns._commands['hello'], SimpleCommand) 246 | assert isinstance(ns._commands['world'], SimpleCommand) 247 | 248 | def test_add_command_class(self): 249 | 250 | manager = Manager(self.app) 251 | manager.add_command('simple', SimpleCommand) 252 | 253 | assert isinstance(manager._commands['simple'], SimpleCommand) 254 | 255 | def test_simple_command_decorator(self, capsys): 256 | 257 | manager = Manager(self.app) 258 | 259 | @manager.command 260 | def hello(): 261 | print('hello') 262 | 263 | assert 'hello' in manager._commands 264 | 265 | code = run('manage.py hello', manager.run) 266 | out, err = capsys.readouterr() 267 | assert 'hello' in out 268 | 269 | def test_simple_command_decorator_with_pos_arg(self, capsys): 270 | 271 | manager = Manager(self.app) 272 | 273 | @manager.command 274 | def hello(name): 275 | print('hello ' + name) 276 | 277 | assert 'hello' in manager._commands 278 | 279 | code = run('manage.py hello joe', manager.run) 280 | out, err = capsys.readouterr() 281 | assert 'hello joe' in out 282 | 283 | def test_method_command_decorator_with_pos_arg(self, capsys): 284 | 285 | manager = Manager(self.app) 286 | 287 | class SomeTest(object): 288 | def hello(self,name): 289 | print('hello ' + name) 290 | sometest = SomeTest() 291 | manager.command(sometest.hello) 292 | 293 | assert 'hello' in manager._commands 294 | 295 | code = run('manage.py hello joe', lambda: manager.run()) 296 | out, err = capsys.readouterr() 297 | assert 'hello joe' in out 298 | 299 | def test_command_decorator_with_options(self, capsys): 300 | 301 | manager = Manager(self.app) 302 | 303 | @manager.command 304 | def hello(name='fred'): 305 | 'Prints your name' 306 | print('hello ' + name) 307 | 308 | assert 'hello' in manager._commands 309 | 310 | code = run('manage.py hello --name=joe', manager.run) 311 | out, err = capsys.readouterr() 312 | assert 'hello joe' in out 313 | 314 | code = run('manage.py hello -n joe', manager.run) 315 | out, err = capsys.readouterr() 316 | assert 'hello joe' in out 317 | 318 | code = run('manage.py hello -?', manager.run) 319 | out, err = capsys.readouterr() 320 | assert 'Prints your name' in out 321 | 322 | code = run('manage.py hello --help', manager.run) 323 | out, err = capsys.readouterr() 324 | assert 'Prints your name' in out 325 | 326 | def test_no_help(self, capsys): 327 | """ 328 | Tests that erasing --help really works. 329 | """ 330 | 331 | manager = Manager(self.app) 332 | manager.help_args = () 333 | 334 | @manager.command 335 | def hello(name='fred'): 336 | 'Prints your name' 337 | print('hello ' + name) 338 | assert 'hello' in manager._commands 339 | 340 | code = run('manage.py --help hello', manager.run) 341 | out, err = capsys.readouterr() 342 | print(out) 343 | assert 'too many arguments' in err 344 | 345 | code = run('manage.py hello --help', manager.run) 346 | out, err = capsys.readouterr() 347 | print(out) 348 | assert 'too many arguments' in err 349 | 350 | def test_command_decorator_with_boolean_options(self, capsys): 351 | 352 | manager = Manager(self.app) 353 | 354 | @manager.command 355 | def verify(verified=False): 356 | 'Checks if verified' 357 | print('VERIFIED ? ' + 'YES' if verified else 'NO') 358 | 359 | assert 'verify' in manager._commands 360 | 361 | code = run('manage.py verify --verified', manager.run) 362 | out, err = capsys.readouterr() 363 | assert 'YES' in out 364 | 365 | code = run('manage.py verify -v', manager.run) 366 | out, err = capsys.readouterr() 367 | assert 'YES' in out 368 | 369 | code = run('manage.py verify', manager.run) 370 | out, err = capsys.readouterr() 371 | assert 'NO' in out 372 | 373 | code = run('manage.py verify -?', manager.run) 374 | out, err = capsys.readouterr() 375 | assert 'Checks if verified' in out 376 | 377 | def test_simple_command_decorator_with_pos_arg_and_options(self, capsys): 378 | 379 | manager = Manager(self.app) 380 | 381 | @manager.command 382 | def hello(name, url=None): 383 | if url: 384 | assert type(url) is text_type 385 | print('hello ' + name + ' from ' + url) 386 | else: 387 | assert type(name) is text_type 388 | print('hello ' + name) 389 | 390 | assert 'hello' in manager._commands 391 | 392 | code = run('manage.py hello joe', manager.run) 393 | out, err = capsys.readouterr() 394 | assert 'hello joe' in out 395 | 396 | code = run('manage.py hello joe --url=reddit.com', manager.run) 397 | out, err = capsys.readouterr() 398 | assert 'hello joe from reddit.com' in out 399 | 400 | def test_command_decorator_with_additional_options(self, capsys): 401 | 402 | manager = Manager(self.app) 403 | 404 | @manager.option('-n', '--name', dest='name', help='Your name') 405 | def hello(name): 406 | print('hello ' + name) 407 | 408 | assert 'hello' in manager._commands 409 | 410 | code = run('manage.py hello --name=joe', manager.run) 411 | out, err = capsys.readouterr() 412 | assert 'hello joe' in out 413 | 414 | code = run('manage.py hello -?', manager.run) 415 | out, err = capsys.readouterr() 416 | assert 'Your name' in out 417 | 418 | @manager.option('-n', '--name', dest='name', help='Your name') 419 | @manager.option('-u', '--url', dest='url', help='Your URL') 420 | def hello_again(name, url=None): 421 | if url: 422 | print('hello ' + name + ' from ' + url) 423 | else: 424 | print('hello ' + name) 425 | 426 | assert 'hello_again' in manager._commands 427 | 428 | code = run('manage.py hello_again --name=joe', manager.run) 429 | out, err = capsys.readouterr() 430 | assert 'hello joe' in out 431 | 432 | code = run('manage.py hello_again --name=joe --url=reddit.com', manager.run) 433 | out, err = capsys.readouterr() 434 | assert 'hello joe from reddit.com' in out 435 | 436 | def test_global_option_provided_before_and_after_command(self, capsys): 437 | 438 | manager = Manager(self.app) 439 | manager.add_option('-c', '--config', dest='config_name', required=False, default='Development') 440 | manager.add_command('simple', SimpleCommand()) 441 | 442 | assert isinstance(manager._commands['simple'], SimpleCommand) 443 | 444 | code = run('manage.py -c Development simple', manager.run) 445 | out, err = capsys.readouterr() 446 | assert code == 0 447 | assert 'OK' in out 448 | 449 | code = run('manage.py simple -c Development', manager.run) 450 | out, err = capsys.readouterr() 451 | assert code == 2 452 | assert 'OK' not in out 453 | 454 | def test_global_option_value(self, capsys): 455 | 456 | def create_app(config_name='Empty'): 457 | print(config_name) 458 | return self.app 459 | 460 | manager = Manager(create_app) 461 | manager.add_option('-c', '--config', dest='config_name', required=False, default='Development') 462 | manager.add_command('simple', SimpleCommand()) 463 | 464 | assert isinstance(manager._commands['simple'], SimpleCommand) 465 | 466 | code = run('manage.py simple', manager.run) 467 | out, err = capsys.readouterr() 468 | assert code == 0 469 | assert 'Empty' not in out # config_name is overwritten by default option value 470 | assert 'Development' in out 471 | assert 'OK' in out 472 | 473 | def test_get_usage(self): 474 | 475 | manager = Manager(self.app) 476 | manager.add_command('simple', SimpleCommand()) 477 | 478 | usage = manager.create_parser('manage.py').format_help() 479 | assert 'simple command' in usage 480 | 481 | def test_get_usage_with_specified_usage(self): 482 | 483 | manager = Manager(self.app, usage='hello') 484 | manager.add_command('simple', SimpleCommand()) 485 | 486 | usage = manager.create_parser('manage.py').format_help() 487 | assert 'simple command' in usage 488 | assert 'hello' in usage 489 | 490 | def test_run_existing_command(self, capsys): 491 | 492 | manager = Manager(self.app) 493 | manager.add_command('simple', SimpleCommand()) 494 | code = run('manage.py simple', manager.run) 495 | out, err = capsys.readouterr() 496 | assert 'OK' in out 497 | 498 | def test_run_non_existant_command(self, capsys): 499 | 500 | manager = Manager(self.app) 501 | run('manage.py simple', manager.run) 502 | out, err = capsys.readouterr() 503 | assert 'invalid choice' in err 504 | 505 | def test_run_existing(self, capsys): 506 | 507 | manager = Manager(self.app) 508 | manager.add_command('simple', SimpleCommand()) 509 | 510 | code = run('manage.py simple', manager.run) 511 | out, err = capsys.readouterr() 512 | assert 0 == code 513 | assert 'OK' in out 514 | 515 | def test_run_existing_bind_later(self, capsys): 516 | 517 | manager = Manager(self.app) 518 | 519 | code = run('manage.py simple', lambda: manager.run({'simple': SimpleCommand()})) 520 | out, err = capsys.readouterr() 521 | assert code == 0 522 | assert 'OK' in out 523 | 524 | def test_run_not_existing(self, capsys): 525 | 526 | manager = Manager(self.app) 527 | 528 | code = run('manage.py simple', manager.run) 529 | out, err = capsys.readouterr() 530 | assert code == 2 531 | assert 'OK' not in out 532 | 533 | def test_run_no_name(self, capsys): 534 | 535 | manager = Manager(self.app) 536 | manager.add_command('simple', SimpleCommand()) 537 | 538 | code = run('manage.py', manager.run) 539 | out, err = capsys.readouterr() 540 | assert code == 2 541 | assert 'simple command' in out 542 | 543 | def test_run_good_options(self, capsys): 544 | 545 | manager = Manager(self.app) 546 | manager.add_command('simple', CommandWithOptions()) 547 | 548 | code = run('manage.py simple --name=Joe', manager.run) 549 | out, err = capsys.readouterr() 550 | assert code == 0 551 | assert 'Joe' in out 552 | 553 | def test_run_dynamic_options(self, capsys): 554 | 555 | manager = Manager(self.app) 556 | manager.add_command('simple', CommandWithDynamicOptions('Fred')) 557 | 558 | code = run('manage.py simple', manager.run) 559 | out, err = capsys.readouterr() 560 | assert code == 0 561 | assert 'Fred' in out 562 | 563 | def test_run_catch_all(self, capsys): 564 | manager = Manager(self.app) 565 | manager.add_command('catch', CommandWithCatchAll()) 566 | 567 | code = run('manage.py catch pos1 --foo pos2 --bar', manager.run) 568 | out, err = capsys.readouterr() 569 | out_list = [o.strip('u\'') for o in out.strip('[]\n').split(', ')] 570 | assert code == 0 571 | assert ['pos1', 'pos2', '--bar'] == out_list 572 | 573 | def test_run_bad_options(self, capsys): 574 | manager = Manager(self.app) 575 | manager.add_command('simple', CommandWithOptions()) 576 | 577 | code = run('manage.py simple --foo=bar', manager.run) 578 | assert code == 2 579 | 580 | def test_init_with_flask_instance(self): 581 | manager = Manager(self.app) 582 | assert callable(manager.app) 583 | 584 | def test_init_with_callable(self): 585 | manager = Manager(lambda: self.app) 586 | assert callable(manager.app) 587 | 588 | def test_raise_index_error(self): 589 | 590 | manager = Manager(self.app) 591 | 592 | @manager.command 593 | def error(): 594 | raise IndexError() 595 | 596 | with raises(IndexError): 597 | run('manage.py error', manager.run) 598 | 599 | def test_run_with_default_command(self, capsys): 600 | manager = Manager(self.app) 601 | manager.add_command('simple', SimpleCommand()) 602 | 603 | code = run('manage.py', lambda: manager.run(default_command='simple')) 604 | out, err = capsys.readouterr() 605 | assert code == 0 606 | assert 'OK' in out 607 | 608 | def test_command_with_prompt(self, capsys): 609 | 610 | manager = Manager(self.app) 611 | 612 | @manager.command 613 | def hello(): 614 | print(prompt(name='hello')) 615 | 616 | @Catcher 617 | def hello_john(msg): 618 | if re.search("hello", msg): 619 | return 'john' 620 | 621 | with hello_john: 622 | code = run('manage.py hello', manager.run) 623 | out, err = capsys.readouterr() 624 | assert 'hello: john' in out 625 | 626 | def test_command_with_default_prompt(self, capsys): 627 | 628 | manager = Manager(self.app) 629 | 630 | @manager.command 631 | def hello(): 632 | print(prompt(name='hello', default='romeo')) 633 | 634 | @Catcher 635 | def hello(msg): 636 | if re.search("hello", msg): 637 | return '\n' # just hit enter 638 | 639 | with hello: 640 | code = run('manage.py hello', manager.run) 641 | out, err = capsys.readouterr() 642 | assert 'hello [romeo]: romeo' in out 643 | 644 | @Catcher 645 | def hello_juliette(msg): 646 | if re.search("hello", msg): 647 | return 'juliette' 648 | 649 | with hello_juliette: 650 | code = run('manage.py hello', manager.run) 651 | out, err = capsys.readouterr() 652 | assert 'hello [romeo]: juliette' in out 653 | 654 | 655 | def test_command_with_prompt_bool(self, capsys): 656 | 657 | manager = Manager(self.app) 658 | 659 | @manager.command 660 | def hello(): 661 | print(prompt_bool(name='correct', default=True, yes_choices=['y'], 662 | no_choices=['n']) and 'yes' or 'no') 663 | 664 | @Catcher 665 | def correct_default(msg): 666 | if re.search("correct", msg): 667 | return '\n' # just hit enter 668 | 669 | @Catcher 670 | def correct_y(msg): 671 | if re.search("correct", msg): 672 | return 'y' 673 | 674 | @Catcher 675 | def correct_n(msg): 676 | if re.search("correct", msg): 677 | return 'n' 678 | 679 | with correct_default: 680 | code = run('manage.py hello', manager.run) 681 | out, err = capsys.readouterr() 682 | assert 'correct [y]: yes' in out 683 | 684 | with correct_y: 685 | code = run('manage.py hello', manager.run) 686 | out, err = capsys.readouterr() 687 | assert 'correct [y]: yes' in out 688 | 689 | with correct_n: 690 | code = run('manage.py hello', manager.run) 691 | out, err = capsys.readouterr() 692 | assert 'correct [y]: no' in out 693 | 694 | def test_command_with_prompt_choices(self, capsys): 695 | 696 | manager = Manager(self.app) 697 | 698 | @manager.command 699 | def hello(): 700 | print(prompt_choices(name='hello', choices=['peter', 'john', 'sam'])) 701 | 702 | @Catcher 703 | def hello_john(msg): 704 | if re.search("hello", msg): 705 | return 'john' 706 | 707 | with hello_john: 708 | code = run('manage.py hello', manager.run) 709 | out, err = capsys.readouterr() 710 | assert 'hello - (peter, john, sam): john' in out 711 | 712 | def test_command_with_default_prompt_choices(self, capsys): 713 | 714 | manager = Manager(self.app) 715 | 716 | @manager.command 717 | def hello(): 718 | print(prompt_choices(name='hello', choices=['peter', 'charlie', 'sam'], default="john")) 719 | 720 | @Catcher 721 | def hello_john(msg): 722 | if re.search("hello", msg): 723 | return '\n' 724 | 725 | with hello_john: 726 | code = run('manage.py hello', manager.run) 727 | out, err = capsys.readouterr() 728 | assert 'hello - (peter, charlie, sam) [john]: john' in out 729 | 730 | @Catcher 731 | def hello_charlie(msg): 732 | if re.search("hello", msg): 733 | return 'charlie' 734 | 735 | with hello_charlie: 736 | code = run('manage.py hello', manager.run) 737 | out, err = capsys.readouterr() 738 | assert 'hello - (peter, charlie, sam) [john]: charlie' in out 739 | 740 | class TestSubManager: 741 | 742 | def setup(self): 743 | 744 | self.app = AppForTesting() 745 | 746 | def test_add_submanager(self): 747 | 748 | sub_manager = Manager() 749 | 750 | manager = Manager(self.app) 751 | manager.add_command('sub_manager', sub_manager) 752 | 753 | assert isinstance(manager._commands['sub_manager'], Manager) 754 | assert sub_manager.parent == manager 755 | assert sub_manager.get_options() == manager.get_options() 756 | 757 | def test_run_submanager_command(self, capsys): 758 | 759 | sub_manager = Manager() 760 | sub_manager.add_command('simple', SimpleCommand()) 761 | 762 | manager = Manager(self.app) 763 | manager.add_command('sub_manager', sub_manager) 764 | 765 | code = run('manage.py sub_manager simple', manager.run) 766 | out, err = capsys.readouterr() 767 | assert code == 0 768 | assert 'OK' in out 769 | 770 | def test_submanager_has_options(self, capsys): 771 | 772 | sub_manager = Manager() 773 | sub_manager.add_command('simple', SimpleCommand()) 774 | 775 | manager = Manager(self.app) 776 | manager.add_command('sub_manager', sub_manager) 777 | manager.add_option('-c', '--config', dest='config', required=False) 778 | 779 | code = run('manage.py sub_manager simple', manager.run) 780 | out, err = capsys.readouterr() 781 | assert code == 0 782 | assert 'OK' in out 783 | 784 | code = run('manage.py -c Development sub_manager simple', manager.run) 785 | out, err = capsys.readouterr() 786 | assert code == 0 787 | assert 'OK' in out 788 | 789 | 790 | def test_submanager_separate_options(self, capsys): 791 | 792 | sub_manager = Manager(AppForTesting(verbose=True), with_default_commands=False) 793 | sub_manager.add_command('opt', CommandWithOptionalArg()) 794 | sub_manager.add_option('-n', '--name', dest='name_sub', required=False) 795 | 796 | manager = Manager(AppForTesting(verbose=True), with_default_commands=False) 797 | manager.add_command('sub_manager', sub_manager) 798 | manager.add_option('-n', '--name', dest='name_main', required=False) 799 | 800 | code = run('manage.py -n MyMainName sub_manager -n MySubName opt -n MyName', manager.run) 801 | out, err = capsys.readouterr() 802 | assert code == 0 803 | assert 'APP name_main=MyMainName' in out 804 | assert 'APP name_sub=MySubName' in out 805 | assert 'OK name=MyName' in out 806 | 807 | def test_manager_usage_with_submanager(self, capsys): 808 | 809 | sub_manager = Manager(usage='Example sub-manager') 810 | 811 | manager = Manager(self.app) 812 | manager.add_command('sub_manager', sub_manager) 813 | 814 | code = run('manage.py -?', manager.run) 815 | out, err = capsys.readouterr() 816 | assert code == 0 817 | assert 'Example sub-manager' in out 818 | 819 | def test_submanager_usage_and_help_and_description(self, capsys): 820 | 821 | sub_manager = Manager(usage='sub_manager [--foo]', 822 | help='shorter desc for submanager', 823 | description='longer desc for submanager') 824 | sub_manager.add_command('simple', SimpleCommand()) 825 | 826 | manager = Manager(self.app) 827 | manager.add_command('sub_manager', sub_manager) 828 | 829 | code = run('manage.py -?', manager.run) 830 | out, err = capsys.readouterr() 831 | assert code == 0 832 | assert 'sub_manager [--foo]' not in out 833 | assert 'shorter desc for submanager' in out 834 | assert 'longer desc for submanager' not in out 835 | 836 | code = run('manage.py sub_manager', manager.run) 837 | out, err = capsys.readouterr() 838 | assert code == 2 839 | assert 'sub_manager [--foo]' in out 840 | assert 'shorter desc for submanager' not in out 841 | assert 'longer desc for submanager' in out 842 | assert 'simple command' in out 843 | 844 | code = run('manage.py sub_manager -?', manager.run) 845 | out, err = capsys.readouterr() 846 | assert code == 0 847 | assert 'sub_manager [--foo]' in out 848 | assert 'shorter desc for submanager' not in out 849 | assert 'longer desc for submanager' in out 850 | assert 'simple command' in out 851 | 852 | code = run('manage.py sub_manager simple -?', manager.run) 853 | out, err = capsys.readouterr() 854 | assert code == 0 855 | assert 'sub_manager [--foo] simple [-?]' in out 856 | assert 'simple command' in out 857 | 858 | def test_submanager_has_no_default_commands(self): 859 | 860 | sub_manager = Manager() 861 | 862 | manager = Manager() 863 | manager.add_command('sub_manager', sub_manager) 864 | manager.set_defaults() 865 | 866 | assert 'runserver' not in sub_manager._commands 867 | assert 'shell' not in sub_manager._commands 868 | --------------------------------------------------------------------------------