Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use own logger, not global logger #10

Closed
wants to merge 14 commits into from
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ See also:
getting-started
customising-structure
formatters
logging
support
changelog

Expand Down
39 changes: 39 additions & 0 deletions docs/logging.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Logging
====================================================

This library utilizes an own instance of `Logger`_, which you may access and add handlers to.

.. _logger: https://docs.python.org/3/library/logging.html#logger-objects


.. code-block:: python
:caption: Example: send all logs messages to the console

import sys
import logging
import serializable

my_log_handler = logging.StreamHandler(sys.stderr)
my_log_handler.setLevel(logging.DEBUG)
my_log_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
serializable.LOGGER.addHandler(my_log_handler)


@serializable.serializable_class
class Chapter:

def __init__(self, *, number: int, title: str) -> None:
self._number = number
self._title = title

@property
def number(self) -> int:
return self._number

@property
def title(self) -> str:
return self._title


moby_dick_c1 = Chapter(number=1, title='Loomings')
print(moby_dick_c1.as_json())
62 changes: 33 additions & 29 deletions serializable/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) Paul Horton. All Rights Reserved.

import enum
import functools
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
Expand All @@ -40,15 +39,16 @@
else:
from typing_extensions import Protocol # type: ignore

from ._logging import _LOGGER, _warning_kwargs
from .formatters import BaseNameFormatter, CurrentFormatter
from .helpers import BaseHelper

# !! version is managed by semantic_release
# do not use typing here, or else `semantic_release` might have issues finding the variable
__version__ = '0.12.0'

logger = logging.getLogger('serializable')
Copy link
Collaborator

@jkowalleck jkowalleck Sep 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


bring back logger and mark it as deprecated.
even though logger was not used for any logging in the past, it still was public API.

but all UPPERCASE is preferred to point downstream users to the fact, that is a readonly/constant value

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or leave as is and consider it a breaking change -- which is okay for a planned v1.0.0

logger.setLevel(logging.INFO)
# make logger publicly available, as stable API
LOGGER = _LOGGER

_F = TypeVar("_F", bound=Callable[..., Any])
_T = TypeVar('_T', bound='_Klass')
Expand Down Expand Up @@ -227,7 +227,7 @@ 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_}...')
_LOGGER.debug(f'Dumping {self} to JSON with view: {view_}...')
return json.dumps(self, cls=_SerializableJsonEncoder, view_=view_)


Expand All @@ -236,13 +236,15 @@ 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}...')
_LOGGER.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)
_LOGGER.warning(
f'{klass_qualified_name} is not a known serializable class',
)
return None

if len(klass_properties) == 1:
Expand All @@ -255,7 +257,7 @@ def _from_json(cls: Type[_T], data: Dict[str, Any]) -> object:
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__}')
_LOGGER.debug(f'Ignoring {k} when deserializing {cls.__module__}.{cls.__qualname__}')
del _data[k]
continue

Expand All @@ -268,7 +270,7 @@ def _from_json(cls: Type[_T], data: Dict[str, Any]) -> object:
new_key = decoded_k

if new_key is None:
logger.error(
_LOGGER.error(
f'Unexpected key {k}/{decoded_k} in data being serialized to {cls.__module__}.{cls.__qualname__}'
)
raise ValueError(
Expand Down Expand Up @@ -306,22 +308,22 @@ def _from_json(cls: Type[_T], data: Dict[str, Any]) -> object:
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}'
_LOGGER.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}'
)

logging.debug(f'Creating {cls} from {_data}')
_LOGGER.debug(f'Creating {cls} from {_data}')

return cls(**_data)


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_}...')
_LOGGER.debug(f'Dumping {self} to XML with view {view_}...')

this_e_attributes = {}
klass_qualified_name = f'{self.__module__}.{self.__class__.__qualname__}'
Expand Down Expand Up @@ -445,10 +447,12 @@ def _as_xml(self: _T, view_: Optional[Type[_T]] = None, as_string: bool = True,

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}...')
_LOGGER.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)
_LOGGER.warning(
f'{cls.__module__}.{cls.__qualname__} is not a known serializable class',
**_warning_kwargs) # type:ignore[arg-type]
return None

klass_properties = ObjectMetadataLibrary.klass_property_mappings.get(f'{cls.__module__}.{cls.__qualname__}', {})
Expand All @@ -471,7 +475,7 @@ def _from_xml(cls: Type[_T], data: Union[TextIOWrapper, Element],
for k, v in data.attrib.items():
decoded_k = CurrentFormatter.formatter.decode(property_name=k)
if decoded_k in klass.ignore_during_deserialization:
logger.debug(f'Ignoring {decoded_k} when deserializing {cls.__module__}.{cls.__qualname__}')
_LOGGER.debug(f'Ignoring {decoded_k} when deserializing {cls.__module__}.{cls.__qualname__}')
continue

if decoded_k not in klass_properties:
Expand Down Expand Up @@ -505,7 +509,7 @@ def _from_xml(cls: Type[_T], data: Union[TextIOWrapper, Element],

decoded_k = CurrentFormatter.formatter.decode(property_name=child_e_tag_name)
if decoded_k in klass.ignore_during_deserialization:
logger.debug(f'Ignoring {decoded_k} when deserializing {cls.__module__}.{cls.__qualname__}')
_LOGGER.debug(f'Ignoring {decoded_k} when deserializing {cls.__module__}.{cls.__qualname__}')
continue

if decoded_k not in klass_properties:
Expand All @@ -529,7 +533,7 @@ def _from_xml(cls: Type[_T], data: Union[TextIOWrapper, Element],

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
Expand Down Expand Up @@ -578,15 +582,15 @@ def _from_xml(cls: Type[_T], data: Union[TextIOWrapper, Element],
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}'
_LOGGER.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}')
_LOGGER.debug(f'Creating {cls} from {_data}')

if len(_data) == 0:
return None
Expand Down Expand Up @@ -970,7 +974,7 @@ def register_klass(cls, klass: _T, custom_name: Optional[str],

qualified_class_name = f'{klass.__module__}.{klass.__qualname__}'
cls.klass_property_mappings.update({qualified_class_name: {}})
logging.debug(f'Registering Class {qualified_class_name} with custom name {custom_name}')
_LOGGER.debug(f'Registering Class {qualified_class_name} with custom name {custom_name}')
for name, o in inspect.getmembers(klass, ObjectMetadataLibrary.is_property):
qualified_property_name = f'{qualified_class_name}.{name}'
prop_arg_specs = inspect.getfullargspec(o.fget)
Expand Down Expand Up @@ -1127,7 +1131,7 @@ def type_mapping(type_: _T) -> Callable[[_F], _F]:
"""

def outer(f: _F) -> _F:
logger.debug(f'Registering {f.__module__}.{f.__qualname__} with custom type: {type_}')
_LOGGER.debug(f'Registering {f.__module__}.{f.__qualname__} with custom type: {type_}')
ObjectMetadataLibrary.register_property_type_mapping(
qual_name=f'{f.__module__}.{f.__qualname__}', mapped_type=type_
)
Expand All @@ -1143,7 +1147,7 @@ def inner(*args: Any, **kwargs: Any) -> Any:

def include_none(view_: Optional[Type[_T]] = None, none_value: Optional[Any] = None) -> Callable[[_F], _F]:
def outer(f: _F) -> _F:
logger.debug(f'Registering {f.__module__}.{f.__qualname__} to include None for view: {view_}')
_LOGGER.debug(f'Registering {f.__module__}.{f.__qualname__} to include None for view: {view_}')
ObjectMetadataLibrary.register_property_include_none(
qual_name=f'{f.__module__}.{f.__qualname__}', view_=view_, none_value=none_value
)
Expand All @@ -1159,7 +1163,7 @@ def inner(*args: Any, **kwargs: Any) -> Any:

def json_name(name: str) -> Callable[[_F], _F]:
def outer(f: _F) -> _F:
logger.debug(f'Registering {f.__module__}.{f.__qualname__} with JSON name: {name}')
_LOGGER.debug(f'Registering {f.__module__}.{f.__qualname__} with JSON name: {name}')
ObjectMetadataLibrary.register_custom_json_property_name(
qual_name=f'{f.__module__}.{f.__qualname__}', json_property_name=name
)
Expand All @@ -1175,7 +1179,7 @@ def inner(*args: Any, **kwargs: Any) -> Any:

def string_format(format_: str) -> Callable[[_F], _F]:
def outer(f: _F) -> _F:
logger.debug(f'Registering {f.__module__}.{f.__qualname__} with String Format: {format_}')
_LOGGER.debug(f'Registering {f.__module__}.{f.__qualname__} with String Format: {format_}')
ObjectMetadataLibrary.register_custom_string_format(
qual_name=f'{f.__module__}.{f.__qualname__}', string_format=format_
)
Expand All @@ -1191,7 +1195,7 @@ def inner(*args: Any, **kwargs: Any) -> Any:

def view(view_: ViewType) -> Callable[[_F], _F]:
def outer(f: _F) -> _F:
logger.debug(f'Registering {f.__module__}.{f.__qualname__} with View: {view_}')
_LOGGER.debug(f'Registering {f.__module__}.{f.__qualname__} with View: {view_}')
ObjectMetadataLibrary.register_property_view(
qual_name=f'{f.__module__}.{f.__qualname__}', view_=view_
)
Expand All @@ -1207,7 +1211,7 @@ def inner(*args: Any, **kwargs: Any) -> Any:

def xml_attribute() -> Callable[[_F], _F]:
def outer(f: _F) -> _F:
logger.debug(f'Registering {f.__module__}.{f.__qualname__} as XML attribute')
_LOGGER.debug(f'Registering {f.__module__}.{f.__qualname__} as XML attribute')
ObjectMetadataLibrary.register_xml_property_attribute(qual_name=f'{f.__module__}.{f.__qualname__}')

@functools.wraps(f)
Expand All @@ -1221,7 +1225,7 @@ def inner(*args: Any, **kwargs: Any) -> Any:

def xml_array(array_type: XmlArraySerializationType, child_name: str) -> Callable[[_F], _F]:
def outer(f: _F) -> _F:
logger.debug(f'Registering {f.__module__}.{f.__qualname__} as XML Array: {array_type}:{child_name}')
_LOGGER.debug(f'Registering {f.__module__}.{f.__qualname__} as XML Array: {array_type}:{child_name}')
ObjectMetadataLibrary.register_xml_property_array_config(
qual_name=f'{f.__module__}.{f.__qualname__}', array_type=array_type, child_name=child_name
)
Expand All @@ -1237,7 +1241,7 @@ def inner(*args: Any, **kwargs: Any) -> Any:

def xml_name(name: str) -> Callable[[_F], _F]:
def outer(f: _F) -> _F:
logger.debug(f'Registering {f.__module__}.{f.__qualname__} with XML name: {name}')
_LOGGER.debug(f'Registering {f.__module__}.{f.__qualname__} with XML name: {name}')
ObjectMetadataLibrary.register_custom_xml_property_name(
qual_name=f'{f.__module__}.{f.__qualname__}', xml_property_name=name
)
Expand All @@ -1253,7 +1257,7 @@ def inner(*args: Any, **kwargs: Any) -> Any:

def xml_sequence(sequence: int) -> Callable[[_F], _F]:
def outer(f: _F) -> _F:
logger.debug(f'Registering {f.__module__}.{f.__qualname__} with XML sequence: {sequence}')
_LOGGER.debug(f'Registering {f.__module__}.{f.__qualname__} with XML sequence: {sequence}')
ObjectMetadataLibrary.register_xml_property_sequence(
qual_name=f'{f.__module__}.{f.__qualname__}', sequence=sequence
)
Expand Down
27 changes: 27 additions & 0 deletions serializable/_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# encoding: utf-8

# This file is part of py-serializable
#
# 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.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) Paul Horton. All Rights Reserved.

import logging
from sys import version_info

_LOGGER = logging.getLogger(f'{__name__}.LOGGER')
_LOGGER.setLevel(logging.DEBUG)

# logger.warning() got additional kwarg since py38
_warning_kwargs = {'stacklevel': 2} if version_info >= (3, 8) else {}
17 changes: 8 additions & 9 deletions serializable/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) Paul Horton. All Rights Reserved.
import re
import warnings
from abc import ABC, abstractmethod
from datetime import date, datetime
from typing import Any

from ._logging import _LOGGER, _warning_kwargs


class BaseHelper(ABC):

Expand Down Expand Up @@ -75,16 +76,14 @@ def deserialize(cls, o: object) -> date:

if str(o).endswith('Z'):
o = str(o)[:-1]
warnings.warn(
'Potential data loss will occur: dates with timezones not supported in Python', UserWarning,
stacklevel=2
)
_LOGGER.warning(
'Potential data loss will occur: dates with timezones not supported in Python',
**_warning_kwargs) # type:ignore[arg-type]
if '+' in str(o):
o = str(o)[:str(o).index('+')]
warnings.warn(
'Potential data loss will occur: dates with timezones not supported in Python', UserWarning,
stacklevel=2
)
_LOGGER.warning(
'Potential data loss will occur: dates with timezones not supported in Python',
**_warning_kwargs) # type:ignore[arg-type]
return date.fromisoformat(str(o))
except ValueError:
raise ValueError(f'Date string supplied ({o}) is not a supported ISO Format')
Expand Down
Loading