From 3d8f8c3c9e5490dbccc65276df15b4437b5a2c0b Mon Sep 17 00:00:00 2001 From: Jeremy Epstein Date: Wed, 24 Aug 2016 21:29:25 +1000 Subject: [PATCH] Use Click instead of Flask-Script for CLI commands - Update Flask to 0.11 (with built-in Click integration) - Remove manage.py - Add autoapp.py per Flask doc's new recommendations for using Click with an app factory - Define cookiecutter-flask's commands in cli.py - Register shell context and shell commands as part of factory-based app creation - Update README instructions with examples for running commands in new Click style --- README.rst | 8 +- {{cookiecutter.app_name}}/README.rst | 16 ++-- {{cookiecutter.app_name}}/manage.py | 77 ------------------- .../requirements/dev.txt | 3 - .../requirements/prod.txt | 9 ++- .../{{cookiecutter.app_name}}/app.py | 17 ++++ .../{{cookiecutter.app_name}}/autoapp.py | 10 +++ .../{{cookiecutter.app_name}}/cli.py | 17 ++++ .../{{cookiecutter.app_name}}/extensions.py | 2 +- 9 files changed, 67 insertions(+), 92 deletions(-) delete mode 100755 {{cookiecutter.app_name}}/manage.py create mode 100755 {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/autoapp.py create mode 100644 {{cookiecutter.app_name}}/{{cookiecutter.app_name}}/cli.py diff --git a/README.rst b/README.rst index b93b9c3..ce59a32 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ Features - Flask-Bcrypt for password hashing - Procfile for deploying to a PaaS (e.g. Heroku) - pytest and Factory-Boy for testing (example tests included) -- A simple ``manage.py`` script. +- Flask's Click CLI configured with simple commands - CSS and JS minification using Flask-Assets - Optional bower support for frontend package management - Caching using Flask-Cache @@ -70,6 +70,12 @@ BSD licensed. Changelog --------- +0.10.0 (08/24/2016) +******************* + +- Update to Flask 0.11. +- Use Click instead of Flask-Script for CLI commands. + 0.9.0 (03/06/2016) ****************** diff --git a/{{cookiecutter.app_name}}/README.rst b/{{cookiecutter.app_name}}/README.rst index 79ea72f..4c43557 100644 --- a/{{cookiecutter.app_name}}/README.rst +++ b/{{cookiecutter.app_name}}/README.rst @@ -42,6 +42,10 @@ Once you have installed your DBMS, run the following to create your app's databa Deployment ---------- +Before running shell commands, set the ``FLASK_APP`` environment variable :: + + export FLASK_APP="{{cookiecutter.app_name}}.autoapp" + In your production environment, make sure the ``{{cookiecutter.app_name|upper}}_ENV`` environment variable is set to ``"prod"``. @@ -50,9 +54,9 @@ Shell To open the interactive shell, run :: - python manage.py shell + flask shell -By default, you will have access to ``app``, ``db``, and the ``User`` model. +By default, you will have access to ``app``, ``db``, ``g``, and the ``User`` model. Running Tests @@ -60,7 +64,7 @@ Running Tests To run all tests, run :: - python manage.py test + flask test Migrations @@ -69,13 +73,13 @@ Migrations Whenever a database migration needs to be made. Run the following commands: :: - python manage.py db migrate + flask db migrate This will generate a new migration script. Then run: :: - python manage.py db upgrade + flask db upgrade To apply the migration. -For a full migration command reference, run ``python manage.py db --help``. +For a full migration command reference, run ``flask db --help``. diff --git a/{{cookiecutter.app_name}}/manage.py b/{{cookiecutter.app_name}}/manage.py deleted file mode 100755 index fce0e49..0000000 --- a/{{cookiecutter.app_name}}/manage.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -"""Management script.""" -import os -from glob import glob -from subprocess import call - -from flask_migrate import Migrate, MigrateCommand -from flask_script import Command, Manager, Option, Server, Shell -from flask_script.commands import Clean, ShowUrls - -from {{cookiecutter.app_name}}.app import create_app -from {{cookiecutter.app_name}}.database import db -from {{cookiecutter.app_name}}.settings import DevConfig, ProdConfig -from {{cookiecutter.app_name}}.user.models import User - -CONFIG = ProdConfig if os.environ.get('{{cookiecutter.app_name | upper}}_ENV') == 'prod' else DevConfig -HERE = os.path.abspath(os.path.dirname(__file__)) -TEST_PATH = os.path.join(HERE, 'tests') - -app = create_app(CONFIG) -manager = Manager(app) -migrate = Migrate(app, db) - - -def _make_context(): - """Return context dict for a shell session so you can access app, db, and the User model by default.""" - return {'app': app, 'db': db, 'User': User} - - -@manager.command -def test(): - """Run the tests.""" - import pytest - exit_code = pytest.main([TEST_PATH, '--verbose']) - return exit_code - - -class Lint(Command): - """Lint and check code style with flake8 and isort.""" - - def get_options(self): - """Command line options.""" - return ( - Option('-f', '--fix-imports', action='store_true', dest='fix_imports', default=False, - help='Fix imports using isort, before linting'), - ) - - def run(self, fix_imports): - """Run command.""" - skip = ['requirements'] - root_files = glob('*.py') - root_directories = [name for name in next(os.walk('.'))[1] if not name.startswith('.')] - files_and_directories = [arg for arg in root_files + root_directories if arg not in skip] - - def execute_tool(description, *args): - """Execute a checking tool with its arguments.""" - command_line = list(args) + files_and_directories - print('{}: {}'.format(description, ' '.join(command_line))) - rv = call(command_line) - if rv is not 0: - exit(rv) - - if fix_imports: - execute_tool('Fixing import order', 'isort', '-rc') - execute_tool('Checking code style', 'flake8') - - -manager.add_command('server', Server()) -manager.add_command('shell', Shell(make_context=_make_context)) -manager.add_command('db', MigrateCommand) -manager.add_command('urls', ShowUrls()) -manager.add_command('clean', Clean()) -manager.add_command('lint', Lint()) - -if __name__ == '__main__': - manager.run() diff --git a/{{cookiecutter.app_name}}/requirements/dev.txt b/{{cookiecutter.app_name}}/requirements/dev.txt index a7f3f24..2f82814 100644 --- a/{{cookiecutter.app_name}}/requirements/dev.txt +++ b/{{cookiecutter.app_name}}/requirements/dev.txt @@ -6,9 +6,6 @@ pytest==2.9.0 WebTest==2.0.20 factory-boy==2.6.1 -# Management script -Flask-Script==2.0.5 - # Lint and code style flake8==2.5.4 flake8-blind-except==0.1.0 diff --git a/{{cookiecutter.app_name}}/requirements/prod.txt b/{{cookiecutter.app_name}}/requirements/prod.txt index dedc736..48be129 100644 --- a/{{cookiecutter.app_name}}/requirements/prod.txt +++ b/{{cookiecutter.app_name}}/requirements/prod.txt @@ -4,11 +4,12 @@ setuptools==20.2.2 wheel==0.29.0 # Flask -Flask==0.10.1 +Flask==0.11.1 MarkupSafe==0.23 Werkzeug==0.11.4 Jinja2==2.8 itsdangerous==0.24 +click>=5.0 # Database Flask-SQLAlchemy==2.1 @@ -16,7 +17,7 @@ psycopg2==2.6.1 SQLAlchemy==1.0.12 # Migrations -Flask-Migrate==1.8.0 +Flask-Migrate==2.0.0 # Forms Flask-WTF==0.12 @@ -26,7 +27,7 @@ WTForms==2.1 gunicorn>=19.1.1 # Assets -Flask-Assets==0.11 +Flask-Assets==0.12 cssmin>=0.2.0 jsmin>=2.0.11 @@ -35,7 +36,7 @@ Flask-Login==0.3.2 Flask-Bcrypt==0.7.1 # Caching -Flask-Cache>=0.13.1 +Flask-Caching>=1.0.0 # Debug toolbar Flask-DebugToolbar==0.10.0 diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/app.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/app.py index 94a1430..67f3120 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/app.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/app.py @@ -18,6 +18,8 @@ def create_app(config_object=ProdConfig): register_extensions(app) register_blueprints(app) register_errorhandlers(app) + register_shellcontext(app) + register_commands(app) return app @@ -51,3 +53,18 @@ def register_errorhandlers(app): for errcode in [401, 404, 500]: app.errorhandler(errcode)(render_error) return None + + +def register_shellcontext(app): + """Register shell context objects.""" + def shell_context(): + """Shell context objects.""" + return { + 'db': db} + + app.shell_context_processor(shell_context) + + +def register_commands(app): + """Register Click commands.""" + app.cli.add_command(cli.test_command) diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/autoapp.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/autoapp.py new file mode 100755 index 0000000..d3f6a8d --- /dev/null +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/autoapp.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +"""Create an application instance.""" +import os + +from {{cookiecutter.app_name}}.app import create_app +from {{cookiecutter.app_name}}.settings import DevConfig, ProdConfig + +CONFIG = ProdConfig if os.environ.get('{{cookiecutter.app_name | upper}}_ENV') == 'prod' else DevConfig + +app = create_app(CONFIG) diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/cli.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/cli.py new file mode 100644 index 0000000..c1f619d --- /dev/null +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/cli.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +"""Click commands.""" +import click +import os +from flask.cli import with_appcontext + +HERE = os.path.abspath(os.path.dirname(__file__)) +PROJECT_ROOT = os.path.join(HERE, os.pardir) +TEST_PATH = os.path.join(PROJECT_ROOT, 'tests') + + +@click.command('test') +@with_appcontext +def test_command(): + """Run the tests.""" + import pytest + pytest.main([TEST_PATH, '--verbose']) diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/extensions.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/extensions.py index 4a36529..cd6f0f8 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/extensions.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/extensions.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Extensions module. Each extension is initialized in the app factory located in app.py.""" from flask_bcrypt import Bcrypt -from flask_cache import Cache +from flask_caching import Cache from flask_debugtoolbar import DebugToolbarExtension from flask_login import LoginManager from flask_migrate import Migrate