1# 2# Copyright (c), 2018-2020, SISSA (International School for Advanced Studies). 3# All rights reserved. 4# This file is distributed under the terms of the MIT License. 5# See the file 'LICENSE' in the root directory of the present 6# distribution, or http://opensource.org/licenses/MIT. 7# 8# @author Davide Brunato <brunato@sissa.it> 9# 10import re 11import math 12from decimal import Decimal 13from typing import Any, Union, SupportsFloat 14 15from ..helpers import collapse_white_spaces 16from .atomic_types import AtomicTypeMeta 17from .untyped import UntypedAtomic 18from .numeric import Float10, Integer 19from .datetime import AbstractDateTime, Duration 20 21FloatArgType = Union[SupportsFloat, str, bytes] 22 23#### 24# Type proxies for basic Python datatypes: a proxy class creates 25# and validates its Python datatype and virtual registered types. 26 27 28class BooleanProxy(metaclass=AtomicTypeMeta): 29 name = 'boolean' 30 pattern = re.compile(r'^(?:true|false|1|0)$') 31 32 def __new__(cls, value: object) -> bool: # type: ignore[misc] 33 if isinstance(value, bool): 34 return value 35 elif isinstance(value, (int, float, Decimal)): 36 if math.isnan(value): 37 return False 38 return bool(value) 39 elif isinstance(value, UntypedAtomic): 40 value = value.value 41 elif not isinstance(value, str): 42 raise TypeError('invalid type {!r} for xs:{}'.format(type(value), cls.name)) 43 44 if value.strip() not in {'true', 'false', '1', '0'}: 45 raise ValueError('invalid value {!r} for xs:{}'.format(value, cls.name)) 46 return 't' in value or '1' in value 47 48 @classmethod 49 def __subclasshook__(cls, subclass: type) -> bool: 50 return issubclass(subclass, bool) 51 52 @classmethod 53 def validate(cls, value: object) -> None: 54 if isinstance(value, bool): 55 return 56 elif isinstance(value, str): 57 if cls.pattern.match(value) is None: 58 raise cls.invalid_value(value) 59 else: 60 raise cls.invalid_type(value) 61 62 63class DecimalProxy(metaclass=AtomicTypeMeta): 64 name = 'decimal' 65 pattern = re.compile(r'^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$') 66 67 def __new__(cls, value: Any) -> Decimal: # type: ignore[misc] 68 if isinstance(value, (str, UntypedAtomic)): 69 value = collapse_white_spaces(str(value)).replace(' ', '') 70 if cls.pattern.match(value) is None: 71 raise cls.invalid_value(value) 72 elif isinstance(value, (float, Float10, Decimal)): 73 if math.isinf(value) or math.isnan(value): 74 raise cls.invalid_value(value) 75 try: 76 return Decimal(value) 77 except (ValueError, ArithmeticError): 78 msg = 'invalid value {!r} for xs:{}' 79 raise ArithmeticError(msg.format(value, cls.name)) from None 80 81 @classmethod 82 def __subclasshook__(cls, subclass: type) -> bool: 83 return issubclass(subclass, (int, Decimal, Integer)) and not issubclass(subclass, bool) 84 85 @classmethod 86 def validate(cls, value: object) -> None: 87 if isinstance(value, Decimal): 88 if math.isnan(value) or math.isinf(value): 89 raise cls.invalid_value(value) 90 elif isinstance(value, (int, Integer)) and not isinstance(value, bool): 91 return 92 elif isinstance(value, str): 93 if cls.pattern.match(value) is None: 94 raise cls.invalid_value(value) 95 else: 96 raise cls.invalid_type(value) 97 98 99class DoubleProxy10(metaclass=AtomicTypeMeta): 100 name = 'double' 101 xsd_version = '1.0' 102 pattern = re.compile( 103 r'^(?:[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?|[+-]?INF|NaN)$' 104 ) 105 106 def __new__(cls, value: Union[SupportsFloat, str]) -> float: # type: ignore[misc] 107 if isinstance(value, str): 108 value = collapse_white_spaces(value) 109 if value in {'INF', '-INF', 'NaN'} or cls.xsd_version != '1.0' and value == '+INF': 110 pass 111 elif value.lower() in {'inf', '+inf', '-inf', 'nan', 112 'infinity', '+infinity', '-infinity'}: 113 raise ValueError('invalid value {!r} for xs:{}'.format(value, cls.name)) 114 return float(value) 115 116 @classmethod 117 def __subclasshook__(cls, subclass: type) -> bool: 118 return issubclass(subclass, float) and not issubclass(subclass, Float10) 119 120 @classmethod 121 def validate(cls, value: object) -> None: 122 if isinstance(value, float) and not isinstance(value, Float10): 123 return 124 elif isinstance(value, str): 125 if cls.pattern.match(value) is None: 126 raise cls.invalid_value(value) 127 else: 128 raise cls.invalid_type(value) 129 130 131class DoubleProxy(DoubleProxy10): 132 name = 'double' 133 xsd_version = '1.1' 134 135 136class StringProxy(metaclass=AtomicTypeMeta): 137 name = 'string' 138 139 def __new__(cls, *args: object, **kwargs: object) -> str: # type: ignore[misc] 140 return str(*args, **kwargs) 141 142 @classmethod 143 def __subclasshook__(cls, subclass: type) -> bool: 144 return issubclass(subclass, str) 145 146 @classmethod 147 def validate(cls, value: object) -> None: 148 if not isinstance(value, str): 149 raise cls.invalid_type(value) 150 151 152#### 153# Type proxies for multiple type-checking in XPath expressions 154class NumericTypeMeta(type): 155 """Metaclass for checking numeric classes and instances.""" 156 157 def __instancecheck__(cls, instance: object) -> bool: 158 return isinstance(instance, (int, float, Decimal)) and not isinstance(instance, bool) 159 160 def __subclasscheck__(cls, subclass: type) -> bool: 161 if issubclass(subclass, bool): 162 return False 163 return issubclass(subclass, int) or issubclass(subclass, float) \ 164 or issubclass(subclass, Decimal) 165 166 167class NumericProxy(metaclass=NumericTypeMeta): 168 """Proxy for xs:numeric related types. Builds xs:float instances.""" 169 170 def __new__(cls, *args: FloatArgType, **kwargs: FloatArgType) -> float: # type: ignore[misc] 171 return float(*args, **kwargs) 172 173 174class ArithmeticTypeMeta(type): 175 """Metaclass for checking numeric, datetime and duration classes/instances.""" 176 177 def __instancecheck__(cls, instance: object) -> bool: 178 return isinstance( 179 instance, (int, float, Decimal, AbstractDateTime, Duration, UntypedAtomic) 180 ) and not isinstance(instance, bool) 181 182 def __subclasscheck__(cls, subclass: type) -> bool: 183 if issubclass(subclass, bool): 184 return False 185 return issubclass(subclass, int) or issubclass(subclass, float) or \ 186 issubclass(subclass, Decimal) or issubclass(subclass, Duration) \ 187 or issubclass(subclass, AbstractDateTime) or issubclass(subclass, UntypedAtomic) 188 189 190class ArithmeticProxy(metaclass=ArithmeticTypeMeta): 191 """Proxy for arithmetic related types. Builds xs:float instances.""" 192 193 def __new__(cls, *args: FloatArgType, **kwargs: FloatArgType) -> float: # type: ignore[misc] 194 return float(*args, **kwargs) 195