From 74e222f1a1cbfc2fac102fefc1115e9d0a6586dc Mon Sep 17 00:00:00 2001 From: Glandos Date: Sun, 6 Jun 2021 14:30:52 +0200 Subject: [PATCH] remove usage of Flask-Script Use flask.cli instead with compatibility layer for existing commands, such as "runserver". --- Makefile | 2 +- docs/installation.rst | 2 +- ihatemoney/manage.py | 127 ++++++++++++++++------------------ ihatemoney/tests/main_test.py | 28 +++----- setup.cfg | 7 +- 5 files changed, 77 insertions(+), 89 deletions(-) diff --git a/Makefile b/Makefile index 30de0dd7..2e8c18ff 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ update: remove-install-stamp install ## Update the dependencies .PHONY: serve serve: install ## Run the ihatemoney server @echo 'Running ihatemoney on http://localhost:5000' - $(PYTHON) -m ihatemoney.manage runserver + $(PYTHON) -m ihatemoney.manage run .PHONY: test test: install-dev ## Run the tests diff --git a/docs/installation.rst b/docs/installation.rst index 82ac12f2..a366cbe3 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -61,7 +61,7 @@ Test it Once installed, you can start a test server:: - ihatemoney runserver + ihatemoney run And point your browser at `http://localhost:5000 `_. diff --git a/ihatemoney/manage.py b/ihatemoney/manage.py index eb1e24c2..805a07f3 100755 --- a/ihatemoney/manage.py +++ b/ihatemoney/manage.py @@ -5,8 +5,8 @@ import os import random import sys -from flask_migrate import Migrate, MigrateCommand -from flask_script import Command, Manager, Option +import click +from flask.cli import FlaskGroup from werkzeug.security import generate_password_hash from ihatemoney.models import Project, db @@ -14,31 +14,48 @@ from ihatemoney.run import create_app from ihatemoney.utils import create_jinja_env -class GeneratePasswordHash(Command): +@click.group(cls=FlaskGroup, create_app=create_app) +def cli(): + """IHateMoney Management script""" + +@cli.command( + context_settings={"ignore_unknown_options": True, "allow_extra_args": True} +) +@click.pass_context +def runserver(ctx): + """Deprecated, use the "run" command instead""" + click.secho( + '"runserver" is deprecated, please use the standard "run" flask command', + fg="red", + ) + run = cli.get_command(ctx, "run") + ctx.forward(run) + + +@click.command(name="generate_password_hash") +def password_hash(): """Get password from user and hash it without printing it in clear text.""" - - def run(self): - password = getpass.getpass(prompt="Password: ") - print(generate_password_hash(password)) + password = getpass.getpass(prompt="Password: ") + print(generate_password_hash(password)) -class GenerateConfig(Command): - def get_options(self): - return [ - Option( - "config_file", - choices=[ - "ihatemoney.cfg", - "apache-vhost.conf", - "gunicorn.conf.py", - "supervisord.conf", - "nginx.conf", - ], - ) +@click.command() +@click.argument( + "config_file", + type=click.Choice( + [ + "ihatemoney.cfg", + "apache-vhost.conf", + "gunicorn.conf.py", + "supervisord.conf", + "nginx.conf", ] + ), +) +def generate_config(config_file): + """Generate front-end server configuration""" - @staticmethod def gen_secret_key(): return "".join( [ @@ -49,59 +66,33 @@ class GenerateConfig(Command): ] ) - def run(self, config_file): - env = create_jinja_env("conf-templates", strict_rendering=True) - template = env.get_template(f"{config_file}.j2") + env = create_jinja_env("conf-templates", strict_rendering=True) + template = env.get_template(f"{config_file}.j2") - bin_path = os.path.dirname(sys.executable) - pkg_path = os.path.abspath(os.path.dirname(__file__)) + bin_path = os.path.dirname(sys.executable) + pkg_path = os.path.abspath(os.path.dirname(__file__)) - print( - template.render( - pkg_path=pkg_path, - bin_path=bin_path, - sys_prefix=sys.prefix, - secret_key=self.gen_secret_key(), - ) + print( + template.render( + pkg_path=pkg_path, + bin_path=bin_path, + sys_prefix=sys.prefix, + secret_key=gen_secret_key(), ) + ) -class DeleteProject(Command): - def run(self, project_name): - demo_project = Project.query.get(project_name) - db.session.delete(demo_project) +@cli.command() +@click.argument("project_name") +def delete_project(project_name): + """Delete a project""" + project = Project.query.get(project_name) + if project is None: + click.secho(f'Project "{project_name}" not found', fg="red") + else: + db.session.delete(project) db.session.commit() -def main(): - QUIET_COMMANDS = ("generate_password_hash", "generate-config") - - exception = None - backup_stderr = sys.stderr - # Hack to divert stderr for commands generating content to stdout - # to avoid confusing the user - if len(sys.argv) > 1 and sys.argv[1] in QUIET_COMMANDS: - sys.stderr = open(os.devnull, "w") - - try: - app = create_app() - Migrate(app, db) - except Exception as e: - exception = e - - # Restore stderr - sys.stderr = backup_stderr - - if exception: - raise exception - - manager = Manager(app) - manager.add_command("db", MigrateCommand) - manager.add_command("generate_password_hash", GeneratePasswordHash) - manager.add_command("generate-config", GenerateConfig) - manager.add_command("delete-project", DeleteProject) - manager.run() - - if __name__ == "__main__": - main() + cli() diff --git a/ihatemoney/tests/main_test.py b/ihatemoney/tests/main_test.py index 06dbfacf..f052ee01 100644 --- a/ihatemoney/tests/main_test.py +++ b/ihatemoney/tests/main_test.py @@ -1,4 +1,3 @@ -import io import os import smtplib import socket @@ -9,7 +8,7 @@ from sqlalchemy import orm from ihatemoney import models from ihatemoney.currency_convertor import CurrencyConverter -from ihatemoney.manage import DeleteProject, GenerateConfig, GeneratePasswordHash +from ihatemoney.manage import delete_project, generate_config, password_hash from ihatemoney.run import load_configuration from ihatemoney.tests.common.ihatemoney_testcase import BaseTestCase, IhatemoneyTestCase @@ -82,28 +81,23 @@ class CommandTestCase(BaseTestCase): - raise no exception - produce something non-empty """ - cmd = GenerateConfig() - for config_file in cmd.get_options()[0].kwargs["choices"]: - with patch("sys.stdout", new=io.StringIO()) as stdout: - cmd.run(config_file) - print(stdout.getvalue()) - self.assertNotEqual(len(stdout.getvalue().strip()), 0) + runner = self.app.test_cli_runner() + for config_file in generate_config.params[0].type.choices: + result = runner.invoke(generate_config, config_file) + self.assertNotEqual(len(result.output.strip()), 0) def test_generate_password_hash(self): - cmd = GeneratePasswordHash() - with patch("sys.stdout", new=io.StringIO()) as stdout, patch( - "getpass.getpass", new=lambda prompt: "secret" - ): # NOQA - cmd.run() - print(stdout.getvalue()) - self.assertEqual(len(stdout.getvalue().strip()), 189) + runner = self.app.test_cli_runner() + with patch("getpass.getpass", new=lambda prompt: "secret"): + result = runner.invoke(password_hash) + self.assertEqual(len(result.output.strip()), 94) def test_demo_project_deletion(self): self.create_project("demo") self.assertEquals(models.Project.query.get("demo").name, "demo") - cmd = DeleteProject() - cmd.run("demo") + runner = self.app.test_cli_runner() + runner.invoke(delete_project, "demo") self.assertEqual(len(models.Project.query.all()), 0) diff --git a/setup.cfg b/setup.cfg index 8446f90c..044defe2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,7 +32,6 @@ install_requires = Flask-Mail~=0.9 Flask-Migrate>=2.5.3,<3 # Not following semantic versioning (e.g. https://github.com/miguelgrinberg/flask-migrate/commit/1af28ba273de6c88544623b8dc02dd539340294b) Flask-RESTful~=0.3 - Flask-Script~=2.0 Flask-SQLAlchemy~=2.4 Flask-WTF~=0.14,>=0.14.3 # See b76da172652da94c1f9c4b2fdd965375da8a2c3f WTForms~=2.2.1,<2.3 # See #567 @@ -58,8 +57,12 @@ doc = docutils==0.17.1 [options.entry_points] +flask.commands = + generate_password_hash = ihatemoney.manage:password_hash + generate-config = ihatemoney.manage:generate_config + console_scripts = - ihatemoney = ihatemoney.manage:main + ihatemoney = ihatemoney.manage:cli paste.app_factory = main = ihatemoney.run:main