2023-11-09 20:37:13 +01:00
|
|
|
"""
|
|
|
|
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'
|
2023-11-26 13:06:24 +01:00
|
|
|
verbose_check_call(
|
|
|
|
sys.executable,
|
|
|
|
app_path / 'manage.py',
|
|
|
|
'diffsettings',
|
|
|
|
cwd=app_path,
|
|
|
|
)
|
2023-11-09 20:37:13 +01:00
|
|
|
|
|
|
|
|
|
|
|
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()
|