Source code for robot.running.arguments.typeconverters

#  Copyright 2008-2015 Nokia Networks
#  Copyright 2016-     Robot Framework Foundation
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

from ast import literal_eval
from collections import OrderedDict
try:
    from collections import abc
except ImportError:    # Python 2
    import collections as abc
try:
    from types import UnionType
except ImportError:    # Python < 3.10
    UnionType = ()
try:
    from typing import Union
except ImportError:
    class Union(object):
        pass
from datetime import datetime, date, timedelta
from decimal import InvalidOperation, Decimal
try:
    from enum import Enum
except ImportError:    # Standard in Py 3.4+ but can be separately installed
[docs] class Enum(object): pass
from numbers import Integral, Real from robot.libraries.DateTime import convert_date, convert_time from robot.utils import (FALSE_STRINGS, IRONPYTHON, TRUE_STRINGS, PY2, eq, get_error_message, is_string, seq2str, type_name, unic, unicode)
[docs]class TypeConverter(object): type = None type_name = None abc = None aliases = () value_types = (unicode,) _converters = OrderedDict() _type_aliases = {} def __init__(self, used_type): self.used_type = used_type
[docs] @classmethod def register(cls, converter): cls._converters[converter.type] = converter for name in (converter.type_name,) + converter.aliases: if name is not None and not isinstance(name, property): cls._type_aliases[name.lower()] = converter.type return converter
[docs] @classmethod def converter_for(cls, type_): if getattr(type_, '__origin__', None) and type_.__origin__ is not Union: type_ = type_.__origin__ if isinstance(type_, (str, unicode)): try: type_ = cls._type_aliases[type_.lower()] except KeyError: return None if type_ in cls._converters: return cls._converters[type_](type_) for converter in cls._converters.values(): if converter.handles(type_): return converter(type_) return None
[docs] @classmethod def handles(cls, type_): handled = (cls.type, cls.abc) if cls.abc else cls.type return isinstance(type_, type) and issubclass(type_, handled)
[docs] def convert(self, name, value, explicit_type=True, strict=True): if self.no_conversion_needed(value): return value if not self._handles_value(value): return self._handle_error(name, value, strict=strict) try: if not isinstance(value, unicode): return self._non_string_convert(value, explicit_type) return self._convert(value, explicit_type) except ValueError as error: return self._handle_error(name, value, error, strict)
[docs] def no_conversion_needed(self, value): try: return isinstance(value, self.used_type) except TypeError: # If the used type doesn't like `isinstance` (e.g. TypedDict), # compare the value to the generic type instead. if self.type and self.type is not self.used_type: return isinstance(value, self.type) raise
def _handles_value(self, value): return isinstance(value, self.value_types) def _non_string_convert(self, value, explicit_type=True): return self._convert(value, explicit_type) def _convert(self, value, explicit_type=True): raise NotImplementedError def _handle_error(self, name, value, error=None, strict=True): if not strict: return value value_type = '' if isinstance(value, unicode) else ' (%s)' % type_name(value) ending = u': %s' % error if (error and error.args) else '.' raise ValueError( "Argument '%s' got value '%s'%s that cannot be converted to %s%s" % (name, unic(value), value_type, self.type_name, ending) ) def _literal_eval(self, value, expected): # ast.literal_eval has some issues with sets: if expected is set: # On Python 2 it doesn't handle sets at all. if PY2: raise ValueError('Sets are not supported on Python 2.') # There is no way to define an empty set. if value == 'set()': return set() try: value = literal_eval(value) except (ValueError, SyntaxError): # Original errors aren't too informative in these cases. raise ValueError('Invalid expression.') except TypeError as err: raise ValueError('Evaluating expression failed: %s' % err) if not isinstance(value, expected): raise ValueError('Value is %s, not %s.' % (type_name(value), expected.__name__)) return value def _remove_number_separators(self, value): if is_string(value): for sep in ' ', '_': if sep in value: value = value.replace(sep, '') return value
[docs]@TypeConverter.register class EnumConverter(TypeConverter): type = Enum @property def type_name(self): return self.used_type.__name__ @property def value_types(self): return (unicode, int) if issubclass(self.used_type, int) else (unicode,) def _convert(self, value, explicit_type=True): enum = self.used_type if isinstance(value, int): return self._find_by_int_value(enum, value) try: # This is compatible with the enum module in Python 3.4, its # enum34 backport, and the older enum module. `enum[value]` # wouldn't work with the old enum module. return getattr(enum, value) except AttributeError: return self._find_by_normalized_name_or_int_value(enum, value) def _find_by_normalized_name_or_int_value(self, enum, value): members = sorted(self._get_members(enum)) matches = [m for m in members if eq(m, value, ignore='_')] if len(matches) == 1: return getattr(enum, matches[0]) if len(matches) > 1: raise ValueError("%s has multiple members matching '%s'. Available: %s" % (self.type_name, value, seq2str(matches))) try: if issubclass(self.used_type, int): return self._find_by_int_value(enum, value) except ValueError: members = ['%s (%d)' % (m, getattr(enum, m)) for m in members] raise ValueError("%s does not have member '%s'. Available: %s" % (self.type_name, value, seq2str(members))) def _get_members(self, enum): try: return list(enum.__members__) except AttributeError: # old enum module return [attr for attr in dir(enum) if not attr.startswith('_')] def _find_by_int_value(self, enum, value): value = int(value) for member in enum: if member.value == value: return member values = sorted(member.value for member in enum) raise ValueError("%s does not have value '%d'. Available: %s" % (self.type_name, value, seq2str(values)))
[docs]@TypeConverter.register class StringConverter(TypeConverter): type = unicode type_name = 'string' aliases = ('string', 'str', 'unicode') def _handles_value(self, value): return True def _convert(self, value, explicit_type=True): if not explicit_type: return value try: return unicode(value) except Exception: raise ValueError(get_error_message())
[docs]@TypeConverter.register class BooleanConverter(TypeConverter): value_types = (unicode, int, float, type(None)) type = bool type_name = 'boolean' aliases = ('bool',) def _non_string_convert(self, value, explicit_type=True): return value def _convert(self, value, explicit_type=True): upper = value.upper() if upper == 'NONE': return None if upper in TRUE_STRINGS: return True if upper in FALSE_STRINGS: return False return value
[docs]@TypeConverter.register class IntegerConverter(TypeConverter): type = int abc = Integral type_name = 'integer' aliases = ('int', 'long') value_types = (unicode, float) def _non_string_convert(self, value, explicit_type=True): if value.is_integer(): return int(value) raise ValueError('Conversion would lose precision.') def _convert(self, value, explicit_type=True): value = self._remove_number_separators(value) value, base = self._get_base(value) try: return int(value, base) except ValueError: if base == 10 and not explicit_type: try: return float(value) except ValueError: pass raise ValueError def _get_base(self, value): value = value.lower() for prefix, base in [('0x', 16), ('0o', 8), ('0b', 2)]: if prefix in value: parts = value.split(prefix) if len(parts) == 2 and parts[0] in ('', '-', '+'): return ''.join(parts), base return value, 10
[docs]@TypeConverter.register class FloatConverter(TypeConverter): type = float abc = Real type_name = 'float' aliases = ('double',) value_types = (unicode, Real) def _convert(self, value, explicit_type=True): try: return float(self._remove_number_separators(value)) except ValueError: raise ValueError
[docs]@TypeConverter.register class DecimalConverter(TypeConverter): type = Decimal type_name = 'decimal' value_types = (unicode, int, float) def _convert(self, value, explicit_type=True): try: return Decimal(self._remove_number_separators(value)) except InvalidOperation: # With Python 3 error messages by decimal module are not very # useful and cannot be included in our error messages: # https://bugs.python.org/issue26208 raise ValueError
[docs]@TypeConverter.register class BytesConverter(TypeConverter): type = bytes abc = getattr(abc, 'ByteString', None) # ByteString is new in Python 3 type_name = 'bytes' value_types = (unicode, bytearray) def _non_string_convert(self, value, explicit_type=True): return bytes(value) def _convert(self, value, explicit_type=True): if PY2 and not explicit_type: return value try: value = value.encode('latin-1') except UnicodeEncodeError as err: raise ValueError("Character '%s' cannot be mapped to a byte." % value[err.start:err.start+1]) return value if not IRONPYTHON else bytes(value)
[docs]@TypeConverter.register class ByteArrayConverter(TypeConverter): type = bytearray type_name = 'bytearray' value_types = (unicode, bytes) def _non_string_convert(self, value, explicit_type=True): return bytearray(value) def _convert(self, value, explicit_type=True): try: return bytearray(value, 'latin-1') except UnicodeEncodeError as err: raise ValueError("Character '%s' cannot be mapped to a byte." % value[err.start:err.start+1])
[docs]@TypeConverter.register class DateTimeConverter(TypeConverter): type = datetime type_name = 'datetime' value_types = (unicode, int, float) def _convert(self, value, explicit_type=True): return convert_date(value, result_format='datetime')
[docs]@TypeConverter.register class DateConverter(TypeConverter): type = date type_name = 'date' def _convert(self, value, explicit_type=True): dt = convert_date(value, result_format='datetime') if dt.hour or dt.minute or dt.second or dt.microsecond: raise ValueError("Value is datetime, not date.") return dt.date()
[docs]@TypeConverter.register class TimeDeltaConverter(TypeConverter): type = timedelta type_name = 'timedelta' value_types = (unicode, int, float) def _convert(self, value, explicit_type=True): return convert_time(value, result_format='timedelta')
[docs]@TypeConverter.register class NoneConverter(TypeConverter): type = type(None) type_name = 'None' def __init__(self, used_type): if used_type is None: used_type = type(None) TypeConverter.__init__(self, used_type)
[docs] @classmethod def handles(cls, type_): return type_ in (type(None), None)
def _convert(self, value, explicit_type=True): if value.upper() == 'NONE': return None raise ValueError
[docs]@TypeConverter.register class ListConverter(TypeConverter): type = list type_name = 'list' abc = abc.Sequence value_types = (unicode, tuple)
[docs] def no_conversion_needed(self, value): if isinstance(value, (str, unicode)): return False return TypeConverter.no_conversion_needed(self, value)
def _non_string_convert(self, value, explicit_type=True): return list(value) def _convert(self, value, explicit_type=True): return self._literal_eval(value, list)
[docs]@TypeConverter.register class TupleConverter(TypeConverter): type = tuple type_name = 'tuple' value_types = (unicode, list) def _non_string_convert(self, value, explicit_type=True): return tuple(value) def _convert(self, value, explicit_type=True): return self._literal_eval(value, tuple)
[docs]@TypeConverter.register class DictionaryConverter(TypeConverter): type = dict abc = abc.Mapping type_name = 'dictionary' aliases = ('dict', 'map') def _convert(self, value, explicit_type=True): return self._literal_eval(value, dict)
[docs]@TypeConverter.register class SetConverter(TypeConverter): type = set type_name = 'set' value_types = (unicode, frozenset, list, tuple, abc.Mapping) abc = abc.Set def _non_string_convert(self, value, explicit_type=True): return set(value) def _convert(self, value, explicit_type=True): return self._literal_eval(value, set)
[docs]@TypeConverter.register class FrozenSetConverter(TypeConverter): type = frozenset type_name = 'frozenset' value_types = (unicode, set, list, tuple, abc.Mapping) def _non_string_convert(self, value, explicit_type=True): return frozenset(value) def _convert(self, value, explicit_type=True): # There are issues w/ literal_eval. See self._literal_eval for details. if value == 'frozenset()' and not PY2: return frozenset() return frozenset(self._literal_eval(value, set))
[docs]@TypeConverter.register class CombinedConverter(TypeConverter): type = Union def __init__(self, union): self.types = self._none_to_nonetype(self._get_types(union)) self.converters = [TypeConverter.converter_for(t) for t in self.types] def _get_types(self, union): if not union: return () if isinstance(union, tuple): return union try: return union.__args__ except AttributeError: # Python 3.5.2's typing uses __union_params__ instead # of __args__. This block can likely be safely removed # when Python 3.5 support is dropped return union.__union_params__ def _none_to_nonetype(self, types): return tuple(t if t is not None else type(None) for t in types) @property def type_name(self): return ' or '.join(type_name(t) for t in self.types) if self.types else None
[docs] @classmethod def handles(cls, type_): return (isinstance(type_, (UnionType, tuple)) or getattr(type_, '__origin__', None) is Union)
def _handles_value(self, value): return True
[docs] def no_conversion_needed(self, value): for converter in self.converters: if converter and converter.no_conversion_needed(value): return True return False
def _convert(self, value, explicit_type=True): for converter in self.converters: if not converter: return value try: return converter.convert('', value, explicit_type) except ValueError: pass raise ValueError