Source code for xsdata.formats.converter

import abc
import contextlib
import math
import warnings
from dataclasses import dataclass
from dataclasses import field
from decimal import Decimal
from decimal import InvalidOperation
from enum import Enum
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Type
from typing import Union
from xml.etree.ElementTree import QName

from lxml import etree

from xsdata.exceptions import ConverterError
from xsdata.exceptions import ConverterWarning
from xsdata.utils import text
from xsdata.utils.namespaces import load_prefix
from xsdata.utils.namespaces import split_qname


[docs]class Converter(metaclass=abc.ABCMeta): """Abstract converter class."""
[docs] @abc.abstractmethod def from_string(self, value: str, **kwargs: Any) -> Any: """ Convert from string. :raises ConverterError: if converter fails with and expected ValueError """
[docs] @abc.abstractmethod def to_string(self, value: Any, **kwargs: Any) -> str: """Convert to string."""
[docs]@dataclass class ConverterAdapter: """ :param registry: Converters registry """ registry: Dict[Type, Converter] = field(default_factory=dict)
[docs] def from_string(self, value: Any, types: List[Type], **kwargs: Any) -> Any: """ Attempt to convert a string to one of the given types. If the input is not a string return the input value. If all attempts fail return the value input value and issue a warning. :return: The first successful converted value. """ if not isinstance(value, str): return value for data_type in types: with contextlib.suppress(ConverterError): instance = self.type_converter(data_type) return instance.from_string(value, data_type=data_type, **kwargs) warnings.warn( f"Failed to convert value `{value}` to one of {types}", ConverterWarning ) return value
[docs] def to_string(self, value: Any, **kwargs: Any) -> Any: """ Convert the given value to string, ignore None values. If the value is a list assume the value is a list of tokens. """ if value is None: return None if isinstance(value, list): return " ".join([self.to_string(val, **kwargs) for val in value]) instance = self.value_converter(value) return instance.to_string(value, **kwargs)
[docs] def register_converter(self, data_type: Type, func: Union[Callable, Converter]): """ Register a callable or converter for the given data type. Callables will be wrapped in a :class:`xsdata.formats.converter.ProxyConverter` """ if isinstance(func, Converter): self.registry[data_type] = func else: self.registry[data_type] = ProxyConverter(func)
[docs] def unregister_converter(self, data_type: Type): """ Unregister the converter for the given data type. :raises KeyError: if the data type is not registered. """ self.registry.pop(data_type)
[docs] def type_converter(self, data_type: Type) -> Converter: """ Get a suitable converter for given data type. Try the parent type if the original doesn't exist, fall back to str and issue a warning if no converter matches. """ try: return self.registry[data_type] except KeyError: pass try: return self.registry[type(data_type)] except KeyError: pass warnings.warn(f"No converter registered for `{data_type}`", ConverterWarning) return self.registry[str]
[docs] def value_converter(self, value: Any) -> Converter: """Get a suitable converter for the given value.""" return self.type_converter(type(value))
[docs] @classmethod def sort_types(cls, types: List[Type]) -> List[Type]: """Sort a list of types by giving priority to strict types first.""" in_order = (bool, int, float, Decimal, str) sorted_types = [] for ordered in in_order: if ordered in types: types.remove(ordered) sorted_types.append(ordered) types.extend(sorted_types) return types
[docs]class BoolConverter(Converter):
[docs] def from_string(self, value: str, **kwargs: Any) -> bool: val = value.strip() if val in ("true", "1"): return True if val in ("false", "0"): return False raise ConverterError(f"Invalid bool literal '{value}'")
[docs] def to_string(self, value: bool, **kwargs: Any) -> str: return "true" if value else "false"
[docs]class IntConverter(Converter):
[docs] def from_string(self, value: str, **kwargs: Any) -> int: try: return int(value) except ValueError: raise ConverterError()
[docs] def to_string(self, value: int, **kwargs: Any) -> str: return str(value)
[docs]class StrConverter(Converter):
[docs] def from_string(self, value: str, **kwargs: Any) -> str: return value
[docs] def to_string(self, value: str, **kwargs: Any) -> str: return value
[docs]class FloatConverter(Converter):
[docs] def from_string(self, value: str, **kwargs: Any) -> float: try: return float(value) except ValueError: raise ConverterError()
[docs] def to_string(self, value: float, **kwargs: Any) -> str: return "NaN" if math.isnan(value) else str(value).upper()
[docs]class DecimalConverter(Converter):
[docs] def from_string(self, value: str, **kwargs: Any) -> Decimal: try: return Decimal(value) except InvalidOperation: raise ConverterError()
[docs] def to_string(self, value: Decimal, **kwargs: Any) -> str: if value.is_infinite(): return str(value).replace("Infinity", "INF") return str(value)
[docs]class QNameConverter(Converter):
[docs] def from_string( self, value: str, ns_map: Optional[Dict] = None, **kwargs: Any ) -> QName: """ Convert namespace prefixed strings, or fully qualified strings to QNames. examples: - xs:string -> QName("http://www.w3.org/2001/XMLSchema", "string") - {foo}bar -> QName("foo", "bar" """ text_or_uri, tag = self.resolve(value, ns_map) if text_or_uri: return QName(text_or_uri, tag) return QName(tag)
[docs] def to_string( self, value: QName, ns_map: Optional[Dict] = None, **kwargs: Any ) -> str: """ Convert a QName instance to string either with a namespace prefix if a prefix-URI namespaces mapping is provided or to a fully qualified name with the namespace. examples: - QName("http://www.w3.org/2001/XMLSchema", "int") & ns_map -> xs:int - QName("foo, "bar") -> {foo}bar """ if ns_map is None: return value.text namespace, tag = split_qname(value.text) if not namespace: return tag prefix = load_prefix(namespace, ns_map) return f"{prefix}:{tag}" if prefix else tag
[docs] @staticmethod def resolve(value: str, ns_map: Optional[Dict]) -> Tuple: if not value: raise ConverterError("Invalid QName") if value[0] == "{": return value, None if ns_map is None: raise ConverterError("QName converter needs ns_map to support prefixes") prefix, suffix = text.split(value.strip()) namespace = ns_map.get(prefix) if prefix and not namespace: raise ConverterError(f"Unknown namespace prefix: `{prefix}`") return namespace, suffix
[docs]class LxmlQNameConverter(Converter):
[docs] def from_string( self, value: str, ns_map: Optional[Dict] = None, **kwargs: Any ) -> etree.QName: """ Convert namespace prefixed strings, or fully qualified strings to QNames. examples: - xs:string -> QName("http://www.w3.org/2001/XMLSchema", "string") - {foo}bar -> QName("foo", "bar" """ try: text_or_uri, tag = QNameConverter.resolve(value, ns_map) return etree.QName(text_or_uri, tag) except ValueError: raise ConverterError()
[docs] def to_string( self, value: etree.QName, ns_map: Optional[Dict] = None, **kwargs: Any ) -> str: """ Convert a QName instance to string either with a namespace prefix if a prefix-URI namespaces mapping is provided or to a fully qualified name with the namespace. examples: - QName("http://www.w3.org/2001/XMLSchema", "int") & ns_map -> xs:int - QName("foo, "bar") -> {foo}bar """ if ns_map is None or not value.namespace: return value.text prefix = load_prefix(value.namespace, ns_map) return f"{prefix}:{value.localname}" if prefix else value.localname
[docs]class EnumConverter(Converter):
[docs] def from_string( self, value: str, data_type: Optional[Type[Enum]] = None, **kwargs: Any ) -> Enum: if data_type is None or not issubclass(data_type, Enum): raise ConverterError("Provide a target data type enum class.") # Convert string value to the type of the first enum member first, otherwise # more complex types like QName, Decimals will fail. member: Enum = list(data_type)[0] value_type = type(member.value) # Suppress warnings with warnings.catch_warnings(): warnings.simplefilter("ignore") real_value = converter.from_string(value, [value_type], **kwargs) # Raise exception if the real value doesn't match the expected type. if not isinstance(real_value, value_type): raise ConverterError() # Attempt #1 use the enum constructor with contextlib.suppress(ValueError): return data_type(real_value) try: # Attempt #2 the enum might be derived from # xs:NMTOKENS or xs:list removing excess whitespace. if isinstance(real_value, str): return data_type(" ".join(value.split())) # Attempt #3 some values are never equal try to match # canonical representations. repr_value = repr(real_value) return next(x for x in data_type if repr(x.value) == repr_value) except (ValueError, StopIteration): raise ConverterError()
[docs] def to_string(self, value: Enum, **kwargs: Any) -> str: return converter.to_string(value.value, **kwargs)
[docs]@dataclass class ProxyConverter(Converter): """ :param func: the callable to convert from string """ func: Callable
[docs] def from_string(self, value: str, **kwargs: Any) -> Any: try: return self.func(value) except ValueError: raise ConverterError
[docs] def to_string(self, value: Any, **kwargs: Any) -> str: return str(value)
converter = ConverterAdapter() converter.register_converter(str, StrConverter()) converter.register_converter(int, IntConverter()) converter.register_converter(bool, BoolConverter()) converter.register_converter(float, FloatConverter()) converter.register_converter(object, StrConverter()) converter.register_converter(etree.QName, LxmlQNameConverter()) converter.register_converter(QName, QNameConverter()) converter.register_converter(Decimal, DecimalConverter()) converter.register_converter(type(Enum), EnumConverter())