Skip to content

Commit

Permalink
CLI: Make loading of config lazy for improved responsiveness (#6140)
Browse files Browse the repository at this point in the history
The `VerdiContext` class, which provides the custom context of the
`verdi` commands, loads the configuration. This has a non-negligible
cost and so slows down the responsiveness of the CLI. This is especially
noticeable during tab-completion.

The `obj` custom object of the `VerdiContext` is replaced with a
subclass of `AttributeDict` that lazily populates the `config` key when
it is called with the loaded `Config` class. In addition, the defaults
of some options of the `verdi setup` command, which load a value from
the config and so require the config, are turned into partials such that
they also are lazily evaluated. These changes should give a reduction in
load time of `verdi` of the order of ~50 ms.

A test of `verdi setup` had to be updated to explicitly provide a value
for the email. This is because now the default is evaluated lazily, i.e.
when the command is actually called in the test. At this point, there is
no value for this config option and so the default is empty. Before, the
default would be evaluated as soon as `aiida.cmdline.commands.cmd_setup`
was imported, at which point an existing config would still contain
these values, binding them to the default, even if the config would be
reset afterwards before the test.
  • Loading branch information
danielhollas authored Oct 11, 2023
1 parent 5ca609b commit d533b7a
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 12 deletions.
38 changes: 31 additions & 7 deletions aiida/cmdline/groups/verdi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import base64
import difflib
import gzip
import typing as t

import click

Expand Down Expand Up @@ -34,19 +35,42 @@
)


class LazyConfigAttributeDict(AttributeDict):
"""Subclass of ``AttributeDict`` that lazily calls :meth:`aiida.manage.configuration.get_config`."""

_LAZY_KEY = 'config'

def __init__(self, ctx: click.Context, dictionary: dict[str, t.Any] | None = None):
super().__init__(dictionary)
self.ctx = ctx

def __getattr__(self, attr: str) -> t.Any:
"""Override of ``AttributeDict.__getattr__`` for lazily loading the config key.
:param attr: The attribute to return.
:returns: The value of the attribute.
:raises AttributeError: If the attribute does not correspond to an existing key.
:raises click.exceptions.UsageError: If loading of the configuration fails.
"""
if attr != self._LAZY_KEY:
return super().__getattr__(attr)

if self._LAZY_KEY not in self:
try:
self[self._LAZY_KEY] = get_config(create=True)
except ConfigurationError as exception:
self.ctx.fail(str(exception))

return self[self._LAZY_KEY]


class VerdiContext(click.Context):
"""Custom context implementation that defines the ``obj`` user object and adds the ``Config`` instance."""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

if self.obj is None:
self.obj = AttributeDict()

try:
self.obj.config = get_config(create=True)
except ConfigurationError as exception:
self.fail(str(exception))
self.obj = LazyConfigAttributeDict(self)


class VerdiCommandGroup(click.Group):
Expand Down
8 changes: 4 additions & 4 deletions aiida/cmdline/params/options/commands/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,28 +176,28 @@ def get_quicksetup_password(ctx, param, value): # pylint: disable=unused-argume

SETUP_USER_EMAIL = options.USER_EMAIL.clone(
prompt='Email Address (for sharing data)',
default=get_config_option('autofill.user.email'),
default=functools.partial(get_config_option, 'autofill.user.email'),
required=True,
cls=options.interactive.InteractiveOption
)

SETUP_USER_FIRST_NAME = options.USER_FIRST_NAME.clone(
prompt='First name',
default=get_config_option('autofill.user.first_name'),
default=functools.partial(get_config_option, 'autofill.user.first_name'),
required=True,
cls=options.interactive.InteractiveOption
)

SETUP_USER_LAST_NAME = options.USER_LAST_NAME.clone(
prompt='Last name',
default=get_config_option('autofill.user.last_name'),
default=functools.partial(get_config_option, 'autofill.user.last_name'),
required=True,
cls=options.interactive.InteractiveOption
)

SETUP_USER_INSTITUTION = options.USER_INSTITUTION.clone(
prompt='Institution',
default=get_config_option('autofill.user.institution'),
default=functools.partial(get_config_option, 'autofill.user.institution'),
required=True,
cls=options.interactive.InteractiveOption
)
Expand Down
3 changes: 2 additions & 1 deletion tests/cmdline/commands/test_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,12 @@ def test_setup_profile_uuid(self):
postgres.create_db(db_user, db_name)

profile_name = 'profile-copy'
user_email = '[email protected]'
profile_uuid = str(uuid.uuid4)
options = [
'--non-interactive', '--profile', profile_name, '--profile-uuid', profile_uuid, '--db-backend',
self.storage_backend_name, '--db-name', db_name, '--db-username', db_user, '--db-password', db_pass,
'--db-port', self.pg_test.dsn['port']
'--db-port', self.pg_test.dsn['port'], '--email', user_email
]

self.cli_runner(cmd_setup.setup, options, use_subprocess=False)
Expand Down

0 comments on commit d533b7a

Please sign in to comment.