""" The classes for storing one configuration value. """
from __future__ import annotations
from ..common.grammar_types import mixed, GrammarType
from .configuration import Configuration
from ..common.misc import as_integer
from .decorators import warnings_from_here
from .warnings import DataValidityError
import warnings
[docs]
class DangerousValue:
""" This class is used to store (encapsulate) a value, which should not be validated
- to overcame sometimes too strict enforment of the options values.
"""
[docs]
def __init__(self, value, value_type:GrammarType | None=None, validate:bool=True):
"""
Parameters
----------
value
A value to be stored
value_type
A grammar type, that the value should satisfy (commonly a mixed type).
Can be None - when the only requirement to the value is that it can be
stringified.
validate
Should be the value validated or not (e.g. the value parsed by grammar
has been already validated, so there is no need to do it again)
"""
if validate:
if value_type:
with warnings.catch_warnings():
warnings.simplefilter("error", DataValidityError)
value = value_type.convert(value)
value_type.validate(value)
else:
value = str(value)
self.value = value
self.value_type = value_type
def __call__(self):
""" Return the actual value."""
return self.value
[docs]
def write_value(self, file):
if self.value_type:
self.value_type.write(file, self.value)
else:
file.write(self.value)
[docs]
class BaseOption(Configuration):
""" A base placeholder for a leaf element of a grammar file,
both the a-value-holding ones (:class:`Option`) and
dummy ones (Dummy)
"""
[docs]
def _save_to_file(self, file, always=False, name_in_grammar=None, delimiter=''):
""" Write the name-value pair to the given file, if the value
is set. """
return self._definition.output_definition._save_to_file(file, self, always, name_in_grammar, delimiter)
[docs]
def _find_members(self, name, lower_case=True, option=None):
if self._definition.has_name(name, lower_case) and option is not False:
yield self
[docs]
def get_path(self):
return self._get_path()
[docs]
def _as_dict(self, get):
return None
[docs]
def clear(self, do_not_check_required=False, call_hooks=True, generated=True):
pass
[docs]
class Dummy(BaseOption):
[docs]
def _validate(self, why='save'):
return True
[docs]
def has_any_value(self):
return False
def __repr__(self):
return f'<DUMMY {self._definition.name}>'
[docs]
class DummyStub(Dummy):
[docs]
def _as_dict(self, get):
if not self._definition.allowed(self._container):
return None
return get(self._container[self._definition.item])
[docs]
class Option(BaseOption):
""" Class for one option (a configuration value) of SPRKKR - either to
be used as a part of InputParameters or Potential configuration.
Usage:
>>> from ase2sprkkr.sprkkr.calculator import SPRKKR
>>> calculator = SPRKKR()
>>> conf = calculator.input_parameters
>>> conf.ENERGY.ImE = 5.
>>> conf.ENERGY.ImE()
5.0
>>> conf.ENERGY.ImE.info
'Configuration value ImE'
>>> conf.ENERGY.ImE.help() # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
Configuration value ImE
<BLANKLINE>
ImE : Energy (<Real> [Ry|eV]) ≝ 0.0 (optional)
>>> conf.ENERGY.ImE.set_dangerous('1J')
>>> conf.ENERGY.ImE()
'1J'
"""
[docs]
def __init__(self, definition, container=None, value=None):
""""
Parameters
----------
definition: ValueDefinition
The value type of the option and its format (in potential and/or task file)
container:
The container, that owns the object
value: mixed
The value of the option.
"""
super().__init__(definition, container)
self._hook = None
self._definition.enrich(self)
self._value = value
[docs]
def _value_or_default(self):
d = self._definition
if d.is_generated:
return d.getter(self._container)
if hasattr(self, '_result'):
return self._result
if self._value is not None:
return self._value
value = self.default_value
if self._definition.is_repeated.is_dict and value is not None:
return { 'def' : value }
return value
def __call__(self, all_values:bool=False, unpack=True):
"""
Return the value of the option.
Parameters
----------
all_values: Control the behavior for the dict_like repeated values
(see `is_repeated` attribute of :class:`ConfigurationDefinition`).
Pass True as this argument to obtain dictionary
of all values. If False (the default) is given, only the 'wildcard' value
(i.e. the one without array index, which is used for the all values not explicitly specified)
is returned.
"""
d = self._definition
value = self._value_or_default()
if not d.is_generated and d.init_by_default and self._value is None:
self._value = self._pack_value( value )
if isinstance(value, DangerousValue) and unpack:
value = value()
if d.is_repeated.is_dict and not all_values:
value = value.get('def', self.default_value)
if unpack:
value = self._unpack_value(value)
return value
[docs]
def is_dangerous(self):
""" Return, whether the option is set to a dangerous value, i.e. a value
that bypass the validation. """
return isinstance(self._value, DangerousValue)
[docs]
def set_dangerous(self, value, index=None):
""" Set the option to a dangerous value - i.e. to a value that bypass the
type and value checks and enforcing.
However, the type of such value is still checked by the proper mixed type.
To completly bypass the check, set the value to an instance of DangerousValue
class directly.
"""
value = self._create_dangerous_value(value)
if index is not None:
self[index] = value
else:
self.set(value)
[docs]
def _create_dangerous_value(self, value):
return DangerousValue(value, self._definition.type_of_dangerous)
@property
def default_value(self):
""" Return default value for the option.
The function is here, and not in the definition, since the default value can be given
by callable, that accepts the Option as argument. This possibility is used in ase2sprkkr,
when the default values of some options are generated from the underlined Atoms object
"""
return self._definition.get_value(self)
[docs]
def set(self, value, *, unknown=None, error=None):
self._set(value, unknown=unknown, error=error)
if self._container and not error:
self._container._validate_section()
[docs]
@warnings_from_here(stacklevel=2)
def _set(self, value, *, unknown=None, error=None):
"""
Set the value of the option.
Parameters
----------
value: mixed
The new value of the option.
unknown: str or None
A dummy argument to make the method compatibile with
ase2sprkkr.sprkkr.common.configuration_containers.ConfigurationContainer.set()
error:
"""
with warnings.catch_warnings(record=True) as recorded_warnings:
d = self._definition
if d.is_generated:
return d.setter(self._container, value)
if value is None:
try:
return self.clear()
except ValueError:
if not error=='ignore':
raise
return
elif d.is_repeated.is_dict:
if isinstance(value, dict):
self.clear(do_not_check_required=value, call_hooks=False)
for k,v in value.items():
self._set_item(k, v, error)
else:
try:
self._set_item('def', value, error)
except ValueError:
for i,v in enumerate(value):
self._set_item(i + 1, v)
else:
try:
self._value = self._pack_value(value)
except ValueError:
if not error=='ignore':
raise
self._post_set()
for w in recorded_warnings:
w.message.args = (
f"During setting the value {value} to {self.get_path()}, "
f"the following warning have been issued:\n {w.message}",
*w.message.args[1:]
)
warnings.warn_explicit(
message=w.message,
category=w.category,
filename=w.filename,
lineno=w.lineno,
source=w.source,
)
[docs]
def _post_set(self):
""" Thus should be called after all modifications """
if hasattr(self,'_result'):
del self._result
if self._hook:
self._hook(self)
[docs]
def add_hook(self, hook):
self._hook = hook
[docs]
def _check_array_access(self):
""" Check, whether the option is array type (or repeated) and thus it can be accessed as array using [] """
return self._definition.check_array_acces(self)
def __setitem__(self, name, value):
""" Set an item of a numbered array. If the Option is not a numbered array, throw an Exception. """
d = self._definition
if d.is_generated:
d.setter(self._container, value, name)
return
d.check_array_access()
if not d.is_repeated.is_dict:
self()[name]=d.convert_and_validate(self, value, item=True)
self.validate(why='set')
else:
if isinstance(name, (list, tuple)):
for n in name:
self._set_item(n, value)
elif isinstance(name, slice):
try:
cnt = len(value)
step = name.step or 1
start = name.start or 1
stop = name.stop or start + step * cnt
for i,v in zip(range(start,stop,step), value):
self._set_item(i, v)
except (TypeError, ValueError):
if slice.stop is None:
raise KeyError("To get/set values in a numbered array using slice with one value, you have to specify the end index of the slice")
for n in range(name.start or 1, name.stop, name.step or 1):
self._set_item(n, value)
else:
self._set_item(name, value)
self._post_set()
[docs]
def _set_item(self, name, value, error=None):
""" Set a single item of a numbered array. For internal use - so no sanity checks """
if self._value is None:
self._value = {}
if not (self._definition.is_repeated.is_numbered.has_default and name == 'def'):
try:
name = as_integer(name)
except TypeError as e:
raise KeyError('Numbered array indexes can be only integers, lists or slices') from e
if name < 1:
raise KeyError('Numbered array indexes has to be greater than zero')
if value is None:
del self._value[name]
if not self._value:
self._value = None
else:
try:
self._value[name] = self._pack_value(value)
except ValueError:
if error!='ignore':
raise
def __getitem__(self, name):
""" Get an item of a numbered array. If the Option is not a numbered array, throw an Exception. """
d = self._definition
if d.is_generated:
return d.getter(self._container, name)
d.check_array_access()
if not d.is_repeated.is_dict:
return self()[name]
if isinstance(name, (list, tuple)):
return [ self._getitem(n) for n in name ]
elif isinstance(name, slice):
if name.stop is None:
if self._value is None:
stop = 2
else:
try:
stop = max( i for i in self._value if i != 'def' ) + 1
except ValueError:
stop = 2
name = slice(max(1, name.start or 1), stop, name.step)
return [ self._getitem(n) for n in range(name.start, name.stop, name.step or 1) ]
return self._getitem(name)
[docs]
def _getitem(self, name):
""" Get a single item from a numbered array. For internal use - so no sanity checks """
if name != 'def':
try:
name = as_integer(name)
except TypeError as e:
raise KeyError('Numbered array indexes can be only integers, lists or slices') from e
if self._value is None:
return self.default_value
if name in self._value:
out = self._value[name]
elif 'def' in self._value:
out = self._value['def']
else:
return self.default_value
return self._unpack_value(out)
[docs]
def _unpack_value(self, value):
""" Unpack potentionally dangerous values. """
if isinstance(value, DangerousValue):
value = value()
if self._definition.is_repeated.is_dict and isinstance(value, dict):
value = { i: v() if isinstance(v, DangerousValue) else v for i,v in value.items() }
return value
[docs]
def _pack_value(self, value):
""" Validate the value, if it's to be. """
if isinstance(value, DangerousValue):
""" The dangerous value is immutable, checked during its creation """
pass
else:
value = self._definition.convert_and_validate(self, value)
return value
def __hasitem__(self, name):
d = self._definition
d.check_array_access()
if not d.is_repeated.is_dict:
return name in self()
if self._value is None:
return False
value = self._unpack_value(self._value)
return name in value
[docs]
def get(self):
""" Return the value of self """
return self()
@property
def result(self):
""" Return the result value.
In some cases, the value of an option have to be translated for the output.
E.g. the site can be given as site object, but the integer index is
required in the output.
In a such case, this property can be utilized: the value of the option is
retained as is and the transformed value is stored in the result.
"""
if hasattr(self, '_result'):
if isinstance(self._result, DangerousValue):
return self._result.value
return self._result
return self(all_values=True)
@result.setter
def result(self, value):
self._result = value
[docs]
def clear_result(self):
if hasattr(self, '_result'):
del self._result
[docs]
def clear(self, do_not_check_required=False, call_hooks=True, generated=True):
""" Clear the value: set it to None """
if self._definition.is_generated:
if not generated:
return
self._definition.setter(self._container, None)
else:
if not self._definition.type.has_value:
return
if self._definition.default_value is None and not do_not_check_required and self.is_required:
raise DataValidityError(f'Option {self._get_path()} must have a value.')
self._value = None
self.clear_result()
if call_hooks:
self._post_set()
[docs]
def is_changed(self) -> bool:
""" True, if the value is set and the value differs from the default """
return self.value_and_changed()[1]
[docs]
def is_set(self) -> bool:
""" True, if the value is set (even equal to the default value) """
return self._value is not None
[docs]
def _written_value(self, always=False):
"""
Parameters
----------
always:
Skip all condition checking
Returns
-------
write value: Any
The value to be written
write: bool,
Whether to write the value or not
"""
d = self._definition
if not d.is_stored:
return None, False
if not always:
if not d.allowed(self._container):
return None, False
if not d.write_condition(self):
return None, False
if not d.type.has_value:
return None, True
if self.is_dangerous():
return self._value, self._value() is not None
value = self.result
missing,_, np = d.type.missing_value()
if np.__class__ is value.__class__ and np == value:
return value, False
if value is None or (not d.is_always_added and self.is_it_the_default_value(value)):
return value, False
return value, True
@property
def is_required(self):
r = self._definition.is_required
if not r:
return False
if callable(r):
return r(self)
return r
[docs]
def _validate(self, why='save'):
d = self._definition
if (not d.is_validated if d.is_validated is not None else d.is_generated) or \
not d.type.has_value:
return
def vali(value):
if isinstance(value, DangerousValue):
return
d.validate(self, value, why)
value = self(unpack=False, all_values=True)
if d.is_repeated.is_dict:
if value is None:
vali(value)
elif isinstance(value, DangerousValue):
return
else:
for i in value.values():
vali(i)
else:
vali(value)
@property
def name(self):
return self._definition.name
[docs]
def _as_dict(self, get):
if not self._definition.allowed(self._container):
return None
return get(self)
[docs]
def value_and_changed(self):
""" Return value and whether the value was changed
Returns
-------
value:mixed
The value of the options (return all values for 'numbered array')
changed:bool
Whether the value is the same as the default value or not
"""
d = self._definition
if d.is_generated:
return self(), False
value = self._unpack_value(self._value)
if value is not None:
return value, not self.is_it_the_default_value(value)
if d.is_repeated.is_numbered.has_default and self.default_value is not None:
return {'def' : self.default_value}, False
else:
return self.default_value, False
[docs]
def is_it_the_default_value(self, value):
""" Return, whether the given value is the default value. For
numbered array, only the wildcard value can be set and this value
have to be the same as the default. """
d = self._definition
if d.is_generated:
return True
default = self.default_value
if d.is_repeated.is_numbered.has_default:
return 'def' in value and len(value) == 1 and \
d.type.is_the_same_value(value['def'], default)
else:
return d.type.is_the_same_value(value, default)
[docs]
def has_any_value(self):
return self.result is not None
def __len__(self):
return len(self())
def __iter__(self):
return iter(self())
def __bool__(self):
return True
def __repr__(self):
if self._definition.is_generated:
return f"<Generated value {self._get_path()}>"
else:
v = self._value
o = None
if o is None and v is None:
v = self.default_value
if callable(v):
v = 'fn()'
if v is not None:
o=' (default)'
if v is None:
if o:
o='out' + o
else:
o='out'
v=''
else:
o=''
v=' = ' + str(v)
if len(v)>20:
v=f'{v[:10]}...{v[-10:]}'
type = '(generated)' if self._definition.is_generated else f'of type {self._definition.type}'
return f"<Option {self._get_path()} {type} with{o} value{v}>"
[docs]
class CustomOption(Option):
""" An user-added option (configuration value). It can be removed from the section. """
[docs]
def remove(self):
""" Remove me from my "parent" section """
self._container.remove_member(self._definition.name)
[docs]
@classmethod
def factory(cls, value_definition, type = mixed):
""" Returns factory function for the given value definition """
def create(name, section):
definition = value_definition(name, type)
definition.removable = True
return cls(definition, section)
create.grammar_type = type
return create