# 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