diff --git a/aiida/backends/sqlalchemy/manage.py b/aiida/backends/sqlalchemy/manage.py index d593b6bb7e..1538a1b9e1 100755 --- a/aiida/backends/sqlalchemy/manage.py +++ b/aiida/backends/sqlalchemy/manage.py @@ -9,10 +9,10 @@ # For further information please visit http://www.aiida.net # ########################################################################### """Simple wrapper around the alembic command line tool that first loads an AiiDA profile.""" - import alembic import click +from aiida.cmdline import is_verbose from aiida.cmdline.params import options @@ -51,18 +51,18 @@ def alembic_revision(message): @alembic_cli.command('current') -@options.VERBOSE() -def alembic_current(verbose): +@options.VERBOSITY() +def alembic_current(): """Show the current revision.""" - execute_alembic_command('current', verbose=verbose) + execute_alembic_command('current', verbose=is_verbose()) @alembic_cli.command('history') @click.option('-r', '--rev-range') -@options.VERBOSE() -def alembic_history(rev_range, verbose): +@options.VERBOSITY() +def alembic_history(rev_range): """Show the history for the given revision range.""" - execute_alembic_command('history', rev_range=rev_range, verbose=verbose) + execute_alembic_command('history', rev_range=rev_range, verbose=is_verbose()) @alembic_cli.command('upgrade') diff --git a/aiida/cmdline/__init__.py b/aiida/cmdline/__init__.py index b997337142..c239c20bdc 100644 --- a/aiida/cmdline/__init__.py +++ b/aiida/cmdline/__init__.py @@ -46,6 +46,7 @@ 'WorkflowParamType', 'dbenv', 'format_call_graph', + 'is_verbose', 'only_if_daemon_running', 'with_dbenv', ) diff --git a/aiida/cmdline/commands/cmd_archive.py b/aiida/cmdline/commands/cmd_archive.py index 88ce38548b..84c967c8f2 100644 --- a/aiida/cmdline/commands/cmd_archive.py +++ b/aiida/cmdline/commands/cmd_archive.py @@ -10,6 +10,7 @@ # pylint: disable=too-many-arguments,import-error,too-many-locals,broad-except """`verdi archive` command.""" from enum import Enum +import logging from typing import List, Tuple import traceback import urllib.request @@ -22,6 +23,7 @@ from aiida.cmdline.params.types import GroupParamType, PathOrUrl from aiida.cmdline.utils import decorators, echo from aiida.common.links import GraphTraversalRules +from aiida.common.log import AIIDA_LOGGER EXTRAS_MODE_EXISTING = ['keep_existing', 'update_existing', 'mirror', 'none', 'ask'] EXTRAS_MODE_NEW = ['import', 'none'] @@ -82,13 +84,6 @@ def inspect(archive, version, meta_data): type=click.Choice(['zip', 'zip-uncompressed', 'zip-lowmemory', 'tar.gz', 'null']), ) @options.FORCE(help='Overwrite output file if it already exists.') -@click.option( - '-v', - '--verbosity', - default='INFO', - type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'CRITICAL']), - help='Control the verbosity of console logging' -) @options.graph_traversal_rules(GraphTraversalRules.EXPORT.value) @click.option( '--include-logs/--exclude-logs', @@ -113,7 +108,7 @@ def inspect(archive, version, meta_data): @decorators.with_dbenv() def create( output_file, codes, computers, groups, nodes, archive_format, force, input_calc_forward, input_work_forward, - create_backward, return_backward, call_calc_backward, call_work_backward, include_comments, include_logs, verbosity + create_backward, return_backward, call_calc_backward, call_work_backward, include_comments, include_logs ): """ Export subsets of the provenance graph to file for sharing. @@ -127,7 +122,7 @@ def create( # pylint: disable=too-many-branches from aiida.common.log import override_log_formatter_context from aiida.common.progress_reporter import set_progress_bar_tqdm, set_progress_reporter - from aiida.tools.importexport import export, ExportFileFormat, EXPORT_LOGGER + from aiida.tools.importexport import export, ExportFileFormat from aiida.tools.importexport.common.exceptions import ArchiveExportError entities = [] @@ -170,11 +165,10 @@ def create( elif archive_format == 'null': export_format = 'null' - if verbosity in ['DEBUG', 'INFO']: - set_progress_bar_tqdm(leave=(verbosity == 'DEBUG')) + if AIIDA_LOGGER.level <= logging.INFO: + set_progress_bar_tqdm(leave=(AIIDA_LOGGER.level == logging.DEBUG)) else: set_progress_reporter(None) - EXPORT_LOGGER.setLevel(verbosity) try: with override_log_formatter_context('%(message)s'): @@ -202,18 +196,12 @@ def create( # version inside the function when needed. help='Archive format version to migrate to (defaults to latest version).', ) -@click.option( - '--verbosity', - default='INFO', - type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'CRITICAL']), - help='Control the verbosity of console logging' -) -def migrate(input_file, output_file, force, in_place, archive_format, version, verbosity): +def migrate(input_file, output_file, force, in_place, archive_format, version): """Migrate an export archive to a more recent format version.""" from aiida.common.log import override_log_formatter_context from aiida.common.progress_reporter import set_progress_bar_tqdm, set_progress_reporter from aiida.tools.importexport import detect_archive_type, EXPORT_VERSION - from aiida.tools.importexport.archive.migrators import get_migrator, MIGRATE_LOGGER + from aiida.tools.importexport.archive.migrators import get_migrator if in_place: if output_file: @@ -225,11 +213,10 @@ def migrate(input_file, output_file, force, in_place, archive_format, version, v 'no output file specified. Please add --in-place flag if you would like to migrate in place.' ) - if verbosity in ['DEBUG', 'INFO']: - set_progress_bar_tqdm(leave=(verbosity == 'DEBUG')) + if AIIDA_LOGGER.level <= logging.INFO: + set_progress_bar_tqdm(leave=(AIIDA_LOGGER.level == logging.DEBUG)) else: set_progress_reporter(None) - MIGRATE_LOGGER.setLevel(verbosity) if version is None: version = EXPORT_VERSION @@ -241,15 +228,14 @@ def migrate(input_file, output_file, force, in_place, archive_format, version, v with override_log_formatter_context('%(message)s'): migrator.migrate(version, output_file, force=force, out_compression=archive_format) except Exception as error: # pylint: disable=broad-except - if verbosity == 'DEBUG': + if AIIDA_LOGGER.level <= logging.DEBUG: raise echo.echo_critical( 'failed to migrate the archive file (use `--verbosity DEBUG` to see traceback): ' f'{error.__class__.__name__}:{error}' ) - if verbosity in ['DEBUG', 'INFO']: - echo.echo_success(f'migrated the archive to version {version}') + echo.echo_success(f'migrated the archive to version {version}') class ExtrasImportCode(Enum): @@ -313,19 +299,11 @@ class ExtrasImportCode(Enum): show_default=True, help='Force migration of archive file archives, if needed.' ) -@click.option( - '-v', - '--verbosity', - default='INFO', - type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'CRITICAL']), - help='Control the verbosity of console logging' -) @options.NON_INTERACTIVE() @decorators.with_dbenv() @click.pass_context def import_archive( - ctx, archives, webpages, group, extras_mode_existing, extras_mode_new, comment_mode, migration, non_interactive, - verbosity + ctx, archives, webpages, group, extras_mode_existing, extras_mode_new, comment_mode, migration, non_interactive ): """Import data from an AiiDA archive file. @@ -334,15 +312,11 @@ def import_archive( # pylint: disable=unused-argument from aiida.common.log import override_log_formatter_context from aiida.common.progress_reporter import set_progress_bar_tqdm, set_progress_reporter - from aiida.tools.importexport.dbimport.utils import IMPORT_LOGGER - from aiida.tools.importexport.archive.migrators import MIGRATE_LOGGER - if verbosity in ['DEBUG', 'INFO']: - set_progress_bar_tqdm(leave=(verbosity == 'DEBUG')) + if AIIDA_LOGGER.level <= logging.INFO: + set_progress_bar_tqdm(leave=(AIIDA_LOGGER.level == logging.DEBUG)) else: set_progress_reporter(None) - IMPORT_LOGGER.setLevel(verbosity) - MIGRATE_LOGGER.setLevel(verbosity) all_archives = _gather_imports(archives, webpages) diff --git a/aiida/cmdline/commands/cmd_code.py b/aiida/cmdline/commands/cmd_code.py index ee1662e1d1..a66212922b 100644 --- a/aiida/cmdline/commands/cmd_code.py +++ b/aiida/cmdline/commands/cmd_code.py @@ -9,7 +9,6 @@ ########################################################################### """`verdi code` command.""" from functools import partial -import logging import click import tabulate @@ -144,11 +143,11 @@ def code_duplicate(ctx, code, non_interactive, **kwargs): @verdi_code.command() @arguments.CODE() -@options.VERBOSE() @with_dbenv() -def show(code, verbose): +def show(code): """Display detailed information for a code.""" from aiida.repository import FileType + from aiida.cmdline import is_verbose table = [] table.append(['PK', code.pk]) @@ -174,7 +173,7 @@ def show(code, verbose): table.append(['Prepend text', code.get_prepend_text()]) table.append(['Append text', code.get_append_text()]) - if verbose: + if is_verbose(): table.append(['Calculations', len(code.get_outgoing().all())]) echo.echo(tabulate.tabulate(table)) @@ -182,20 +181,16 @@ def show(code, verbose): @verdi_code.command() @arguments.CODES() -@options.VERBOSE() @options.DRY_RUN() @options.FORCE() @with_dbenv() -def delete(codes, verbose, dry_run, force): +def delete(codes, dry_run, force): """Delete a code. Note that codes are part of the data provenance, and deleting a code will delete all calculations using it. """ from aiida.common.log import override_log_formatter_context - from aiida.tools import delete_nodes, DELETE_LOGGER - - verbosity = logging.DEBUG if verbose else logging.INFO - DELETE_LOGGER.setLevel(verbosity) + from aiida.tools import delete_nodes node_pks_to_delete = [code.pk for code in codes] diff --git a/aiida/cmdline/commands/cmd_database.py b/aiida/cmdline/commands/cmd_database.py index 35d674f3e9..c70aeba485 100644 --- a/aiida/cmdline/commands/cmd_database.py +++ b/aiida/cmdline/commands/cmd_database.py @@ -197,28 +197,28 @@ def detect_invalid_nodes(): @verdi_database.command('summary') -@options.VERBOSE() -def database_summary(verbose): +def database_summary(): """Summarise the entities in the database.""" + from aiida.cmdline import is_verbose from aiida.orm import QueryBuilder, Node, Group, Computer, Comment, Log, User data = {} # User query_user = QueryBuilder().append(User, project=['email']) data['Users'] = {'count': query_user.count()} - if verbose: + if is_verbose(): data['Users']['emails'] = query_user.distinct().all(flat=True) # Computer query_comp = QueryBuilder().append(Computer, project=['label']) data['Computers'] = {'count': query_comp.count()} - if verbose: + if is_verbose(): data['Computers']['labels'] = query_comp.distinct().all(flat=True) # Node count = QueryBuilder().append(Node).count() data['Nodes'] = {'count': count} - if verbose: + if is_verbose(): node_types = QueryBuilder().append(Node, project=['node_type']).distinct().all(flat=True) data['Nodes']['node_types'] = node_types process_types = QueryBuilder().append(Node, project=['process_type']).distinct().all(flat=True) @@ -227,7 +227,7 @@ def database_summary(verbose): # Group query_group = QueryBuilder().append(Group, project=['type_string']) data['Groups'] = {'count': query_group.count()} - if verbose: + if is_verbose(): data['Groups']['type_strings'] = query_group.distinct().all(flat=True) # Comment diff --git a/aiida/cmdline/commands/cmd_devel.py b/aiida/cmdline/commands/cmd_devel.py index 06f4e18f03..21d127c752 100644 --- a/aiida/cmdline/commands/cmd_devel.py +++ b/aiida/cmdline/commands/cmd_devel.py @@ -11,7 +11,6 @@ import sys from aiida.cmdline.commands.cmd_verdi import verdi -from aiida.cmdline.params import options from aiida.cmdline.utils import decorators, echo @@ -21,8 +20,7 @@ def verdi_devel(): @verdi_devel.command('check-load-time') -@options.VERBOSE() -def devel_check_load_time(verbose): +def devel_check_load_time(): """Check for common indicators that slowdown `verdi`. Check for environment properties that negatively affect the responsiveness of the `verdi` command line interface. @@ -37,8 +35,7 @@ def devel_check_load_time(verbose): loaded_aiida_modules = [key for key in sys.modules if key.startswith('aiida.')] aiida_modules_str = '\n- '.join(sorted(loaded_aiida_modules)) - if verbose: - echo.echo(f'aiida modules loaded:\n- {aiida_modules_str}') + echo.echo_info(f'aiida modules loaded:\n- {aiida_modules_str}') manager = get_manager() diff --git a/aiida/cmdline/commands/cmd_group.py b/aiida/cmdline/commands/cmd_group.py index ab01f33140..46d0b86339 100644 --- a/aiida/cmdline/commands/cmd_group.py +++ b/aiida/cmdline/commands/cmd_group.py @@ -8,7 +8,6 @@ # For further information please visit http://www.aiida.net # ########################################################################### """`verdi group` commands""" -import logging import click from aiida.common.exceptions import UniquenessError @@ -93,17 +92,13 @@ def group_remove_nodes(group, nodes, clear, force): ) @options.graph_traversal_rules(GraphTraversalRules.DELETE.value) @options.DRY_RUN() -@options.VERBOSE() @with_dbenv() -def group_delete(group, delete_nodes, dry_run, force, verbose, **traversal_rules): +def group_delete(group, delete_nodes, dry_run, force, **traversal_rules): """Delete a group and (optionally) the nodes it contains.""" from aiida.common.log import override_log_formatter_context - from aiida.tools import delete_group_nodes, DELETE_LOGGER + from aiida.tools import delete_group_nodes from aiida import orm - verbosity = logging.DEBUG if verbose else logging.INFO - DELETE_LOGGER.setLevel(verbosity) - if not (force or dry_run): click.confirm(f'Are you sure you want to delete {group}?', abort=True) elif dry_run: diff --git a/aiida/cmdline/commands/cmd_node.py b/aiida/cmdline/commands/cmd_node.py index f4e11d332e..9be18aff3e 100644 --- a/aiida/cmdline/commands/cmd_node.py +++ b/aiida/cmdline/commands/cmd_node.py @@ -8,7 +8,6 @@ # For further information please visit http://www.aiida.net # ########################################################################### """`verdi node` command.""" -import logging import shutil import pathlib @@ -280,12 +279,11 @@ def extras(nodes, keys, fmt, identifier, raw): @verdi_node.command('delete') @click.argument('identifier', nargs=-1, metavar='NODES') -@options.VERBOSE() @options.DRY_RUN() @options.FORCE() @options.graph_traversal_rules(GraphTraversalRules.DELETE.value) @with_dbenv() -def node_delete(identifier, dry_run, verbose, force, **traversal_rules): +def node_delete(identifier, dry_run, force, **traversal_rules): """Delete nodes from the provenance graph. This will not only delete the nodes explicitly provided via the command line, but will also include @@ -294,10 +292,7 @@ def node_delete(identifier, dry_run, verbose, force, **traversal_rules): """ from aiida.common.log import override_log_formatter_context from aiida.orm.utils.loaders import NodeEntityLoader - from aiida.tools import delete_nodes, DELETE_LOGGER - - verbosity = logging.DEBUG if verbose else logging.INFO - DELETE_LOGGER.setLevel(verbosity) + from aiida.tools import delete_nodes pks = [] diff --git a/aiida/cmdline/params/options/__init__.py b/aiida/cmdline/params/options/__init__.py index 65f2992601..a2c585fc5b 100644 --- a/aiida/cmdline/params/options/__init__.py +++ b/aiida/cmdline/params/options/__init__.py @@ -104,7 +104,6 @@ 'USER_FIRST_NAME', 'USER_INSTITUTION', 'USER_LAST_NAME', - 'VERBOSE', 'VERBOSITY', 'VISUALIZATION_FORMAT', 'WAIT', diff --git a/aiida/cmdline/params/options/main.py b/aiida/cmdline/params/options/main.py index 4a74905415..861b3af5f9 100644 --- a/aiida/cmdline/params/options/main.py +++ b/aiida/cmdline/params/options/main.py @@ -30,8 +30,8 @@ 'OLDER_THAN', 'ORDER_BY', 'ORDER_DIRECTION', 'PAST_DAYS', 'PAUSED', 'PORT', 'PREPEND_TEXT', 'PRINT_TRACEBACK', 'PROCESS_LABEL', 'PROCESS_STATE', 'PROFILE', 'PROFILE_ONLY_CONFIG', 'PROFILE_SET_DEFAULT', 'PROJECT', 'RAW', 'REPOSITORY_PATH', 'SCHEDULER', 'SILENT', 'TIMEOUT', 'TRAJECTORY_INDEX', 'TRANSPORT', 'TRAVERSAL_RULE_HELP_STRING', - 'TYPE_STRING', 'USER', 'USER_EMAIL', 'USER_FIRST_NAME', 'USER_INSTITUTION', 'USER_LAST_NAME', 'VERBOSE', - 'VERBOSITY', 'VISUALIZATION_FORMAT', 'WAIT', 'WITH_ELEMENTS', 'WITH_ELEMENTS_EXCLUSIVE', 'active_process_states', + 'TYPE_STRING', 'USER', 'USER_EMAIL', 'USER_FIRST_NAME', 'USER_INSTITUTION', 'USER_LAST_NAME', 'VERBOSITY', + 'VISUALIZATION_FORMAT', 'WAIT', 'WITH_ELEMENTS', 'WITH_ELEMENTS_EXCLUSIVE', 'active_process_states', 'graph_traversal_rules', 'valid_calc_job_states', 'valid_process_states' ) @@ -553,8 +553,6 @@ def set_log_level(ctx, __, value): FREQUENCY = OverridableOption('-F', '--frequency', 'frequency', type=click.INT) -VERBOSE = OverridableOption('-v', '--verbose', is_flag=True, default=False, help='Be more verbose in printing output.') - TIMEOUT = OverridableOption( '-t', '--timeout', diff --git a/aiida/cmdline/utils/__init__.py b/aiida/cmdline/utils/__init__.py index 9562427ead..e7c4eff64f 100644 --- a/aiida/cmdline/utils/__init__.py +++ b/aiida/cmdline/utils/__init__.py @@ -16,11 +16,13 @@ # pylint: disable=wildcard-import from .ascii_vis import * +from .common import * from .decorators import * __all__ = ( 'dbenv', 'format_call_graph', + 'is_verbose', 'only_if_daemon_running', 'with_dbenv', ) diff --git a/aiida/cmdline/utils/common.py b/aiida/cmdline/utils/common.py index 4e22a195d4..f27d04096c 100644 --- a/aiida/cmdline/utils/common.py +++ b/aiida/cmdline/utils/common.py @@ -8,6 +8,7 @@ # For further information please visit http://www.aiida.net # ########################################################################### """Common utility functions for command line commands.""" +import logging import os import sys @@ -15,6 +16,20 @@ from . import echo +__all__ = ('is_verbose',) + + +def is_verbose(): + """Return whether the configured logging verbosity is considered verbose, i.e., equal or lower to ``INFO`` level. + + .. note:: This checks the effective logging level that is set on the ``CMDLINE_LOGGER``. This means that it will + consider the logging level set on the parent ``AIIDA_LOGGER`` if not explicitly set on itself. The level of the + main logger can be manipulated from the command line through the ``VERBOSITY`` option that is available for all + commands. + + """ + return echo.CMDLINE_LOGGER.getEffectiveLevel() <= logging.INFO + def get_env_with_venv_bin(): """Create a clone of the current running environment with the AIIDA_PATH variable set directory of the config.""" diff --git a/tests/cmdline/commands/test_database.py b/tests/cmdline/commands/test_database.py index 019ab40737..e5d79167b2 100644 --- a/tests/cmdline/commands/test_database.py +++ b/tests/cmdline/commands/test_database.py @@ -182,9 +182,9 @@ def tests_database_version(run_cli_command, manager): @pytest.mark.usefixtures('clear_database_before_test') def tests_database_summary(aiida_localhost, run_cli_command): - """Test the ``verdi database summary -v`` command.""" + """Test the ``verdi database summary`` command with the ``-verbosity`` option.""" from aiida import orm node = orm.Dict().store() - result = run_cli_command(cmd_database.database_summary, ['--verbose']) + result = run_cli_command(cmd_database.database_summary, ['--verbosity', 'info']) assert aiida_localhost.label in result.output assert node.node_type in result.output diff --git a/tests/cmdline/commands/test_node.py b/tests/cmdline/commands/test_node.py index f651f603b7..29b0712823 100644 --- a/tests/cmdline/commands/test_node.py +++ b/tests/cmdline/commands/test_node.py @@ -16,6 +16,7 @@ import gzip from click.testing import CliRunner +import pytest from aiida import orm from aiida.backends.testbase import AiidaTestCase @@ -597,55 +598,37 @@ def test_rehash_invalid_entry_point(self): self.assertIsNotNone(result.exception) -class TestVerdiDelete(AiidaTestCase): +@pytest.mark.parametrize( + 'options', ( + ['--verbosity', 'info'], + ['--verbosity', 'info', '--force'], + ['--create-forward'], + ['--call-calc-forward'], + ['--call-work-forward'], + ['--force'], + ) +) +@pytest.mark.usefixtures('clear_database_before_test') +def test_node_delete_basics(run_cli_command, options): """ - Tests for the ``verdi node delete`` command. - These test do not test the delete functionality, just that the command internal - logic does not create any problems before the call to the function. - For the actual functionality, see: - * source: manage.database.delete.nodes.py - * test: backends.tests.test_nodes.py + Testing the correct translation for the `--force` and `--verbosity` options. + This just checks that the calls do not except and that in all cases with the + force flag there is no messages. """ + from aiida.common.exceptions import NotExistent - def setUp(self): - self.cli_runner = CliRunner() + node = orm.Data().store() + pk = node.pk - def test_basics(self): - """ - Testing the correct translation for the `--force` and `--verbose` options. - This just checks that the calls do not except and that in all cases with the - force flag there is no messages. - """ - from aiida.common.exceptions import NotExistent + run_cli_command(cmd_node.node_delete, options + [str(pk), '--dry-run']) - newnode = orm.Data().store() - newnodepk = newnode.pk - options_list = [] - options_list.append(['--create-forward']) - options_list.append(['--call-calc-forward']) - options_list.append(['--call-work-forward']) - options_list.append(['--force']) - options_list.append(['--verbose']) - options_list.append(['--verbose', '--force']) - - for options in options_list: - run_options = [str(newnodepk)] - run_options.append('--dry-run') - for an_option in options: - run_options.append(an_option) - result = self.cli_runner.invoke(cmd_node.node_delete, run_options) - self.assertClickResultNoException(result) - - # To delete the created node - run_options = [str(newnodepk)] - run_options.append('--force') - result = self.cli_runner.invoke(cmd_node.node_delete, run_options) - self.assertClickResultNoException(result) + # To delete the created node + run_cli_command(cmd_node.node_delete, [str(pk), '--force']) - with self.assertRaises(NotExistent): - orm.load_node(newnodepk) + with pytest.raises(NotExistent): + orm.load_node(pk) - def test_missing_pk(self): - """Check that no exception is raised when a non-existent pk is given (just warns).""" - result = self.cli_runner.invoke(cmd_node.node_delete, ['999']) - self.assertClickResultNoException(result) + +def test_node_delete_missing_pk(run_cli_command): + """Check that no exception is raised when a non-existent pk is given (just warns).""" + run_cli_command(cmd_node.node_delete, ['999'])