Source code for ae.literal

"""
literal type detection and evaluation
=====================================

a number, calendar date or other none-text-value gets represented by a literal string, if entered as user input or
has to be stored, e.g. as :ref:`configuration variable <config-variables>`.

the :class:`Literal` class implemented by this portion converts such a
:ref:`evaluable literal string <evaluable-literal-formats>` into the corresponding value (and type).
"""
import datetime
from typing import Any, Callable, Optional, Tuple, Type

from ae.base import DEF_ENCODE_ERRORS, UNSET                                        # type: ignore
from ae.parse_date import parse_date                                                # type: ignore
from ae.dynamicod import try_call, try_eval, try_exec                               # type: ignore


__version__ = '0.3.34'


BEG_CHARS = "([{'\""
END_CHARS = ")]}'\""


[docs]def evaluable_literal(literal: str) -> Tuple[Optional[Callable], Optional[str]]: """ check evaluable format of literal string, possibly return appropriate evaluation function and stripped literal. :param literal: string to be checked if it is in the :ref:`evaluable literal format <evaluable-literal-formats>` and if it has to be stripped. :return: tuple of evaluation/execution function and the (optionally stripped) literal string (removed triple high-commas on expression/code-blocks) - if :paramref:`~evaluable_literal.literal` is in one of the supported :ref:`evaluable literal formats <evaluable-literal-formats>` - else the tuple (None, <empty string>). """ func = None ret = '' if (literal.startswith("'''") and literal.endswith("'''")) \ or (literal.startswith('"""') and literal.endswith('"""')): func = try_exec ret = literal[3:-3] # code block elif literal and literal[0] in BEG_CHARS and BEG_CHARS.find(literal[0]) == END_CHARS.find(literal[-1]): func = try_eval ret = literal # expression/list/dict/tuple/str/... literal elif literal in ('False', 'True'): func = bool # bool literal if literal == 'True': ret = literal # else return empty string to get bool('') == False else: try: int(literal) func = int ret = literal # int literal except ValueError: try: float(literal) func = float ret = literal # float literal except ValueError: pass return func, ret
[docs]class Literal: """ convert literal string into the corresponding value (and type). pass the literal string on instantiation as the first (the :paramref:`~Literal.literal_or_value`) argument:: >>> number = Literal("3") >>> number Literal('3') >>> number.value 3 >>> type(number.value) <class 'int'> :class:`Literal` will interpret the value type from the specified literal string. the corresponding `int` value provides the :attr:`~Literal.value` attribute. to make sure that a number-like literal will be interpreted as a string enclose it in high-commas. the following example will therefore result as a string type:: >>> number = Literal("'3'") >>> number Literal("'3'") >>> number.value '3' >>> type(number.value) <class 'str'> another way to ensure the correct value type, is to specify it with the optional :paramref:`second argument <Literal.value_type>`:: >>> number = Literal("3", str) >>> number Literal('3') >>> number.value '3' alternatively assign the :ref:`evaluable literal string <evaluable-literal-formats>` after the instantiation, directly to the :attr:`~Literal.value` attribute of the :class:`Literal` instance:: >>> number = Literal(value_type=str) >>> number.value = "3" >>> number.value '3' any type can be specified as the literal value type:: >>> my_list = Literal(value_type=list) >>> my_dict = Literal(value_type=dict) >>> my_datetime = Literal(value_type=datetime.datetime) >>> class MyClass: ... pass >>> my_instance = Literal(value_type=MyClass) the value type get automatically determined also for :ref:`evaluable python expression literal <evaluable-literal-formats>` . for example the following literal gets converted into a `datetime` object:: >>> datetime_value = Literal('(datetime.datetime.now())') also if assigned directly to the :attr:`~Literal.value` attribute:: >>> date_value = Literal() >>> date_value.value = '(datetime.date.today())' .. note:: the literal string of the last two examples has to start and end with round brackets, to mark it as a :ref:`evaluable literal <evaluable-literal-formats>`. to convert calendar date literal strings into one of the supported ISO formats (:data:`~ae.base.DATE_TIME_ISO` or :data:`~ae.base.DATE_ISO`), the expected value type has to be specified:: >>> date_value = Literal('2033-12-31', value_type=datetime.date) >>> assert date_value.value == datetime.date(2033, 12, 31) a `ValueError` exception will be raised if the conversion fails, or if the result cannot be converted into the requested value type:: >>> date_literal = Literal(value_type=datetime.date) >>> date_literal.value = "invalid-date-literal" >>> date_value = date_literal.value # doctest: +ELLIPSIS, +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... ValueError all supported literal formats are documented at the :attr:`~Literal.value` property/attribute. """
[docs] def __init__(self, literal_or_value: Optional[Any] = None, value_type: Optional[Type] = None, name: str = 'LiT'): """ create new Literal instance. :param literal_or_value: initial literal (evaluable string expression) or value of this instance. :param value_type: type of the value of this instance (def=determined latest by/in the :attr:`~Literal.value` property getter). :param name: name of the literal (only used for debugging/error-message). """ self._name = name self._literal_or_value = None self._type = None if isinstance(value_type, type(None)) else value_type if literal_or_value is not None: self.value = literal_or_value
[docs] def __repr__(self): return f"Literal({self._literal_or_value!r})"
@property def value(self) -> Any: """ property representing the value of this Literal instance. :setter: assign literal or a new value; can be either a value literal string or directly the represented/resulting value. if the assigned value is not a string and the value type of this instance got still unspecified then this instance will be restricted to the type of the assigned value. assigning a None value will be ignored - neither the literal nor the value will change with that! :getter: return the literal value; on the first call the literal will be evaluated (lazy/late) and the value type will be set if still unspecified. further getter calls will directly return the already converted literal value. .. _evaluable-literal-formats: if the literal of this :class:`Literal` instance coincide with one of the following evaluable formats then the value and the type of the value gets automatically recognized. an evaluable formatted literal strings has to start and end with one of the character pairs shown in the following table: +-------------+------------+------------------------------+ | starts with | ends with | evaluation value type | +=============+============+==============================+ | ( | ) | tuple literal or expression | +-------------+------------+------------------------------+ | [ | ] | list literal | +-------------+------------+------------------------------+ | { | } | dict literal | +-------------+------------+------------------------------+ | ' | ' | string literal | +-------------+------------+------------------------------+ | \" | \" | string literal | +-------------+------------+------------------------------+ | ''' | ''' | code block with return | +-------------+------------+------------------------------+ | \"\"\" | \"\"\" | code block with return | +-------------+------------+------------------------------+ **other supported literals and values** literals with type restriction to a boolean type are evaluated as python expression. this way literal strings like 'True', 'False', '0' and '1' will be correctly recognized and converted into a boolean value. literal strings that representing a date value (with type restriction to either :class:`datetime.datetime` or :class:`datetime.date`) will be converted with the :func:`~ae.core.parse_date` function and should be formatted in one of the standard date formats (defined via the :mod:`ae.base` constants :data:`~ae.base.DATE_TIME_ISO` and :data:`~ae.base.DATE_ISO`). literals and values that are not in one of the above formats will finally be passed to the constructor of the restricted type class to try to convert them into their representing value. """ check_val = self._literal_or_value msg = f"Literal {self._name} with value {check_val!r} " if self.type_mismatching_with(check_val): # first or new late real value conversion/initialization try: check_val = self._determine_value(check_val) except Exception as ex: raise ValueError(msg + f"throw exception: {ex}") from ex self._chk_val_reset_else_set_type(check_val) if check_val is not None: if self._type and self.type_mismatching_with(check_val): raise ValueError(msg + f"type mismatch: {self._type} != {type(check_val)}") self._literal_or_value = check_val return self._literal_or_value @value.setter def value(self, lit_or_val: Any): if lit_or_val is not None: if isinstance(lit_or_val, bytes) and self._type != bytes: # if not restricted to bytes lit_or_val = lit_or_val.decode('utf-8', DEF_ENCODE_ERRORS) # ..then convert bytes to string self._literal_or_value = lit_or_val # late evaluation: real value will be checked/converted by getter if not self._type and not isinstance(lit_or_val, str): # set type if unset and no eval self._type = type(lit_or_val)
[docs] def append_value(self, item_value: Any) -> Any: """ add new item to the list value of this Literal instance (lazy `self.value` getter call function pointer). :param item_value: value of the item to be appended to the value of this Literal instance. :return: the value (==list) of this Literal instance. this method gets e.g. used by the :class:`~.console.ConsoleApp` method :meth:`~.console.ConsoleApp.add_option` to have a function pointer to this literal value with lazy/late execution of the value getter (value.append cannot be used in this case because the list could have be changed before it get finally read/used). .. note:: this method calls the append method of the value object and will therefore only work if the value is of type :class:`list` (or a compatible type). """ self.value.append(item_value) return self.value
[docs] def convert_value(self, lit_or_val: Any) -> Any: """ set/change the literal/value of this :class:`Literal` instance and return the represented value. :param lit_or_val: the new value to be set. :return: the final/converted value of this Literal instance. this method gets e.g. used by the :class:`~.console.ConsoleApp` method :meth:`~.console.ConsoleApp.add_option` to have a function pointer to let the ArgumentParser convert a configuration option literal into the represented value. """ self.value = lit_or_val return self.value
[docs] def type_mismatching_with(self, value: Any) -> bool: """ check if this literal instance would reject the passed value because of type mismatch. :param value: new literal value. :return: True if the passed value would have a type mismatch or if literal type is still not set, else False. """ return self._type != type(value)
[docs] def _determine_value(self, lit_or_val: Any) -> Any: """ check passed value if it is still a literal determine the represented value. :param lit_or_val: new literal value or the representing literal string. :return: determined/converted value or self._lit_or_val if value could not be recognized/converted. """ if isinstance(lit_or_val, str): func, eval_expr = evaluable_literal(lit_or_val) if func: lit_or_val = self._chk_val_reset_else_set_type(func(eval_expr)) if self._type: if self.type_mismatching_with(lit_or_val) and isinstance(lit_or_val, str): if self._type == bool: lit_or_val = bool(try_eval(lit_or_val)) elif self._type in (datetime.date, datetime.datetime): lit_or_val = parse_date(lit_or_val, ret_date=self._type == datetime.date) lit_or_val = self._chk_val_reset_else_set_type(lit_or_val) if self.type_mismatching_with(lit_or_val): # finally try type conversion with type constructor lit_or_val = self._chk_val_reset_else_set_type( try_call(self._type, lit_or_val, ignored_exceptions=(TypeError,))) # ignore int(None) exception return lit_or_val
[docs] def _chk_val_reset_else_set_type(self, value: Any) -> Any: """ reset and return passed value if is None, else determine value type and set type (if not already set). :param value: just converted new literal value to be checked and if ok used to set an unset type. :return: passed value or the stored literal/value if passed value is None. """ if value is None or value is UNSET: value = self._literal_or_value # literal evaluation failed, therefore reset to try with type conversion elif not self._type and value is not None and value is not UNSET: self._type = type(value) return value