diff --git a/.gitignore b/.gitignore index 81f6607..92dff97 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # Exclude coverage test-reports +/.pytype/ # Exlude IDE related files .idea/* diff --git a/.pytype.toml b/.pytype.toml new file mode 100644 index 0000000..6d68ea2 --- /dev/null +++ b/.pytype.toml @@ -0,0 +1,89 @@ +# config file for https://pypi.org/project/pytype/ +# generated via `pytype --generate-config .pytype.toml` +# run via `pytype --config .pytype.toml` + +# NOTE: All relative paths are relative to the location of this file. + +[tool.pytype] + +# Space-separated list of files or directories to exclude. +exclude = [ + '**/*_test.py', + '**/test_*.py', +] + +# Space-separated list of files or directories to process. +inputs = [ + 'serializable', + # 'tests/model.py' +] + +# Keep going past errors to analyze as many files as possible. +keep_going = true + +# Run N jobs in parallel. When 'auto' is used, this will be equivalent to the +# number of CPUs on the host system. +# jobs = 4 + +# All pytype output goes here. +output = '.pytype' + +# Platform (e.g., "linux", "win32") that the target code runs on. +# platform = 'linux' + +# Paths to source code directories, separated by ':'. +pythonpath = '.' + +# Python version (major.minor) of the target code. +# python_version = '3.7' + +# Enable parameter count checks for overriding methods. This flag is temporary +# and will be removed once this behavior is enabled by default. +overriding_parameter_count_checks = true + +# Enable parameter count checks for overriding methods with renamed arguments. +# This flag is temporary and will be removed once this behavior is enabled by +# default. +overriding_renamed_parameter_count_checks = false + +# Use the enum overlay for more precise enum checking. This flag is temporary +# and will be removed once this behavior is enabled by default. +use_enum_overlay = false + +# Variables initialized as None retain their None binding. This flag is +# temporary and will be removed once this behavior is enabled by default. +strict_none_binding = false + +# Support the third-party fiddle library. This flag is temporary and will be +# removed once this behavior is enabled by default. +use_fiddle_overlay = false + +# Opt-in: Do not allow Any as a return type. +no_return_any = false + +# Experimental: Infer precise return types even for invalid function calls. +precise_return = false + +# Experimental: Solve unknown types to label with structural types. +protocols = false + +# Experimental: Only load submodules that are explicitly imported. +strict_import = false + +# Experimental: Enable exhaustive checking of function parameter types. +strict_parameter_checks = false + +# Experimental: Emit errors for comparisons between incompatible primitive +# types. +strict_primitive_comparisons = false + +# Experimental: Check that variables are defined in all possible code paths. +strict_undefined_checks = false + +# Space-separated list of error names to ignore. +disable = [ + 'pyi-error', +] + +# Don't report errors. +report_errors = true diff --git a/serializable/__init__.py b/serializable/__init__.py index 973c5d2..6a5ff80 100644 --- a/serializable/__init__.py +++ b/serializable/__init__.py @@ -16,31 +16,39 @@ # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) Paul Horton. All Rights Reserved. + import enum import inspect import json import logging import os import re -import typing # noqa: F401 import warnings from copy import copy from decimal import Decimal -from io import StringIO, TextIOWrapper +from io import StringIO, TextIOBase from json import JSONEncoder from sys import version_info -from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type, TypeVar, Union, cast +from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type, TypeVar, Union, cast, overload from xml.etree.ElementTree import Element, SubElement from defusedxml import ElementTree as SafeElementTree # type: ignore +from .formatters import BaseNameFormatter, CurrentFormatter +from .helpers import BaseHelper + if version_info >= (3, 8): - from typing import Protocol + from typing import Literal, Protocol # type:ignore[attr-defined] else: - from typing_extensions import Protocol # type: ignore + from typing_extensions import Literal, Protocol # type:ignore[assignment] -from .formatters import BaseNameFormatter, CurrentFormatter -from .helpers import BaseHelper +# `Intersection` is still not implemented, so it is interim replaced by Union for any support +# see section "Intersection" in https://peps.python.org/pep-0483/ +# see https://github.com/python/typing/issues/213 +from typing import Union as Intersection # isort: skip + +# MUST import the whole thing to get some eval/hacks working for dynamic type detection. +import typing # noqa: F401 # isort: skip # !! version is managed by semantic_release # do not use typing here, or else `semantic_release` might have issues finding the variable @@ -50,15 +58,14 @@ logger.setLevel(logging.INFO) -class _Klass(Protocol): - __name__: str - __qualname__: str +class ViewType: + """Base of all views.""" + pass _F = TypeVar("_F", bound=Callable[..., Any]) -_T = TypeVar('_T', bound=_Klass) - -ViewType = _Klass +_T = TypeVar('_T') +_E = TypeVar('_E', bound=enum.Enum) @enum.unique @@ -109,7 +116,7 @@ class XmlArraySerializationType(enum.Enum): def _allow_property_for_view(prop_info: 'ObjectMetadataLibrary.SerializableProperty', value_: Any, - view_: Optional[Type[_Klass]]) -> bool: + view_: Optional[Type[ViewType]]) -> bool: # First check Property is part of the View is given allow_for_view = False if view_: @@ -144,15 +151,15 @@ class _SerializableJsonEncoder(JSONEncoder): def __init__(self, *, skipkeys: bool = False, ensure_ascii: bool = True, check_circular: bool = True, allow_nan: bool = True, sort_keys: bool = False, indent: Optional[int] = None, separators: Optional[Tuple[str, str]] = None, default: Optional[Callable[[Any], Any]] = None, - view_: Optional[Type[_Klass]] = None) -> None: + view_: Optional[Type[ViewType]] = None) -> None: super().__init__( skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular, allow_nan=allow_nan, sort_keys=sort_keys, indent=indent, separators=separators, default=default ) - self._view = view_ + self._view: Optional[Type[ViewType]] = view_ @property - def view(self) -> Optional[Type[_Klass]]: + def view(self) -> Optional[Type[ViewType]]: return self._view def default(self, o: Any) -> Any: @@ -225,378 +232,395 @@ def default(self, o: Any) -> Any: super().default(o=o) -def _as_json(self: _T, view_: Optional[Type[Any]] = None) -> str: - """ - Internal function that is injected into Classes that are annotated for serialization and deserialization by - ``serializable``. - """ - logging.debug(f'Dumping {self} to JSON with view: {view_}...') - return json.dumps(self, cls=_SerializableJsonEncoder, view_=view_) +class _JsonSerializable(Protocol): + def as_json(self: Any, view_: Optional[Type[ViewType]] = None) -> str: + """ + Internal method that is injected into Classes that are annotated for serialization and deserialization by + ``serializable``. + """ + logging.debug(f'Dumping {self} to JSON with view: {view_}...') + return json.dumps(self, cls=_SerializableJsonEncoder, view_=view_) -def _from_json(cls: Type[_T], data: Dict[str, Any]) -> object: - """ - Internal function that is injected into Classes that are annotated for serialization and deserialization by - ``serializable``. - """ - logging.debug(f'Rendering JSON to {cls}...') - klass_qualified_name = f'{cls.__module__}.{cls.__qualname__}' - klass = ObjectMetadataLibrary.klass_mappings.get(klass_qualified_name, None) - klass_properties = ObjectMetadataLibrary.klass_property_mappings.get(klass_qualified_name, {}) - - if klass is None: - warnings.warn(f'{klass_qualified_name} is not a known serializable class', stacklevel=2) - return None - - if len(klass_properties) == 1: - k, only_prop = next(iter(klass_properties.items())) - if only_prop.custom_names.get(SerializationType.JSON, None) == '.': - _data = {only_prop.name: data} - return cls(**_data) - - _data = copy(data) - for k, v in data.items(): - decoded_k = CurrentFormatter.formatter.decode(property_name=k) - if decoded_k in klass.ignore_during_deserialization: - logger.debug(f'Ignoring {k} when deserializing {cls.__module__}.{cls.__qualname__}') - del _data[k] - continue - - new_key = None - if decoded_k not in klass_properties: - for p, pi in klass_properties.items(): - if pi.custom_names.get(SerializationType.JSON, None) in [decoded_k, k]: - new_key = p - else: - new_key = decoded_k + @classmethod + def from_json(cls: Type[_T], data: Dict[str, Any]) -> Optional[_T]: + """ + Internal method that is injected into Classes that are annotated for serialization and deserialization by + ``serializable``. + """ + logging.debug(f'Rendering JSON to {cls}...') + klass_qualified_name = f'{cls.__module__}.{cls.__qualname__}' + klass = ObjectMetadataLibrary.klass_mappings.get(klass_qualified_name, None) + klass_properties = ObjectMetadataLibrary.klass_property_mappings.get(klass_qualified_name, {}) - if new_key is None: - logger.error( - f'Unexpected key {k}/{decoded_k} in data being serialized to {cls.__module__}.{cls.__qualname__}' - ) - raise ValueError( - f'Unexpected key {k}/{decoded_k} in data being serialized to {cls.__module__}.{cls.__qualname__}' - ) + if klass is None: + warnings.warn(f'{klass_qualified_name} is not a known serializable class', stacklevel=2) + return None - del (_data[k]) - _data[new_key] = v + if len(klass_properties) == 1: + k, only_prop = next(iter(klass_properties.items())) + if only_prop.custom_names.get(SerializationType.JSON, None) == '.': + _data = {only_prop.name: data} + return cls(**_data) + + _data = copy(data) + for k, v in data.items(): + decoded_k = CurrentFormatter.formatter.decode(property_name=k) + if decoded_k in klass.ignore_during_deserialization: + logger.debug(f'Ignoring {k} when deserializing {cls.__module__}.{cls.__qualname__}') + del _data[k] + continue - for k, v in _data.items(): - prop_info = klass_properties.get(k, None) - if not prop_info: - raise ValueError(f'No Prop Info for {k} in {cls}') + new_key = None + if decoded_k not in klass_properties: + for p, pi in klass_properties.items(): + if pi.custom_names.get(SerializationType.JSON, None) in [decoded_k, k]: + new_key = p + else: + new_key = decoded_k - try: - if prop_info.custom_type: - if prop_info.is_helper_type(): - _data[k] = prop_info.custom_type.json_deserialize(v) - else: - _data[k] = prop_info.custom_type(v) - elif prop_info.is_array: - items = [] - for j in v: - if not prop_info.is_primitive_type() and not prop_info.is_enum: - items.append(prop_info.concrete_type.from_json(data=j)) - else: - items.append(prop_info.concrete_type(j)) - _data[k] = items # type: ignore - elif prop_info.is_enum: - _data[k] = prop_info.concrete_type(v) - elif not prop_info.is_primitive_type(): - global_klass_name = f'{prop_info.concrete_type.__module__}.{prop_info.concrete_type.__name__}' - if global_klass_name in ObjectMetadataLibrary.klass_mappings: - _data[k] = prop_info.concrete_type.from_json(data=v) - else: - _data[k] = prop_info.concrete_type(v) - except AttributeError as e: - logging.error(f'There was an AttributeError deserializing JSON to {cls}.{os.linesep}' - f'The Property is: {prop_info}{os.linesep}' - f'The Value was: {v}{os.linesep}' - f'Exception: {e}{os.linesep}') - raise AttributeError( - f'There was an AttributeError deserializing JSON to {cls} the Property {prop_info}: {e}' - ) + if new_key is None: + logger.error( + f'Unexpected key {k}/{decoded_k} in data being serialized to {cls.__module__}.{cls.__qualname__}' + ) + raise ValueError( + f'Unexpected key {k}/{decoded_k} in data being serialized to {cls.__module__}.{cls.__qualname__}' + ) - logging.debug(f'Creating {cls} from {_data}') + del (_data[k]) + _data[new_key] = v - return cls(**_data) + for k, v in _data.items(): + prop_info = klass_properties.get(k, None) + if not prop_info: + raise ValueError(f'No Prop Info for {k} in {cls}') + try: + if prop_info.custom_type: + if prop_info.is_helper_type(): + _data[k] = prop_info.custom_type.json_deserialize(v) + else: + _data[k] = prop_info.custom_type(v) + elif prop_info.is_array: + items = [] + for j in v: + if not prop_info.is_primitive_type() and not prop_info.is_enum: + try: + items.append(prop_info.concrete_type.from_json(data=j)) + except AttributeError as e: + raise e + else: + items.append(prop_info.concrete_type(j)) + _data[k] = items # type: ignore + elif prop_info.is_enum: + _data[k] = prop_info.concrete_type(v) + elif not prop_info.is_primitive_type(): + global_klass_name = f'{prop_info.concrete_type.__module__}.{prop_info.concrete_type.__name__}' + if global_klass_name in ObjectMetadataLibrary.klass_mappings: + _data[k] = prop_info.concrete_type.from_json(data=v) + else: + _data[k] = prop_info.concrete_type(v) + except AttributeError as e: + logging.error(f'There was an AttributeError deserializing JSON to {cls}.{os.linesep}' + f'The Property is: {prop_info}{os.linesep}' + f'The Value was: {v}{os.linesep}' + f'Exception: {e}{os.linesep}') + raise AttributeError( + f'There was an AttributeError deserializing JSON to {cls} the Property {prop_info}: {e}' + ) -def _as_xml(self: _T, view_: Optional[Type[_T]] = None, as_string: bool = True, element_name: Optional[str] = None, - xmlns: Optional[str] = None) -> Union[Element, str]: - logging.debug(f'Dumping {self} to XML with view {view_}...') + logging.debug(f'Creating {cls} from {_data}') - this_e_attributes = {} - klass_qualified_name = f'{self.__class__.__module__}.{self.__class__.__qualname__}' - serializable_property_info = {k: v for k, v in sorted( - ObjectMetadataLibrary.klass_property_mappings.get(klass_qualified_name, {}).items(), - key=lambda i: i[1].xml_sequence)} + return cls(**_data) - for k, v in self.__dict__.items(): - # Remove leading _ in key names - new_key = k[1:] - if new_key.startswith('_') or '__' in new_key: - continue - new_key = BaseNameFormatter.decode_handle_python_builtins_and_keywords(name=new_key) - if new_key in serializable_property_info: - prop_info = cast('ObjectMetadataLibrary.SerializableProperty', serializable_property_info.get(new_key)) +class _XmlSerializable(Protocol): - if not _allow_property_for_view(prop_info=prop_info, view_=view_, value_=v): - # Skip as rendering for a view and this Property is not registered form this View + def as_xml(self: Any, view_: Optional[Type[ViewType]] = None, + as_string: bool = True, element_name: Optional[str] = None, + xmlns: Optional[str] = None) -> Union[Element, str]: + """ + Internal method that is injected into Classes that are annotated for serialization and deserialization by + ``serializable``. + """ + logging.debug(f'Dumping {self} to XML with view {view_}...') + + this_e_attributes = {} + klass_qualified_name = f'{self.__class__.__module__}.{self.__class__.__qualname__}' + serializable_property_info = {k: v for k, v in sorted( + ObjectMetadataLibrary.klass_property_mappings.get(klass_qualified_name, {}).items(), + key=lambda i: i[1].xml_sequence)} + + for k, v in self.__dict__.items(): + # Remove leading _ in key names + new_key = k[1:] + if new_key.startswith('_') or '__' in new_key: continue + new_key = BaseNameFormatter.decode_handle_python_builtins_and_keywords(name=new_key) - if prop_info and prop_info.is_xml_attribute: - new_key = prop_info.custom_names.get(SerializationType.XML, new_key) - if CurrentFormatter.formatter: - new_key = CurrentFormatter.formatter.encode(property_name=new_key) + if new_key in serializable_property_info: + prop_info = cast('ObjectMetadataLibrary.SerializableProperty', serializable_property_info.get(new_key)) - if prop_info.custom_type and prop_info.is_helper_type(): - v = prop_info.custom_type.xml_serialize(v) - elif prop_info.is_enum: - v = v.value + if not _allow_property_for_view(prop_info=prop_info, view_=view_, value_=v): + # Skip as rendering for a view and this Property is not registered form this View + continue - this_e_attributes.update({_namespace_element_name(new_key, xmlns): str(v)}) + if prop_info and prop_info.is_xml_attribute: + new_key = prop_info.custom_names.get(SerializationType.XML, new_key) + if CurrentFormatter.formatter: + new_key = CurrentFormatter.formatter.encode(property_name=new_key) - element_name = _namespace_element_name( - element_name if element_name else CurrentFormatter.formatter.encode(self.__class__.__name__), - xmlns) - this_e = Element(element_name, this_e_attributes) + if prop_info.custom_type and prop_info.is_helper_type(): + v = prop_info.custom_type.xml_serialize(v) + elif prop_info.is_enum: + v = v.value - # Handle remaining Properties that will be sub elements - for k, prop_info in serializable_property_info.items(): - # Skip if rendering for a View and this Property is not designated for this View - v = getattr(self, k) + this_e_attributes.update({_namespace_element_name(new_key, xmlns): str(v)}) - if not _allow_property_for_view(prop_info=prop_info, view_=view_, value_=v): - # Skip as rendering for a view and this Property is not registered form this View - continue + element_name = _namespace_element_name( + element_name if element_name else CurrentFormatter.formatter.encode(self.__class__.__name__), + xmlns) + this_e = Element(element_name, this_e_attributes) - if v is None: - v = prop_info.get_none_value_for_view(view_=view_) + # Handle remaining Properties that will be sub elements + for k, prop_info in serializable_property_info.items(): + # Skip if rendering for a View and this Property is not designated for this View + v = getattr(self, k) - new_key = BaseNameFormatter.decode_handle_python_builtins_and_keywords(name=k) + if not _allow_property_for_view(prop_info=prop_info, view_=view_, value_=v): + # Skip as rendering for a view and this Property is not registered form this View + continue - if not prop_info: - raise ValueError(f'{new_key} is not a known Property for {klass_qualified_name}') + if v is None: + v = prop_info.get_none_value_for_view(view_=view_) - if not prop_info.is_xml_attribute: - new_key = prop_info.custom_names.get(SerializationType.XML, new_key) + new_key = BaseNameFormatter.decode_handle_python_builtins_and_keywords(name=k) - if v is None: - SubElement(this_e, _namespace_element_name(tag_name=new_key, xmlns=xmlns)) - continue + if not prop_info: + raise ValueError(f'{new_key} is not a known Property for {klass_qualified_name}') - if new_key == '.': - this_e.text = str(v) - continue + if not prop_info.is_xml_attribute: + new_key = prop_info.custom_names.get(SerializationType.XML, new_key) - if CurrentFormatter.formatter: - new_key = CurrentFormatter.formatter.encode(property_name=new_key) - new_key = _namespace_element_name(new_key, xmlns) + if v is None: + SubElement(this_e, _namespace_element_name(tag_name=new_key, xmlns=xmlns)) + continue - if prop_info.is_array and prop_info.xml_array_config: - _array_type, nested_key = prop_info.xml_array_config - nested_key = _namespace_element_name(nested_key, xmlns) - if _array_type and _array_type == XmlArraySerializationType.NESTED: - nested_e = SubElement(this_e, new_key) - else: - nested_e = this_e - for j in v: - if not prop_info.is_primitive_type() and not prop_info.is_enum: - nested_e.append(j.as_xml(view_=view_, as_string=False, element_name=nested_key, xmlns=xmlns)) - elif prop_info.is_enum: - SubElement(nested_e, nested_key).text = str(j.value) - elif prop_info.concrete_type in (float, int): - SubElement(nested_e, nested_key).text = str(j) - elif prop_info.concrete_type is bool: - SubElement(nested_e, nested_key).text = str(j).lower() + if new_key == '.': + this_e.text = str(v) + continue + + if CurrentFormatter.formatter: + new_key = CurrentFormatter.formatter.encode(property_name=new_key) + new_key = _namespace_element_name(new_key, xmlns) + + if prop_info.is_array and prop_info.xml_array_config: + _array_type, nested_key = prop_info.xml_array_config + nested_key = _namespace_element_name(nested_key, xmlns) + if _array_type and _array_type == XmlArraySerializationType.NESTED: + nested_e = SubElement(this_e, new_key) else: - # Assume type is str - SubElement(nested_e, nested_key).text = str(j) - elif prop_info.custom_type: - if prop_info.is_helper_type(): - SubElement(this_e, new_key).text = str(prop_info.custom_type.xml_serialize(v)) - else: - SubElement(this_e, new_key).text = str(prop_info.custom_type(v)) - elif prop_info.is_enum: - SubElement(this_e, new_key).text = str(v.value) - elif not prop_info.is_primitive_type(): - global_klass_name = f'{prop_info.concrete_type.__module__}.{prop_info.concrete_type.__name__}' - if global_klass_name in ObjectMetadataLibrary.klass_mappings: - # Handle other Serializable Classes - this_e.append(v.as_xml(view_=view_, as_string=False, element_name=new_key, xmlns=xmlns)) - else: - # Handle properties that have a type that is not a Python Primitive (e.g. int, float, str) - if prop_info.string_format: - SubElement(this_e, new_key).text = f'{v:{prop_info.string_format}}' + nested_e = this_e + for j in v: + if not prop_info.is_primitive_type() and not prop_info.is_enum: + nested_e.append( + j.as_xml(view_=view_, as_string=False, element_name=nested_key, xmlns=xmlns)) + elif prop_info.is_enum: + SubElement(nested_e, nested_key).text = str(j.value) + elif prop_info.concrete_type in (float, int): + SubElement(nested_e, nested_key).text = str(j) + elif prop_info.concrete_type is bool: + SubElement(nested_e, nested_key).text = str(j).lower() + else: + # Assume type is str + SubElement(nested_e, nested_key).text = str(j) + elif prop_info.custom_type: + if prop_info.is_helper_type(): + SubElement(this_e, new_key).text = str(prop_info.custom_type.xml_serialize(v)) else: - SubElement(this_e, new_key).text = str(v) - elif prop_info.concrete_type in (float, int): - SubElement(this_e, new_key).text = str(v) - elif prop_info.concrete_type is bool: - SubElement(this_e, new_key).text = str(v).lower() - else: - # Assume type is str - SubElement(this_e, new_key).text = str(v) + SubElement(this_e, new_key).text = str(prop_info.custom_type(v)) + elif prop_info.is_enum: + SubElement(this_e, new_key).text = str(v.value) + elif not prop_info.is_primitive_type(): + global_klass_name = f'{prop_info.concrete_type.__module__}.{prop_info.concrete_type.__name__}' + if global_klass_name in ObjectMetadataLibrary.klass_mappings: + # Handle other Serializable Classes + this_e.append(v.as_xml(view_=view_, as_string=False, element_name=new_key, xmlns=xmlns)) + else: + # Handle properties that have a type that is not a Python Primitive (e.g. int, float, str) + if prop_info.string_format: + SubElement(this_e, new_key).text = f'{v:{prop_info.string_format}}' + else: + SubElement(this_e, new_key).text = str(v) + elif prop_info.concrete_type in (float, int): + SubElement(this_e, new_key).text = str(v) + elif prop_info.concrete_type is bool: + SubElement(this_e, new_key).text = str(v).lower() + else: + # Assume type is str + SubElement(this_e, new_key).text = str(v) - if as_string: - return cast(Element, SafeElementTree.tostring(this_e, 'unicode')) - else: - return this_e + if as_string: + return cast(Element, SafeElementTree.tostring(this_e, 'unicode')) + else: + return this_e + + @classmethod + def from_xml(cls: Type[_T], data: Union[TextIOBase, Element], + default_namespace: Optional[str] = None) -> Optional[_T]: + """ + Internal method that is injected into Classes that are annotated for serialization and deserialization by + ``serializable``. + """ + logging.debug(f'Rendering XML from {type(data)} to {cls}...') + klass = ObjectMetadataLibrary.klass_mappings.get(f'{cls.__module__}.{cls.__qualname__}', None) + if klass is None: + warnings.warn(f'{cls.__module__}.{cls.__qualname__} is not a known serializable class', stacklevel=2) + return None + klass_properties = ObjectMetadataLibrary.klass_property_mappings.get(f'{cls.__module__}.{cls.__qualname__}', {}) -def _from_xml(cls: Type[_T], data: Union[TextIOWrapper, Element], - default_namespace: Optional[str] = None) -> object: - logging.debug(f'Rendering XML from {type(data)} to {cls}...') - klass = ObjectMetadataLibrary.klass_mappings.get(f'{cls.__module__}.{cls.__qualname__}', None) - if klass is None: - warnings.warn(f'{cls.__module__}.{cls.__qualname__} is not a known serializable class', stacklevel=2) - return None + if isinstance(data, TextIOBase): + data = cast(Element, SafeElementTree.fromstring(data.read())) - klass_properties = ObjectMetadataLibrary.klass_property_mappings.get(f'{cls.__module__}.{cls.__qualname__}', {}) + if default_namespace is None: + _namespaces = dict([node for _, node in + SafeElementTree.iterparse(StringIO(SafeElementTree.tostring(data, 'unicode')), + events=['start-ns'])]) + default_namespace = (re.compile(r'^\{(.*?)\}.').search(data.tag) or (None, _namespaces.get('')))[1] - if isinstance(data, TextIOWrapper): - data = cast(Element, SafeElementTree.fromstring(data.read())) + if default_namespace is None: + def strip_default_namespace(s: str) -> str: + return s + else: + def strip_default_namespace(s: str) -> str: + return s.replace(f'{{{default_namespace}}}', '') - if default_namespace is None: - _namespaces = dict([node for _, node in - SafeElementTree.iterparse(StringIO(SafeElementTree.tostring(data, 'unicode')), - events=['start-ns'])]) - default_namespace = (re.compile(r'^\{(.*?)\}.').search(data.tag) or (None, _namespaces.get('')))[1] + _data: Dict[str, Any] = {} - if default_namespace is None: - def strip_default_namespace(s: str) -> str: - return s - else: - def strip_default_namespace(s: str) -> str: - return s.replace(f'{{{default_namespace}}}', '') + # Handle attributes on the root element if there are any + for k, v in data.attrib.items(): + decoded_k = CurrentFormatter.formatter.decode(strip_default_namespace(k)) + if decoded_k in klass.ignore_during_deserialization: + logger.debug(f'Ignoring {decoded_k} when deserializing {cls.__module__}.{cls.__qualname__}') + continue - _data: Dict[str, Any] = {} + if decoded_k not in klass_properties: + for p, pi in klass_properties.items(): + if pi.custom_names.get(SerializationType.XML, None) == decoded_k: + decoded_k = p - # Handle attributes on the root element if there are any - for k, v in data.attrib.items(): - decoded_k = CurrentFormatter.formatter.decode(strip_default_namespace(k)) - if decoded_k in klass.ignore_during_deserialization: - logger.debug(f'Ignoring {decoded_k} when deserializing {cls.__module__}.{cls.__qualname__}') - continue + prop_info = klass_properties.get(decoded_k, None) + if not prop_info: + raise ValueError(f'Non-primitive types not supported from XML Attributes - see {decoded_k} for ' + f'{cls.__module__}.{cls.__qualname__} which has Prop Metadata: {prop_info}') - if decoded_k not in klass_properties: - for p, pi in klass_properties.items(): - if pi.custom_names.get(SerializationType.XML, None) == decoded_k: - decoded_k = p - - prop_info = klass_properties.get(decoded_k, None) - if not prop_info: - raise ValueError(f'Non-primitive types not supported from XML Attributes - see {decoded_k} for ' - f'{cls.__module__}.{cls.__qualname__} which has Prop Metadata: {prop_info}') - - if prop_info.custom_type and prop_info.is_helper_type(): - _data[decoded_k] = prop_info.custom_type.xml_deserialize(v) - elif prop_info.is_enum: - _data[decoded_k] = prop_info.concrete_type(v) - elif prop_info.is_primitive_type(): - _data[decoded_k] = prop_info.concrete_type(v) - else: - raise ValueError(f'Non-primitive types not supported from XML Attributes - see {decoded_k}') - - # Handle Node text content - if data.text: - for p, pi in klass_properties.items(): - if pi.custom_names.get(SerializationType.XML, None) == '.': - _data[p] = data.text.strip() - - # Handle Sub-Elements - for child_e in data: - decoded_k = CurrentFormatter.formatter.decode(strip_default_namespace(child_e.tag)) - if decoded_k in klass.ignore_during_deserialization: - logger.debug(f'Ignoring {decoded_k} when deserializing {cls.__module__}.{cls.__qualname__}') - continue - - if decoded_k not in klass_properties: + if prop_info.custom_type and prop_info.is_helper_type(): + _data[decoded_k] = prop_info.custom_type.xml_deserialize(v) + elif prop_info.is_enum: + _data[decoded_k] = prop_info.concrete_type(v) + elif prop_info.is_primitive_type(): + _data[decoded_k] = prop_info.concrete_type(v) + else: + raise ValueError(f'Non-primitive types not supported from XML Attributes - see {decoded_k}') + + # Handle Node text content + if data.text: for p, pi in klass_properties.items(): - if pi.xml_array_config: - array_type, nested_name = pi.xml_array_config - if nested_name == decoded_k: - if array_type == XmlArraySerializationType.FLAT: - decoded_k = p - else: - decoded_k = '____SKIP_ME____' - elif pi.custom_names.get(SerializationType.XML, None) == decoded_k: - decoded_k = p + if pi.custom_names.get(SerializationType.XML, None) == '.': + _data[p] = data.text.strip() + + # Handle Sub-Elements + for child_e in data: + decoded_k = CurrentFormatter.formatter.decode(strip_default_namespace(child_e.tag)) + if decoded_k in klass.ignore_during_deserialization: + logger.debug(f'Ignoring {decoded_k} when deserializing {cls.__module__}.{cls.__qualname__}') + continue - if decoded_k == '____SKIP_ME____': - continue + if decoded_k not in klass_properties: + for p, pi in klass_properties.items(): + if pi.xml_array_config: + array_type, nested_name = pi.xml_array_config + if nested_name == decoded_k: + if array_type == XmlArraySerializationType.FLAT: + decoded_k = p + else: + decoded_k = '____SKIP_ME____' + elif pi.custom_names.get(SerializationType.XML, None) == decoded_k: + decoded_k = p + + if decoded_k == '____SKIP_ME____': + continue - prop_info = klass_properties.get(decoded_k, None) - if not prop_info: - raise ValueError(f'{decoded_k} is not a known Property for {cls.__module__}.{cls.__qualname__}') + prop_info = klass_properties.get(decoded_k, None) + if not prop_info: + raise ValueError(f'{decoded_k} is not a known Property for {cls.__module__}.{cls.__qualname__}') - try: + try: - logger.debug(f'Handling {prop_info}') + logger.debug(f'Handling {prop_info}') - if prop_info.is_array and prop_info.xml_array_config: - array_type, nested_name = prop_info.xml_array_config + if prop_info.is_array and prop_info.xml_array_config: + array_type, nested_name = prop_info.xml_array_config - if decoded_k not in _data: - _data[decoded_k] = [] + if decoded_k not in _data: + _data[decoded_k] = [] - if array_type == XmlArraySerializationType.NESTED: - for sub_child_e in child_e: + if array_type == XmlArraySerializationType.NESTED: + for sub_child_e in child_e: + if not prop_info.is_primitive_type() and not prop_info.is_enum: + _data[decoded_k].append(prop_info.concrete_type.from_xml( + data=sub_child_e, default_namespace=default_namespace) + ) + else: + _data[decoded_k].append(prop_info.concrete_type(sub_child_e.text)) + else: if not prop_info.is_primitive_type() and not prop_info.is_enum: _data[decoded_k].append(prop_info.concrete_type.from_xml( - data=sub_child_e, default_namespace=default_namespace) + data=child_e, default_namespace=default_namespace) ) + elif prop_info.custom_type: + if prop_info.is_helper_type(): + _data[decoded_k] = prop_info.custom_type.xml_deserialize(child_e) + else: + _data[decoded_k] = prop_info.custom_type(child_e.text) else: - _data[decoded_k].append(prop_info.concrete_type(sub_child_e.text)) - else: - if not prop_info.is_primitive_type() and not prop_info.is_enum: - _data[decoded_k].append(prop_info.concrete_type.from_xml( - data=child_e, default_namespace=default_namespace) - ) - elif prop_info.custom_type: - if prop_info.is_helper_type(): - _data[decoded_k] = prop_info.custom_type.xml_deserialize(child_e) - else: - _data[decoded_k] = prop_info.custom_type(child_e.text) + _data[decoded_k].append(prop_info.concrete_type(child_e.text)) + elif prop_info.custom_type: + if prop_info.is_helper_type(): + _data[decoded_k] = prop_info.custom_type.xml_deserialize(child_e.text) else: - _data[decoded_k].append(prop_info.concrete_type(child_e.text)) - elif prop_info.custom_type: - if prop_info.is_helper_type(): - _data[decoded_k] = prop_info.custom_type.xml_deserialize(child_e.text) - else: - _data[decoded_k] = prop_info.custom_type(child_e.text) - elif prop_info.is_enum: - _data[decoded_k] = prop_info.concrete_type(child_e.text) - elif not prop_info.is_primitive_type(): - global_klass_name = f'{prop_info.concrete_type.__module__}.{prop_info.concrete_type.__name__}' - if global_klass_name in ObjectMetadataLibrary.klass_mappings: - _data[decoded_k] = prop_info.concrete_type.from_xml( - data=child_e, default_namespace=default_namespace - ) - else: + _data[decoded_k] = prop_info.custom_type(child_e.text) + elif prop_info.is_enum: _data[decoded_k] = prop_info.concrete_type(child_e.text) - else: - if prop_info.concrete_type == bool: - _data[decoded_k] = True if str(child_e.text) in (1, 'true') else False + elif not prop_info.is_primitive_type(): + global_klass_name = f'{prop_info.concrete_type.__module__}.{prop_info.concrete_type.__name__}' + if global_klass_name in ObjectMetadataLibrary.klass_mappings: + _data[decoded_k] = prop_info.concrete_type.from_xml( + data=child_e, default_namespace=default_namespace + ) + else: + _data[decoded_k] = prop_info.concrete_type(child_e.text) else: - _data[decoded_k] = prop_info.concrete_type(child_e.text) - except AttributeError as e: - logging.error(f'There was an AttributeError deserializing JSON to {cls}.{os.linesep}' - f'The Property is: {prop_info}{os.linesep}' - f'The Value was: {v}{os.linesep}' - f'Exception: {e}{os.linesep}') - raise AttributeError( - f'There was an AttributeError deserializing XML to {cls} the Property {prop_info}: {e}' - ) + if prop_info.concrete_type == bool: + _data[decoded_k] = True if str(child_e.text) in (1, 'true') else False + else: + _data[decoded_k] = prop_info.concrete_type(child_e.text) + except AttributeError as e: + logging.error(f'There was an AttributeError deserializing JSON to {cls}.{os.linesep}' + f'The Property is: {prop_info}{os.linesep}' + f'The Value was: {v}{os.linesep}' + f'Exception: {e}{os.linesep}') + raise AttributeError( + f'There was an AttributeError deserializing XML to {cls} the Property {prop_info}: {e}' + ) - logging.debug(f'Creating {cls} from {_data}') + logging.debug(f'Creating {cls} from {_data}') - if len(_data) == 0: - return None + if len(_data) == 0: + return None - return cls(**_data) + return cls(**_data) def _namespace_element_name(tag_name: str, xmlns: Optional[str]) -> str: @@ -614,16 +638,16 @@ class ObjectMetadataLibrary: serialization and deserialization. """ _deferred_property_type_parsing: Dict[str, Set['ObjectMetadataLibrary.SerializableProperty']] = {} - _klass_views: Dict[str, Type[Any]] = {} + _klass_views: Dict[str, Type[ViewType]] = {} _klass_property_array_config: Dict[str, Tuple[XmlArraySerializationType, str]] = {} _klass_property_attributes: Set[str] = set() - _klass_property_include_none: Dict[str, Set[Tuple[_Klass, Any]]] = {} + _klass_property_include_none: Dict[str, Set[Tuple[Type[ViewType], Any]]] = {} _klass_property_names: Dict[str, Dict[SerializationType, str]] = {} _klass_property_string_formats: Dict[str, str] = {} - _klass_property_types: Dict[str, Type[Any]] = {} - _klass_property_views: Dict[str, Set[_Klass]] = {} + _klass_property_types: Dict[str, type] = {} + _klass_property_views: Dict[str, Set[Type[ViewType]]] = {} _klass_property_xml_sequence: Dict[str, int] = {} - custom_enum_klasses: Set[_Klass] = set() + custom_enum_klasses: Set[Type[enum.Enum]] = set() klass_mappings: Dict[str, 'ObjectMetadataLibrary.SerializableClass'] = {} klass_property_mappings: Dict[str, Dict[str, 'ObjectMetadataLibrary.SerializableProperty']] = {} @@ -633,7 +657,7 @@ class SerializableClass: (de-)serialization. """ - def __init__(self, *, klass: Any, custom_name: Optional[str] = None, + def __init__(self, *, klass: type, custom_name: Optional[str] = None, serialization_types: Optional[Iterable[SerializationType]] = None, ignore_during_deserialization: Optional[Iterable[str]] = None) -> None: self._name = str(klass.__name__) @@ -649,7 +673,7 @@ def name(self) -> str: return self._name @property - def klass(self) -> Any: + def klass(self) -> type: return self._klass @property @@ -679,9 +703,10 @@ class SerializableProperty: _PRIMITIVE_TYPES = (bool, int, float, str) def __init__(self, *, prop_name: str, prop_type: Any, custom_names: Dict[SerializationType, str], - custom_type: Optional[Any] = None, include_none_config: Optional[Set[Tuple[_Klass, Any]]] = None, + custom_type: Optional[Any] = None, + include_none_config: Optional[Set[Tuple[Type[ViewType], Any]]] = None, is_xml_attribute: bool = False, string_format_: Optional[str] = None, - views: Optional[Iterable[_Klass]] = None, + views: Optional[Iterable[Type[ViewType]]] = None, xml_array_config: Optional[Tuple[XmlArraySerializationType, str]] = None, xml_sequence_: Optional[int] = None) -> None: @@ -736,17 +761,17 @@ def include_none(self) -> bool: return self._include_none @property - def include_none_views(self) -> Set[Tuple[_Klass, Any]]: + def include_none_views(self) -> Set[Tuple[Type[ViewType], Any]]: return self._include_none_views - def include_none_for_view(self, view_: _Klass) -> bool: + def include_none_for_view(self, view_: Type[ViewType]) -> bool: for _v, _a in self._include_none_views: if _v == view_: return True return False - def get_none_value_for_view(self, view_: Optional[Type[_Klass]]) -> Any: + def get_none_value_for_view(self, view_: Optional[Type[ViewType]]) -> Any: if view_: for _v, _a in self._include_none_views: if _v == view_: @@ -762,7 +787,7 @@ def string_format(self) -> Optional[str]: return self._string_format @property - def views(self) -> Set[_Klass]: + def views(self) -> Set[Type[ViewType]]: return self._views @property @@ -785,14 +810,13 @@ def is_optional(self) -> bool: def xml_sequence(self) -> int: return self._xml_sequence - def get_none_value(self, view_: Optional[_Klass] = None) -> Any: + def get_none_value(self, view_: Optional[Type[ViewType]] = None) -> Any: if not self.include_none: raise ValueError('No None Value for property that is not include_none') def is_helper_type(self) -> bool: - if inspect.isclass(self.custom_type): - return issubclass(self.custom_type, BaseHelper) - return False + ct = self.custom_type + return inspect.isclass(ct) and issubclass(ct, BaseHelper) def is_primitive_type(self) -> bool: return self.concrete_type in self._PRIMITIVE_TYPES @@ -847,9 +871,9 @@ def _parse_type(self, type_: Any) -> None: self._type_ = mapped_array_type[_k] # type: ignore self._concrete_type = _k # type: ignore - elif results.get('array_type', None).replace('typing.', '') in self._ARRAY_TYPES: + elif results.get('array_type', '').replace('typing.', '') in self._ARRAY_TYPES: mapped_array_type = self._ARRAY_TYPES.get( - str(results.get('array_type', None).replace('typing.', '')) + str(results.get('array_type', '').replace('typing.', '')) ) self._is_array = True try: @@ -946,24 +970,25 @@ def defer_property_type_parsing(cls, prop: 'ObjectMetadataLibrary.SerializablePr ObjectMetadataLibrary._deferred_property_type_parsing[_k].add(prop) @classmethod - def is_klass_serializable(cls, klass: _T) -> bool: + def is_klass_serializable(cls, klass: Any) -> bool: if type(klass) is Type: return f'{klass.__module__}.{klass.__name__}' in cls.klass_mappings # type: ignore return klass in cls.klass_mappings @classmethod - def is_property(cls, o: object) -> bool: + def is_property(cls, o: Any) -> bool: return isinstance(o, property) @classmethod - def register_enum(cls, klass: _T) -> _T: + def register_enum(cls, klass: Type[_E]) -> Type[_E]: cls.custom_enum_klasses.add(klass) return klass @classmethod - def register_klass(cls, klass: _T, custom_name: Optional[str], + def register_klass(cls, klass: Type[_T], custom_name: Optional[str], serialization_types: Iterable[SerializationType], - ignore_during_deserialization: Optional[Iterable[str]] = None) -> _T: + ignore_during_deserialization: Optional[Iterable[str]] = None + ) -> Intersection[Type[_T], Type[_JsonSerializable], Type[_XmlSerializable]]: if cls.is_klass_serializable(klass=klass): return klass @@ -1005,12 +1030,12 @@ def register_klass(cls, klass: _T, custom_name: Optional[str], }) if SerializationType.JSON in serialization_types: - klass.as_json = _as_json # type: ignore - klass.from_json = classmethod(_from_json) # type: ignore + klass.as_json = _JsonSerializable.as_json # type:ignore[attr-defined] + klass.from_json = classmethod(_JsonSerializable.from_json.__func__) # type:ignore[attr-defined] if SerializationType.XML in serialization_types: - klass.as_xml = _as_xml # type: ignore - klass.from_xml = classmethod(_from_xml) # type: ignore + klass.as_xml = _XmlSerializable.as_xml # type:ignore[attr-defined] + klass.from_xml = classmethod(_XmlSerializable.from_xml.__func__) # type:ignore[attr-defined] # Handle any deferred Properties depending on this class if klass.__qualname__ in ObjectMetadataLibrary._deferred_property_type_parsing: @@ -1038,24 +1063,24 @@ def register_custom_xml_property_name(cls, qual_name: str, xml_property_name: st cls._klass_property_names.update({qual_name: {SerializationType.XML: xml_property_name}}) @classmethod - def register_klass_view(cls, klass: _T, view_: Type[Any]) -> _T: + def register_klass_view(cls, klass: Type[_T], view_: Type[ViewType]) -> Type[_T]: ObjectMetadataLibrary._klass_views.update({ f'{klass.__module__}.{klass.__qualname__}': view_ }) return klass @classmethod - def register_property_include_none(cls, qual_name: str, view_: Optional[_Klass] = None, + def register_property_include_none(cls, qual_name: str, view_: Optional[Type[ViewType]] = None, none_value: Optional[Any] = None) -> None: if qual_name not in cls._klass_property_include_none: cls._klass_property_include_none.update({qual_name: set()}) if view_: cls._klass_property_include_none.get(qual_name, set()).add((view_, none_value)) else: - cls._klass_property_include_none.get(qual_name, set()).add((_Klass, none_value)) + cls._klass_property_include_none.get(qual_name, set()).add((ViewType, none_value)) @classmethod - def register_property_view(cls, qual_name: str, view_: _T) -> None: + def register_property_view(cls, qual_name: str, view_: Type[ViewType]) -> None: if qual_name not in ObjectMetadataLibrary._klass_property_views: ObjectMetadataLibrary._klass_property_views.update({qual_name: {view_}}) else: @@ -1075,14 +1100,27 @@ def register_xml_property_sequence(cls, qual_name: str, sequence: int) -> None: cls._klass_property_xml_sequence.update({qual_name: sequence}) @classmethod - def register_property_type_mapping(cls, qual_name: str, mapped_type: Any) -> None: + def register_property_type_mapping(cls, qual_name: str, mapped_type: type) -> None: cls._klass_property_types.update({qual_name: mapped_type}) -def serializable_enum(cls: Optional[Any] = None) -> Any: +@overload +def serializable_enum(cls: Literal[None] = None) -> Callable[[Type[_E]], Type[_E]]: + ... + + +@overload +def serializable_enum(cls: Type[_E]) -> Type[_E]: # type:ignore[misc] # mypy on py37 + ... + + +def serializable_enum(cls: Optional[Type[_E]] = None) -> Union[ + Callable[[Type[_E]], Type[_E]], + Type[_E] +]: """Decorator""" - def decorate(kls: Type[_T]) -> Type[_T]: + def decorate(kls: Type[_E]) -> Type[_E]: ObjectMetadataLibrary.register_enum(klass=kls) return kls @@ -1095,10 +1133,35 @@ def decorate(kls: Type[_T]) -> Type[_T]: return decorate(cls) -def serializable_class(cls: Optional[Any] = None, *, name: Optional[str] = None, - serialization_types: Optional[Iterable[SerializationType]] = None, - ignore_during_deserialization: Optional[Iterable[str]] = None - ) -> Any: +@overload +def serializable_class( + cls: Literal[None] = None, *, + name: Optional[str] = ..., + serialization_types: Optional[Iterable[SerializationType]] = ..., + ignore_during_deserialization: Optional[Iterable[str]] = ... +) -> Callable[[Type[_T]], Intersection[Type[_T], Type[_JsonSerializable], Type[_XmlSerializable]]]: + ... + + +@overload +def serializable_class( # type:ignore[misc] # mypy on py37 + cls: Type[_T], *, + name: Optional[str] = ..., + serialization_types: Optional[Iterable[SerializationType]] = ..., + ignore_during_deserialization: Optional[Iterable[str]] = ... +) -> Intersection[Type[_T], Type[_JsonSerializable], Type[_XmlSerializable]]: + ... + + +def serializable_class( + cls: Optional[Type[_T]] = None, *, + name: Optional[str] = None, + serialization_types: Optional[Iterable[SerializationType]] = None, + ignore_during_deserialization: Optional[Iterable[str]] = None +) -> Union[ + Callable[[Type[_T]], Intersection[Type[_T], Type[_JsonSerializable], Type[_XmlSerializable]]], + Intersection[Type[_T], Type[_JsonSerializable], Type[_XmlSerializable]] +]: """ Decorator used to tell ``serializable`` that a class is to be included in (de-)serialization. @@ -1111,7 +1174,7 @@ def serializable_class(cls: Optional[Any] = None, *, name: Optional[str] = None, if serialization_types is None: serialization_types = _DEFAULT_SERIALIZATION_TYPES - def decorate(kls: Type[_T]) -> Type[_T]: + def decorate(kls: Type[_T]) -> Intersection[Type[_T], Type[_JsonSerializable], Type[_XmlSerializable]]: ObjectMetadataLibrary.register_klass( klass=kls, custom_name=name, serialization_types=serialization_types or [], ignore_during_deserialization=ignore_during_deserialization @@ -1127,7 +1190,7 @@ def decorate(kls: Type[_T]) -> Type[_T]: return decorate(cls) -def type_mapping(type_: _T) -> Callable[[_F], _F]: +def type_mapping(type_: type) -> Callable[[_F], _F]: """Decorator""" def decorate(f: _F) -> _F: @@ -1140,7 +1203,7 @@ def decorate(f: _F) -> _F: return decorate -def include_none(view_: Optional[Type[_T]] = None, none_value: Optional[Any] = None) -> Callable[[_F], _F]: +def include_none(view_: Optional[Type[ViewType]] = None, none_value: Optional[Any] = None) -> Callable[[_F], _F]: """Decorator""" def decorate(f: _F) -> _F: @@ -1179,7 +1242,7 @@ def decorate(f: _F) -> _F: return decorate -def view(view_: ViewType, ) -> Callable[[_F], _F]: +def view(view_: Type[ViewType]) -> Callable[[_F], _F]: """Decorator""" def decorate(f: _F) -> _F: diff --git a/serializable/helpers.py b/serializable/helpers.py index 198a7fe..fdd5c9f 100644 --- a/serializable/helpers.py +++ b/serializable/helpers.py @@ -78,14 +78,14 @@ class Iso8601Date(BaseHelper): _PATTERN_DATE = '%Y-%m-%d' @classmethod - def serialize(cls, o: object) -> str: + def serialize(cls, o: Any) -> str: if isinstance(o, date): return o.strftime(Iso8601Date._PATTERN_DATE) raise ValueError(f'Attempt to serialize a non-date: {o.__class__}') @classmethod - def deserialize(cls, o: object) -> date: + def deserialize(cls, o: Any) -> date: try: return date.fromisoformat(str(o)) except ValueError: @@ -95,14 +95,14 @@ def deserialize(cls, o: object) -> date: class XsdDate(BaseHelper): @classmethod - def serialize(cls, o: object) -> str: + def serialize(cls, o: Any) -> str: if isinstance(o, date): return o.isoformat() raise ValueError(f'Attempt to serialize a non-date: {o.__class__}') @classmethod - def deserialize(cls, o: object) -> date: + def deserialize(cls, o: Any) -> date: try: if str(o).startswith('-'): # Remove any leading hyphen @@ -128,14 +128,14 @@ def deserialize(cls, o: object) -> date: class XsdDateTime(BaseHelper): @classmethod - def serialize(cls, o: object) -> str: + def serialize(cls, o: Any) -> str: if isinstance(o, datetime): return o.isoformat() raise ValueError(f'Attempt to serialize a non-date: {o.__class__}') @classmethod - def deserialize(cls, o: object) -> datetime: + def deserialize(cls, o: Any) -> datetime: try: if str(o).startswith('-'): # Remove any leading hyphen diff --git a/tests/base.py b/tests/base.py index aefbb59..0b505cb 100644 --- a/tests/base.py +++ b/tests/base.py @@ -62,9 +62,9 @@ def assertEqualXml(self, a: str, b: str) -> None: class DeepCompareMixin(object): - def assertDeepEqual(self, first: Any, second: Any, msg: Optional[str] = None) -> None: + def assertDeepEqual(self: Union[TestCase, 'DeepCompareMixin'], + first: Any, second: Any, msg: Optional[str] = None) -> None: """costly compare, but very verbose""" - self: Union[TestCase, 'DeepCompareMixin'] _omd = self.maxDiff try: self.maxDiff = None diff --git a/tests/model.py b/tests/model.py index fbf0bfc..141fed6 100644 --- a/tests/model.py +++ b/tests/model.py @@ -20,7 +20,7 @@ import re from datetime import date from enum import Enum, unique -from typing import Iterable, List, Optional, Set +from typing import Any, Dict, Iterable, List, Optional, Set, Type from uuid import UUID, uuid4 import serializable @@ -28,8 +28,7 @@ from serializable.helpers import BaseHelper, Iso8601Date """ -Model classes used in unit tests. - +Model classes used in unit tests and examples. """ @@ -49,17 +48,25 @@ class SchemaVersion4(ViewType): pass +SCHEMAVERSION_MAP: Dict[int, Type[ViewType]] = { + 1: SchemaVersion1, + 2: SchemaVersion2, + 3: SchemaVersion3, + 4: SchemaVersion4, +} + + class ReferenceReferences(BaseHelper): @classmethod - def serialize(cls, o: object) -> Set[str]: + def serialize(cls, o: Any) -> Set[str]: if isinstance(o, set): return set(map(lambda i: str(i.ref), o)) raise ValueError(f'Attempt to serialize a non-set: {o.__class__}') @classmethod - def deserialize(cls, o: object) -> Set["BookReference"]: + def deserialize(cls, o: Any) -> Set["BookReference"]: print(f'Deserializing {o} ({type(o)})') references: Set["BookReference"] = set() if isinstance(o, list): @@ -104,7 +111,7 @@ def number(self) -> int: def title(self) -> str: return self._title - def __eq__(self, other: object) -> bool: + def __eq__(self, other: Any) -> bool: if isinstance(other, Chapter): return hash(other) == hash(self) return False @@ -341,3 +348,15 @@ def references(self, references: Iterable[BookReference]) -> None: ThePhoenixProject_v2.references = {Ref3, Ref2, Ref1} ThePhoenixProject = ThePhoenixProject_v2 + +if __name__ == '__main__': + tpp_as_xml = ThePhoenixProject.as_xml() # type:ignore[attr-defined] + tpp_as_json = ThePhoenixProject.as_json() # type:ignore[attr-defined] + print(tpp_as_xml, tpp_as_json, sep='\n\n') + + import io + import json + tpp_from_xml = ThePhoenixProject.from_xml( # type:ignore[attr-defined] + io.StringIO(tpp_as_xml)) + tpp_from_json = ThePhoenixProject.from_json( # type:ignore[attr-defined] + json.loads(tpp_as_json))