""" CLI for development """ import logging import os import sys from pathlib import Path import django import rich_click as click from bx_py_utils.path import assert_is_file from cli_base.cli_tools.subprocess_utils import verbose_check_call from cli_base.cli_tools.version_info import print_version from django.core.management.commands.test import Command as DjangoTestCommand from django_yunohost_integration.local_test import CreateResults, create_local_test from manageprojects.utilities import code_style from manageprojects.utilities.publish import publish_package from rich import print # noqa; noqa from rich_click import RichGroup import for_runners_ynh logger = logging.getLogger(__name__) PACKAGE_ROOT = Path(for_runners_ynh.__file__).parent.parent assert_is_file(PACKAGE_ROOT / 'pyproject.toml') OPTION_ARGS_DEFAULT_TRUE = dict(is_flag=True, show_default=True, default=True) OPTION_ARGS_DEFAULT_FALSE = dict(is_flag=True, show_default=True, default=False) ARGUMENT_EXISTING_DIR = dict( type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True, path_type=Path) ) ARGUMENT_NOT_EXISTING_DIR = dict( type=click.Path( exists=False, file_okay=False, dir_okay=True, readable=False, writable=True, path_type=Path, ) ) ARGUMENT_EXISTING_FILE = dict( type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, path_type=Path) ) CLI_EPILOG = 'Project Homepage: https://github.com/YunoHost-Apps/django-for-runners_ynh' class ClickGroup(RichGroup): # FIXME: How to set the "info_name" easier? def make_context(self, info_name, *args, **kwargs): info_name = './dev-cli.py' return super().make_context(info_name, *args, **kwargs) @click.group(cls=ClickGroup, epilog=CLI_EPILOG) def cli(): pass @click.command() @click.option('--verbose/--no-verbose', **OPTION_ARGS_DEFAULT_FALSE) def mypy(verbose: bool = True): """Run Mypy (configured in pyproject.toml)""" verbose_check_call('mypy', '.', cwd=PACKAGE_ROOT, verbose=verbose, exit_on_error=True) cli.add_command(mypy) @click.command() @click.option('--verbose/--no-verbose', **OPTION_ARGS_DEFAULT_FALSE) def coverage(verbose: bool = True): """ Run and show coverage. """ verbose_check_call('coverage', 'run', verbose=verbose, exit_on_error=True) verbose_check_call('coverage', 'combine', '--append', verbose=verbose, exit_on_error=True) verbose_check_call('coverage', 'report', '--fail-under=10', verbose=verbose, exit_on_error=True) verbose_check_call('coverage', 'xml', verbose=verbose, exit_on_error=True) verbose_check_call('coverage', 'json', verbose=verbose, exit_on_error=True) cli.add_command(coverage) @click.command() def install(): """ Run pip-sync and install 'for_runners_ynh' via pip as editable. """ verbose_check_call('pip-sync', PACKAGE_ROOT / 'requirements.dev.txt') verbose_check_call('pip', 'install', '--no-deps', '-e', '.') cli.add_command(install) @click.command() def safety(): """ Run safety check against current requirements files """ verbose_check_call('safety', 'check', '-r', 'requirements.dev.txt') cli.add_command(safety) @click.command() def update(): """ Update "requirements*.txt" dependencies files """ bin_path = Path(sys.executable).parent verbose_check_call(bin_path / 'pip', 'install', '-U', 'pip') verbose_check_call(bin_path / 'pip', 'install', '-U', 'pip-tools') extra_env = dict( CUSTOM_COMPILE_COMMAND='./dev-cli.py update', ) pip_compile_base = [ bin_path / 'pip-compile', '--verbose', '--allow-unsafe', # https://pip-tools.readthedocs.io/en/latest/#deprecations '--resolver=backtracking', # https://pip-tools.readthedocs.io/en/latest/#deprecations '--upgrade', '--generate-hashes', ] # Only "prod" dependencies: verbose_check_call( *pip_compile_base, 'pyproject.toml', '--output-file', 'conf/requirements.txt', extra_env=extra_env, ) # dependencies + "dev"-optional-dependencies: verbose_check_call( *pip_compile_base, 'pyproject.toml', '--extra=dev', '--output-file', 'requirements.dev.txt', extra_env=extra_env, ) verbose_check_call(bin_path / 'safety', 'check', '-r', 'requirements.dev.txt') # Install new dependencies in current .venv: verbose_check_call(bin_path / 'pip-sync', 'requirements.dev.txt') cli.add_command(update) @click.command() def publish(): """ Build and upload this project to PyPi """ try: _run_django_test_cli() # Don't publish a broken state except SystemExit as err: assert err.code == 0, f'Exit code is not 0: {err.code}' publish_package( module=for_runners_ynh, package_path=PACKAGE_ROOT, distribution_name='for_runners_ynh', ) cli.add_command(publish) @click.command() @click.option('--color/--no-color', **OPTION_ARGS_DEFAULT_TRUE) @click.option('--verbose/--no-verbose', **OPTION_ARGS_DEFAULT_FALSE) def fix_code_style(color: bool = True, verbose: bool = False): """ Fix code style of all for_runners_ynh source code files via darker """ code_style.fix(package_root=PACKAGE_ROOT, color=color, verbose=verbose) cli.add_command(fix_code_style) @click.command() @click.option('--color/--no-color', **OPTION_ARGS_DEFAULT_TRUE) @click.option('--verbose/--no-verbose', **OPTION_ARGS_DEFAULT_FALSE) def check_code_style(color: bool = True, verbose: bool = False): """ Check code style by calling darker + flake8 """ code_style.check(package_root=PACKAGE_ROOT, color=color, verbose=verbose) cli.add_command(check_code_style) @click.command() def update_test_snapshot_files(): """ Update all test snapshot files (by remove and recreate all snapshot files) """ def iter_snapshot_files(): yield from PACKAGE_ROOT.rglob('*.snapshot.*') removed_file_count = 0 for item in iter_snapshot_files(): item.unlink() removed_file_count += 1 print(f'{removed_file_count} test snapshot files removed... run tests...') # Just recreate them by running tests: os.environ['RAISE_SNAPSHOT_ERRORS'] = '0' # Recreate snapshot files without error try: _run_django_test_cli() finally: new_files = len(list(iter_snapshot_files())) print(f'{new_files} test snapshot files created, ok.\n') cli.add_command(update_test_snapshot_files) def _run_django_test_cli(): """ Call the origin Django test manage command CLI and pass all args to it. """ os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' print('Compile YunoHost files...') result: CreateResults = create_local_test( django_settings_path=PACKAGE_ROOT / 'conf' / 'settings.py', destination=PACKAGE_ROOT / 'local_test', runserver=False, extra_replacements={ '__DEBUG_ENABLED__': '0', # "1" or "0" string '__LOG_LEVEL__': 'INFO', '__ADMIN_EMAIL__': 'foo-bar@test.tld', '__DEFAULT_FROM_EMAIL__': 'django_app@test.tld', }, ) print('Local test files created:') print(result) data_dir = str(result.data_dir_path) if data_dir not in sys.path: sys.path.insert(0, data_dir) django.setup() os.chdir(Path(for_runners_ynh.__file__).parent) test_command = DjangoTestCommand() test_command.run_from_argv(sys.argv) @click.command() # Dummy command def test(): """ Compile YunoHost files and run Django unittests """ _run_django_test_cli() cli.add_command(test) def _run_tox(): verbose_check_call(sys.executable, '-m', 'tox', *sys.argv[2:]) sys.exit(0) @click.command() # Dummy "tox" command def tox(): """ Run tox """ _run_tox() cli.add_command(tox) @click.command() def version(): """Print version and exit""" # Pseudo command, because the version always printed on every CLI call ;) sys.exit(0) cli.add_command(version) @click.command() def local_test(): """ Build a "local_test" YunoHost installation and start the Django dev. server against it. """ create_local_test( django_settings_path=PACKAGE_ROOT / 'conf' / 'settings.py', destination=PACKAGE_ROOT / 'local_test', runserver=True, extra_replacements={ '__DEBUG_ENABLED__': '1', }, ) cli.add_command(local_test) @click.command() def diffsettings(): """ Run "diffsettings" manage command against a "local_test" YunoHost installation. """ destination = PACKAGE_ROOT / 'local_test' create_local_test( django_settings_path=PACKAGE_ROOT / 'conf' / 'settings.py', destination=destination, runserver=False, extra_replacements={ '__DEBUG_ENABLED__': '1', }, ) app_path = destination / 'opt_yunohost' verbose_check_call( sys.executable, app_path / 'manage.py', 'diffsettings', cwd=app_path, ) cli.add_command(diffsettings) def main(): print_version(for_runners_ynh) print(f'{sys.argv=}') if len(sys.argv) >= 2: # Check if we just pass a command call command = sys.argv[1] if command == 'test': _run_django_test_cli() sys.exit(0) elif command == 'tox': _run_tox() sys.exit(0) print('Execute Click CLI') cli()