""" CLI for development """ import logging import sys from pathlib import Path 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_yunohost_integration.local_test import 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) ) 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='Project Homepage: https://github.com/YunoHost-Apps/django-for-runners_ynh', ) 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=30', 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 """ _run_unittest_cli(verbose=False, exit_after_run=False) # Don't publish a broken state 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: _run_unittest_cli( extra_env=dict( RAISE_SNAPSHOT_ERRORS='0', # Recreate snapshot files without error ), verbose=False, exit_after_run=False, ) 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_unittest_cli(extra_env=None, verbose=True, exit_after_run=True): """ Call the origin unittest CLI and pass all args to it. """ if extra_env is None: extra_env = dict() extra_env.update( dict( PYTHONUNBUFFERED='1', PYTHONWARNINGS='always', ) ) args = sys.argv[2:] if not args: if verbose: args = ('--verbose', '--locals', '--buffer') else: args = ('--locals', '--buffer') verbose_check_call( sys.executable, '-m', 'unittest', *args, timeout=15 * 60, extra_env=extra_env, ) if exit_after_run: sys.exit(0) @click.command() # Dummy command def test(): """ Run unittests """ _run_unittest_cli() # TODO: Replace pytest with normal Django unittests: # 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() # TODO: 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) @click.command() def pytest(): """ Run tests via "pytest" """ verbose_check_call(sys.executable, '-m', 'pytest', *sys.argv[2:], cwd=PACKAGE_ROOT) cli.add_command(pytest) def main(): print_version(for_runners_ynh) if len(sys.argv) >= 2: # Check if we just pass a command call command = sys.argv[1] if command == 'test': _run_unittest_cli() elif command == 'tox': _run_tox() # Execute Click CLI: cli()