"""
Helper Wizard Mixin classes.
"""
__all__ = ['JSONListWizard',
'JSONFileWizard',
'TOMLWizard',
'YAMLWizard']
import json
from .bases_meta import DumpMeta
from .class_helper import _META
from .dumpers import asdict
from .enums import LetterCase
from .lazy_imports import toml, toml_w, yaml
from .loader_selection import fromdict, fromlist
from .models import Container
from .serial_json import JSONSerializable
[docs]
class JSONListWizard(JSONSerializable, str=False):
"""
A Mixin class that extends :class:`JSONSerializable` (JSONWizard)
to return :class:`Container` - instead of `list` - objects.
Note that `Container` objects are simply convenience wrappers around a
collection of dataclass instances. For all intents and purposes, they
behave exactly the same as `list` objects, with some added helper methods:
* ``prettify`` - Convert the list of instances to a *prettified* JSON
string.
* ``to_json`` - Convert the list of instances to a JSON string.
* ``to_json_file`` - Serialize the list of instances and write it to a
JSON file.
"""
[docs]
@classmethod
def from_json(cls, string, *,
decoder=json.loads,
**decoder_kwargs):
"""
Converts a JSON `string` to an instance of the dataclass, or a
Container (list) of the dataclass instances.
"""
o = decoder(string, **decoder_kwargs)
if isinstance(o, dict):
return fromdict(cls, o)
return Container[cls](fromlist(cls, o))
[docs]
@classmethod
def from_list(cls, o):
"""
Converts a Python `list` object to a Container (list) of the dataclass
instances.
"""
return Container[cls](fromlist(cls, o))
[docs]
class JSONFileWizard:
"""
A Mixin class that makes it easier to interact with JSON files.
This can be paired with the :class:`JSONSerializable` (JSONWizard) Mixin
class for more complete extensibility.
"""
[docs]
@classmethod
def from_json_file(cls, file, *,
decoder=json.load,
**decoder_kwargs):
"""
Reads in the JSON file contents and converts to an instance of the
dataclass, or a list of the dataclass instances.
"""
with open(file) as in_file:
o = decoder(in_file, **decoder_kwargs)
return fromdict(cls, o) if isinstance(o, dict) else fromlist(cls, o)
[docs]
def to_json_file(self, file, mode='w',
encoder=json.dump,
**encoder_kwargs):
"""
Serializes the instance and writes it to a JSON file.
"""
with open(file, mode) as out_file:
encoder(asdict(self), out_file, **encoder_kwargs)
[docs]
class TOMLWizard:
# noinspection PyUnresolvedReferences
"""
A Mixin class that makes it easier to interact with TOML data.
.. NOTE::
By default, *NO* key transform is used in the TOML dump process.
In practice, this means that a `snake_case` field name in Python is saved
as `snake_case` to TOML; however, this can easily be customized without
the need to sub-class from :class:`JSONWizard`.
For example:
>>> @dataclass
>>> class MyClass(TOMLWizard, key_transform='CAMEL'):
>>> ...
"""
def __init_subclass__(cls, key_transform=LetterCase.NONE):
"""Allow easy setup of common config, such as key casing transform."""
# Only add the key transform if Meta config has not been specified
# for the dataclass.
if key_transform and cls not in _META:
DumpMeta(key_transform=key_transform).bind_to(cls)
[docs]
@classmethod
def from_toml(cls,
string_or_stream, *,
decoder=None,
header='items',
parse_float=float):
"""
Converts a TOML `string` to an instance of the dataclass, or a list of
the dataclass instances.
If ``header`` is provided and the corresponding value in the parsed
data is a ``list``, the return type is ``List[T]``.
"""
if decoder is None: # pragma: no cover
decoder = toml.loads
o = decoder(string_or_stream, parse_float=parse_float)
return (fromlist(cls, maybe_l)
if (maybe_l := o.get(header)) and isinstance(maybe_l, list)
else fromdict(cls, o))
[docs]
@classmethod
def from_toml_file(cls, file, *,
decoder=None,
header='items',
parse_float=float):
"""
Reads the contents of a TOML file and converts them
into an instance (or list of instances) of the dataclass.
Similar to :meth:`from_toml`, it can return a list if ``header``
is specified and points to a list in the TOML data.
"""
if decoder is None: # pragma: no cover
decoder = toml.load
with open(file, 'rb') as in_file:
return cls.from_toml(in_file,
decoder=decoder,
header=header,
parse_float=parse_float)
[docs]
def to_toml(self,
/,
*encoder_args,
encoder=None,
multiline_strings=False,
indent=4):
"""
Converts a dataclass instance to a TOML `string`.
Optional parameters include ``multiline_strings``
for enabling/disabling multiline formatting of strings,
and ``indent`` for setting the indentation level.
"""
if encoder is None: # pragma: no cover
encoder = toml_w.dumps
return encoder(asdict(self), *encoder_args,
multiline_strings=multiline_strings,
indent=indent)
[docs]
def to_toml_file(self, file, mode='wb',
encoder=None,
multiline_strings=False,
indent=4):
"""
Serializes a dataclass instance and writes it to a TOML file.
By default, opens the file in "write binary" mode.
"""
if encoder is None: # pragma: no cover
encoder = toml_w.dump
with open(file, mode) as out_file:
self.to_toml(out_file, encoder=encoder,
multiline_strings=multiline_strings,
indent=indent)
[docs]
@classmethod
def list_to_toml(cls,
instances,
header='items',
encoder=None,
**encoder_kwargs):
"""
Serializes a ``list`` of dataclass instances into a TOML `string`,
grouped under a specified header.
"""
if encoder is None:
encoder = toml_w.dumps
list_of_dict = [asdict(o, cls=cls) for o in instances]
return encoder({header: list_of_dict}, **encoder_kwargs)
[docs]
class YAMLWizard:
# noinspection PyUnresolvedReferences
"""
A Mixin class that makes it easier to interact with YAML data.
.. NOTE::
The default key transform used in the YAML dump process is `lisp-case`,
however this can easily be customized without the need to sub-class
from :class:`JSONWizard`.
For example:
>>> @dataclass
>>> class MyClass(YAMLWizard, key_transform='CAMEL'):
>>> ...
"""
def __init_subclass__(cls, key_transform=LetterCase.LISP):
"""Allow easy setup of common config, such as key casing transform."""
# Only add the key transform if Meta config has not been specified
# for the dataclass.
if key_transform and cls not in _META:
DumpMeta(key_transform=key_transform).bind_to(cls)
[docs]
@classmethod
def from_yaml(cls,
string_or_stream, *,
decoder=None,
**decoder_kwargs):
"""
Converts a YAML `string` to an instance of the dataclass, or a list of
the dataclass instances.
"""
if decoder is None:
decoder = yaml.safe_load
o = decoder(string_or_stream, **decoder_kwargs)
return fromdict(cls, o) if isinstance(o, dict) else fromlist(cls, o)
[docs]
@classmethod
def from_yaml_file(cls, file, *,
decoder=None,
**decoder_kwargs):
"""
Reads in the YAML file contents and converts to an instance of the
dataclass, or a list of the dataclass instances.
"""
with open(file) as in_file:
return cls.from_yaml(in_file, decoder=decoder,
**decoder_kwargs)
[docs]
def to_yaml(self, *,
encoder=None,
**encoder_kwargs):
"""
Converts the dataclass instance to a YAML `string` representation.
"""
if encoder is None:
encoder = yaml.dump
return encoder(asdict(self), **encoder_kwargs)
[docs]
def to_yaml_file(self, file, mode='w',
encoder = None,
**encoder_kwargs):
"""
Serializes the instance and writes it to a YAML file.
"""
with open(file, mode) as out_file:
self.to_yaml(stream=out_file, encoder=encoder,
**encoder_kwargs)
[docs]
@classmethod
def list_to_yaml(cls,
instances,
encoder = None,
**encoder_kwargs):
"""
Converts a ``list`` of dataclass instances to a YAML `string`
representation.
"""
if encoder is None:
encoder = yaml.dump
list_of_dict = [asdict(o, cls=cls) for o in instances]
return encoder(list_of_dict, **encoder_kwargs)