Source code for ase2sprkkr.bindings.tests.grammar_types.basic

""" Common GrammarTypes as numbers, strings etc. """

from ase.units import Rydberg
import datetime
import pyparsing as pp
from typing import Optional
import numpy as np
import re

from ..decorators import add_to_signature, cached_property
from ..grammar import generate_grammar, separator_grammar, \
                     replace_whitechars, optional_quote
from .grammar_type import TypedGrammarType, GrammarType, add_to_parent_validation

ppc = pp.pyparsing_common


[docs] class Number(TypedGrammarType): """ Base class for a number - descendants of this class can have minimal and/or maximal possible value. """
[docs] @add_to_signature(GrammarType.__init__) def __init__(self, min:Optional[int]=None, max:Optional[int]=None, *args, **kwargs): """ Parameters ---------- min: Minimal allowed value. max: Maximal allowed value. """ self.min = min self.max = max super().__init__(*args, **kwargs)
[docs] @add_to_parent_validation def _validate(self, value, why='set'): if self.min is not None and self.min > value: return f"A value greater that or equal to {self.min} is required, {value} have been given." if self.max is not None and self.max < value: return f"A value less than or equal to {self.max} is required, {value} have been given." return True
[docs] class FixedPointNumber(Number): numpy_type = int
[docs] def convert(self, value): if isinstance(value, float) and np.abs(int(value) - value) < 1e-15: from .warnings import SuspiciousValueWarning SuspiciousValueWarning(value, 'An attemtp to set <integer> value using an <float>. I will do a conversion for you.').warn( stacklevel=6 ) value=int(value) return super().convert(value)
[docs] @add_to_parent_validation def _validate(self, value, why='set'): if isinstance(value, float): return "A float value is not allowed for integer." return super()._validate(value, why)
[docs] class Unsigned(FixedPointNumber): """ Unsigned integer (zero is possible) """ _grammar = replace_whitechars(ppc.integer).setParseAction(lambda x:int(x[0]))
[docs] @add_to_parent_validation def _validate(self, value, why='set'): return value >= 0 or "A positive value required"
[docs] def grammar_name(self): return '<+int>'
datatype_name = 'unsigned integer'
[docs] class ObjectNumber(Unsigned): """ An abstract class, that describe an unsigned integer, that reffers to an object. User can give the object either using the object, or by the number. Descendant classes should take care of transforming the object to the resulting integer (by setting the result property of the described :class:`Option<ase2sprkkr.common.options.Option>`) The type of te object should be given by the type class property. """
[docs] def convert(self, value): if isinstance(value, self.type): return value return super().convert(value)
[docs] def _validate(self, value, why='set'): return isinstance(value, self.type) or super()._validate(value, why=why)
[docs] class Integer(FixedPointNumber): """ Signed integer """ _grammar = replace_whitechars(ppc.signed_integer).setParseAction(lambda x:int(x[0]))
[docs] def grammar_name(self): return '<int>'
[docs] class BaseBool(TypedGrammarType): """ A base type for all kind of boolean values.""" numpy_type = bool type_name = 'boolean'
[docs] class Bool(BaseBool): """ A bool type, whose value is represented by a letter (T or F) """ _grammar = (pp.CaselessKeyword('T') | pp.CaselessKeyword('F')).setParseAction( lambda x: x[0].upper() == 'T' )
[docs] def grammar_name(self): return '<T|F>'
[docs] def _string(self, val): return 'T' if val else 'F'
[docs] class Boolean(BaseBool): """ A bool type, whose value is represented by a letter (T or F) """ _items = [ 'True', 'False', '1', '0', 'yes', 'no' ] _grammar = pp.Or([pp.CaselessKeyword(i) for i in _items]).\ setParseAction( lambda x: x[0].lower() in ('true','yes','1') )
[docs] def grammar_name(self): return '<True|False|0|1|yes|no>'
[docs] def _string(self, val): return 'True' if val else 'False'
[docs] class IntBool(BaseBool): """ A bool type, whose value is represented by a letter (1 or 0) """ _grammar = (pp.CaselessKeyword('1') | pp.CaselessKeyword('0')).setParseAction( lambda x: x[0] == '1' ) _rev_grammar = _grammar.copy().setParseAction( lambda x: x[0] == '0' )
[docs] @add_to_signature(TypedGrammarType.__init__) def __init__(self, reversed=False, *args, **kwargs): """ Parameters ---------- reversed "reversed integer-boolean" returns 1 if it is False """ self.reversed = bool(reversed) super().__init__(*args, **kwargs)
[docs] def grammar_name(self): return '<1|0>'
[docs] def _string(self, val): return '1' if val != self.reversed else '0'
[docs] class Real(Number): """ A real value """ _grammar = replace_whitechars(ppc.fnumber).setParseAction(lambda x: float(x[0]))
[docs] def grammar_name(self): return '<float>'
numpy_type = float nan = None
[docs] @add_to_signature(Number.__init__) def __init__(self, nan=None ,*args, **kwargs): self.nan = nan if nan is not None: self._grammar = self._grammar | pp.Regex(nan).set_parse_action(lambda x: float('NaN')) super().__init__(*args, **kwargs)
[docs] def convert(self, value): if isinstance(value, (int, np.integer)): from .warnings import SuspiciousValueWarning SuspiciousValueWarning(value, 'An attemtp to set <float> value using an <integer>. I will do a conversion for you.').warn( stacklevel=6 ) value=float(value) return super().convert(value)
[docs] class Date(Number): """ A date value of the form 'DD.MM.YYYY' """ _grammar = pp.Regex(r'(?P<d>\d{2}).(?P<m>\d{2}).(?P<y>\d{4})').setParseAction(lambda x: datetime.date(int(x['y']), int(x['m']), int(x['d'])))
[docs] def grammar_name(self): return '<dd.mm.yyyy>'
[docs] def _string(self, val): return val.strftime("%d.%m.%Y")
numpy_type = datetime.date type_name = 'date'
[docs] class BaseRealWithUnits(Real): """ The base class for float value, which can have units append. The value is converted automatically to the base units. """ grammar_cache = {} """ The grammar for units is cached """
[docs] def _grammar_units(self, units): i = id(units) if not i in self.grammar_cache: units = pp.Or( (pp.Empty() if v is None else pp.CaselessKeyword(v)) .setParseAction(lambda x,*args, u=u: u) for v,u in units.items() ) out = Real.I.grammar() + pp.Or(units) out.setParseAction(lambda x: x[0] * x[1]) self.grammar_cache[i] = out return out return self.grammar_cache[i]
[docs] def _grammar(self, param_name): return self._grammar_units(self.units)
[docs] def _validate(self, value, why='set'): return isinstance(value, float) or "A float value required"
[docs] def grammar_name(self): return '<float>[{}]'.format("|".join(('' if i is None else i for i in self.units)))
numpy_type = float
[docs] class RealWithUnits(BaseRealWithUnits): """ A float value with user-defined units """
[docs] def __init__(self, *args, units, **kwargs): self.units = units super().__init__(*args, **kwargs)
[docs] class Energy(BaseRealWithUnits): """ The grammar type for energy. The default units are Rydberg, one can specify eV. """ units = { 'Ry' : 1., 'eV' : 1. / Rydberg, None : 1., } """ The allowed units and their conversion factors """ def __str__(self): return "Energy (<Real> [Ry|eV])"
[docs] class BaseString(TypedGrammarType): """ Base type for string grammar types """ datatype = str datatype_name = 'string'
[docs] @add_to_parent_validation def _validate(self, value, why='set'): if not why=='parse': try: self._grammar.parseString(value, True) except pp.ParseException as e: return f"Forbidden character '{e.line[e.col-1]}' in the string" return True
[docs] class String(BaseString): """ Just a string (without whitespaces and few special chars) """ _grammar = pp.Word(pp.printables,excludeChars=",;{}").setParseAction(lambda x:x[0])
[docs] def grammar_name(self): return '<str>'
[docs] class AlwaysQString(BaseString): """ Either a quoted string, or just a word (without whitespaces or special chars). Always printed with quotes. """ _grammar = (pp.Word(pp.printables, excludeChars="\",;{}") | pp.QuotedString('"')).setParseAction(lambda x:x[0])
[docs] def _validate(self, value, why='set'): if not value.__class__ is str: return "String expected" return True
[docs] def _string(self, value): value = value.replace('"', '\\"') return f'"{value}"'
[docs] def grammar_name(self): return '"<str>"'
[docs] class QString(AlwaysQString): """ Either a quoted string, or just a word (without whitespaces or special chars) """
[docs] def _string(self, value): value = str(value) if re.match(r"^[\S\"]+$", value): return value return super()._string(value)
[docs] class LineString(BaseString): """ A string, that takes all up to the end of the line """ _grammar = pp.SkipTo(pp.LineEnd() | pp.StringEnd())
[docs] def grammar_name(self): return "<str....>\n"
[docs] class Keyword(GrammarType): """ A value, that can take values from the predefined set of strings. """
[docs] def __init__(self, *keywords, aliases=None, transform='upper', quote=None, **kwargs): self.aliases = aliases or {} self.quote = quote if transform == 'upper': self.transform = lambda x: str(x).upper() elif transform == 'lower': self.transform = lambda x: str(x).lower() else: self.transform = lambda x:x if len(keywords)==1 and isinstance(keywords[0], dict): self.choices = keywords[0] keywords = self.choices.keys() else: self.choices = None self.keywords = [ self.transform(i) for i in keywords ] with generate_grammar(): self._grammar = optional_quote + pp.MatchFirst((pp.CaselessKeyword(str(i)) for i in self.keywords)).setParseAction(lambda x: self.transform(x[0])) + optional_quote super().__init__(**kwargs)
[docs] def _validate(self, value, why='set'): return value in self.keywords or "Required one of [" + "|".join(self.keywords) + "]"
[docs] def _string(self, val): if self.quote and val.__class__ == str: return f"{self.quote}{val}{self.quote}" else: return str(val)
[docs] def grammar_name(self): if len(self.keywords) == 1: return f'FixedValue({next(iter(self.keywords))})' return 'AnyOf(' + ','.join((str(i) for i in self.keywords )) + ')'
def __str__(self): return self.grammar_name()
[docs] def convert(self, value): out = self.transform(value) return self.aliases.get(out, out)
[docs] def additional_description(self, prefix=''): ad = super().additional_description(prefix) if not self.choices: return ad out = f'\n{prefix}Possible values:\n' out += '\n'.join([f"{prefix} {str(k):<10}{v}" for k,v in self.choices.items()]) if ad: out += f'\n\n{prefix}' + ad return out
is_independent_on_the_predecessor = True
[docs] def DefKeyword(default, *others, **kwargs): """ A value, that can take values from the predefined set of strings, the first one is the default value. """ if isinstance(default, dict) and len(others) == 0: def_val = next(iter(default)) else: def_val = default return Keyword(default, *others, default_value=def_val, **kwargs)
[docs] class Flag(BaseBool): """ A boolean value, which is True, if a name of the value appears in the input file. The resulting type of a Flag value is bool """ _grammar = pp.Empty().setParseAction(lambda x: True)
[docs] def grammar_name(self): return None
[docs] def str(self): return "(Flag)"
[docs] def missing_value(self): return (True, True, False)
[docs] def _validate(self, value, why='set'): return value is True or value is False or value is None or "This is Flag with no value, please set to True to be present or to False/None to not"
[docs] class BasicSeparator(GrammarType): """ Basic type for separators - fake items in input/output file, which has no value """ has_value = False
[docs] def _validate(self, value, why='set'): return 'You can not set a value to a separator'
[docs] class Separator(BasicSeparator): """ Special class for a separator inside a section. By default, it is a line of stars. """ @cached_property def _grammar(self): return separator_grammar(self.char)
[docs] def __init__(self, grammar=None, char='*', length=79, *args, **kwargs): self.char = char self.length = length if grammar: self._grammar = grammar super().__init__(*args, **kwargs)
[docs] def _grammar_name(self): return f'{self.char*4}...{self.char*4}\n'
[docs] def _string(self, val=None): return self.char * self.length
[docs] class BlankSeparator(BasicSeparator): """ Special class for a blank separator. In fact (with a delimiter) it is a blank line. """ _grammar = pp.Empty().setParseAction(lambda x: [None])
[docs] def _grammar_name(self): return ''
[docs] def _string(self, val=None): return ''
# commonly used types integer = Integer.I = Integer() """ A standard grammar type instance for (signed) integers """ unsigned = Unsigned.I = Unsigned() # NOQA: E741 """ A standard grammar type instance for unsigned integers """ boolean = Bool.I = Bool() # NOQA: E741 """ A standard grammar type instance for booleans in potential files """ flag = Flag.I = Flag() # NOQA: E741 """ A standard grammar type instance for booleans in input files """ real = Real.I = Real() # NOQA: E741 """ A standard grammar type instance for reals""" date = Date.I = Date() # NOQA: E741 """ A standard instance for the grammar type for dates """ string = String.I = String() # NOQA: E741 """ A standard grammar type instance for strings """ qstring = QString.I = QString() # NOQA: E741 """ A standard grammar type instance for quoted strings in input files """ qstring = AlwaysQString.I = AlwaysQString() # NOQA: E741 """ A standard grammar type instance for quoted strings in configuration files """ line_string = LineString.I = LineString() # NOQA: E741 """ A standard grammar type instance for one-line strings in potential files """ energy = Energy.I = Energy() # NOQA: E741 """ A standard grammar type instance for energy values (float) for potential files """ separator = Separator.I = Separator() # NOQA: E741 """ A standard grammar type instance for separators in potential files """ int_bool = IntBool.I = IntBool() # NOQA: E741 """ A standard grammar type instance for bool expressed as integer """ boolean = Boolean.I = Boolean() # NOQA: E741 """ A standard grammar type instance for True|False 0|1 yes|no boolean"""