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