django-for-runners_ynh/for_runners_ynh/cli/dev.py
2023-11-26 21:01:26 +01:00

364 lines
9.4 KiB
Python

"""
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()