Source code for aiida.orm.utils.builders.code

###########################################################################
# Copyright (c), The AiiDA team. All rights reserved.                     #
# This file is part of the AiiDA code.                                    #
#                                                                         #
# The code is hosted on GitHub at https://github.com/aiidateam/aiida-core #
# For further information on the license, see the LICENSE.txt file        #
# For further information please visit http://www.aiida.net               #
###########################################################################
"""Manage code objects with lazy loading of the db env"""

import enum
import pathlib

from aiida.common.utils import ErrorAccumulator
from aiida.common.warnings import warn_deprecation
from aiida.orm import InstalledCode, PortableCode

warn_deprecation('This module is deprecated. To create a new code instance, simply use the constructor.', version=3)


[docs] class CodeBuilder: """Build a code with validation of attribute combinations"""
[docs] def __init__(self, **kwargs): """Construct a new instance.""" self._err_acc = ErrorAccumulator(self.CodeValidationError) self._code_spec = {} # code_type must go first for key in ['code_type']: self.__setattr__(key, kwargs.pop(key)) # then set the rest for key, value in kwargs.items(): self.__setattr__(key, value)
[docs] def validate(self, raise_error=True): self._err_acc.run(self.validate_code_type) self._err_acc.run(self.validate_upload) self._err_acc.run(self.validate_installed) return self._err_acc.result(raise_error=self.CodeValidationError if raise_error else False)
[docs] def new(self): """Build and return a new code instance (not stored)""" from aiida.manage import get_manager # Load the profile backend if not already the case. get_manager().get_profile_storage() self.validate() # Will be used at the end to check if all keys are known (those that are not None) passed_keys = set(k for k in self._code_spec.keys() if self._code_spec[k] is not None) used = set() if self._get_and_count('code_type', used) == self.CodeType.STORE_AND_UPLOAD: code = PortableCode( filepath_executable=self._get_and_count('code_rel_path', used), filepath_files=pathlib.Path(self._get_and_count('code_folder', used)), ) else: code = InstalledCode( computer=self._get_and_count('computer', used), filepath_executable=self._get_and_count('remote_abs_path', used), ) code.label = self._get_and_count('label', used) code.description = self._get_and_count('description', used) code.default_calc_job_plugin = self._get_and_count('input_plugin', used) code.use_double_quotes = self._get_and_count('use_double_quotes', used) code.prepend_text = self._get_and_count('prepend_text', used) code.append_text = self._get_and_count('append_text', used) # Complain if there are keys that are passed but not used if passed_keys - used: raise self.CodeValidationError( f"Unknown parameters passed to the CodeBuilder: {', '.join(sorted(passed_keys - used))}" ) return code
[docs] @staticmethod def from_code(code): """Create CodeBuilder from existing code instance. See also :py:func:`~CodeBuilder.get_code_spec` """ spec = CodeBuilder.get_code_spec(code) return CodeBuilder(**spec)
[docs] @staticmethod def get_code_spec(code): """Get code attributes from existing code instance. These attributes can be used to create a new CodeBuilder:: spec = CodeBuilder.get_code_spec(old_code) builder = CodeBuilder(**spec) new_code = builder.new() """ spec = {} spec['label'] = code.label spec['description'] = code.description spec['input_plugin'] = code.default_calc_job_plugin spec['use_double_quotes'] = code.use_double_quotes spec['prepend_text'] = code.prepend_text spec['append_text'] = code.append_text if isinstance(code, PortableCode): spec['code_type'] = CodeBuilder.CodeType.STORE_AND_UPLOAD spec['code_folder'] = code.get_code_folder() spec['code_rel_path'] = code.get_code_rel_path() else: spec['code_type'] = CodeBuilder.CodeType.ON_COMPUTER spec['computer'] = code.computer spec['remote_abs_path'] = str(code.get_executable()) return spec
[docs] def __getattr__(self, key): """Access code attributes used to build the code""" if not key.startswith('_'): try: return self._code_spec[key] except KeyError: raise KeyError(f"Attribute '{key}' not set") return None
[docs] def _get(self, key): """Return a spec, or None if not defined :param key: name of a code spec """ return self._code_spec.get(key)
[docs] def _get_and_count(self, key, used): """Return a spec, or raise if not defined. Moreover, add the key to the 'used' dict. :param key: name of a code spec :param used: should be a set of keys that you want to track. ``key`` will be added to this set if the value exists in the spec and can be retrieved. """ retval = self.__getattr__(key) # I first get a retval, so if I get an exception, I don't add it to the 'used' set used.add(key) return retval
[docs] def __setattr__(self, key, value): if not key.startswith('_'): self._set_code_attr(key, value) super().__setattr__(key, value)
[docs] def _set_code_attr(self, key, value): """Set a code attribute, if it passes validation. Checks compatibility with other code attributes. """ if key == 'description' and value is None: value = '' backup = self._code_spec.copy() self._code_spec[key] = value success, _ = self.validate(raise_error=False) if not success: self._code_spec = backup self.validate()
[docs] def validate_code_type(self): """Make sure the code type is set correctly""" if self._get('code_type') and self.code_type not in self.CodeType: raise self.CodeValidationError( f'invalid code type: must be one of {list(self.CodeType)}, not {self.code_type}' )
[docs] def validate_upload(self): """If the code is stored and uploaded, catch invalid on-computer attributes""" messages = [] if self.is_local(): if self._get('computer'): messages.append('invalid option for store-and-upload code: "computer"') if self._get('remote_abs_path'): messages.append('invalid option for store-and-upload code: "remote_abs_path"') if messages: raise self.CodeValidationError(f'{messages}')
[docs] def validate_installed(self): """If the code is on-computer, catch invalid store-and-upload attributes""" messages = [] if self._get('code_type') == self.CodeType.ON_COMPUTER: if self._get('code_folder'): messages.append('invalid options for on-computer code: "code_folder"') if self._get('code_rel_path'): messages.append('invalid options for on-computer code: "code_rel_path"') if messages: raise self.CodeValidationError(f'{messages}')
[docs] class CodeValidationError(ValueError): """A CodeBuilder instance may raise this * when asked to instanciate a code with missing or invalid code attributes * when asked for a code attibute that has not been set yet """
[docs] def __init__(self, msg): super().__init__() self.msg = msg
[docs] def __str__(self): return self.msg
[docs] def __repr__(self): return f'<CodeValidationError: {self}>'
[docs] def is_local(self): """Analogous to Code.is_local()""" return self.__getattr__('code_type') == self.CodeType.STORE_AND_UPLOAD
[docs] class CodeType(enum.Enum): STORE_AND_UPLOAD = 'store in the db and upload' ON_COMPUTER = 'on computer'