Skip to content

Commit 64111ab

Browse files
danielhollassphuber
authored andcommitted
CLI: Make loading of config lazy
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.
1 parent 5ca609b commit 64111ab

3 files changed

Lines changed: 34 additions & 12 deletions

File tree

‎aiida/cmdline/groups/verdi.py‎

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,40 @@
3434
)
3535

3636

37+
class LazyConfigAttributeDict(AttributeDict):
38+
"""We want to avoid needlesly loading user config on every verdi invocation"""
39+
40+
_LAZY_KEY = 'config'
41+
42+
def __init__(self, ctx, dictionary=None):
43+
super().__init__(dictionary)
44+
# We need click context so we can stop in case we fail to parse the config
45+
self.ctx = ctx
46+
47+
def __getattr__(self, attr):
48+
"""Override of AttributeDict.__getattr__ for lazy loading the config key.
49+
50+
Stops if config file cannot be parsed.
51+
:raises AttributeError: if the attribute does not correspond to an existing key.
52+
"""
53+
if attr != self._LAZY_KEY:
54+
return super().__getattr__(attr)
55+
56+
if self._LAZY_KEY not in self:
57+
try:
58+
self[self._LAZY_KEY] = get_config(create=True)
59+
except ConfigurationError as exception:
60+
self.ctx.fail(str(exception))
61+
return self[self._LAZY_KEY]
62+
63+
3764
class VerdiContext(click.Context):
3865
"""Custom context implementation that defines the ``obj`` user object and adds the ``Config`` instance."""
3966

4067
def __init__(self, *args, **kwargs):
4168
super().__init__(*args, **kwargs)
42-
4369
if self.obj is None:
44-
self.obj = AttributeDict()
45-
46-
try:
47-
self.obj.config = get_config(create=True)
48-
except ConfigurationError as exception:
49-
self.fail(str(exception))
70+
self.obj = LazyConfigAttributeDict(self)
5071

5172

5273
class VerdiCommandGroup(click.Group):

‎aiida/cmdline/params/options/commands/setup.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,28 +176,28 @@ def get_quicksetup_password(ctx, param, value): # pylint: disable=unused-argume
176176

177177
SETUP_USER_EMAIL = options.USER_EMAIL.clone(
178178
prompt='Email Address (for sharing data)',
179-
default=get_config_option('autofill.user.email'),
179+
default=functools.partial(get_config_option, 'autofill.user.email'),
180180
required=True,
181181
cls=options.interactive.InteractiveOption
182182
)
183183

184184
SETUP_USER_FIRST_NAME = options.USER_FIRST_NAME.clone(
185185
prompt='First name',
186-
default=get_config_option('autofill.user.first_name'),
186+
default=functools.partial(get_config_option, 'autofill.user.first_name'),
187187
required=True,
188188
cls=options.interactive.InteractiveOption
189189
)
190190

191191
SETUP_USER_LAST_NAME = options.USER_LAST_NAME.clone(
192192
prompt='Last name',
193-
default=get_config_option('autofill.user.last_name'),
193+
default=functools.partial(get_config_option, 'autofill.user.last_name'),
194194
required=True,
195195
cls=options.interactive.InteractiveOption
196196
)
197197

198198
SETUP_USER_INSTITUTION = options.USER_INSTITUTION.clone(
199199
prompt='Institution',
200-
default=get_config_option('autofill.user.institution'),
200+
default=functools.partial(get_config_option, 'autofill.user.institution'),
201201
required=True,
202202
cls=options.interactive.InteractiveOption
203203
)

‎tests/cmdline/commands/test_setup.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,12 @@ def test_setup_profile_uuid(self):
205205
postgres.create_db(db_user, db_name)
206206

207207
profile_name = 'profile-copy'
208+
user_email = 'some@email.com'
208209
profile_uuid = str(uuid.uuid4)
209210
options = [
210211
'--non-interactive', '--profile', profile_name, '--profile-uuid', profile_uuid, '--db-backend',
211212
self.storage_backend_name, '--db-name', db_name, '--db-username', db_user, '--db-password', db_pass,
212-
'--db-port', self.pg_test.dsn['port']
213+
'--db-port', self.pg_test.dsn['port'], '--email', user_email
213214
]
214215

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

0 commit comments

Comments
 (0)