From 8c915195fa0509f116c019ff2257a7f4a4f5b9cc Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Thu, 14 Nov 2024 15:46:40 -0500 Subject: [PATCH] update docs for `TOMLWizard` --- README.rst | 2 + dataclass_wizard/wizard_mixins.py | 28 +++-- docs/common_use_cases/wizard_mixins.rst | 145 ++++++++++++++++++++++++ setup.py | 7 +- 4 files changed, 169 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 1c5bed50..2f128cd7 100644 --- a/README.rst +++ b/README.rst @@ -111,6 +111,7 @@ In addition to the ``JSONWizard``, here are a few extra Mixin_ classes that migh * `JSONListWizard`_ -- Extends ``JSONWizard`` to return `Container`_ -- instead of *list* -- objects where possible. * `JSONFileWizard`_ -- Makes it easier to convert dataclass instances from/to JSON files on a local drive. +* `TOMLWizard`_ -- Provides support to convert dataclass instances to/from TOML. * `YAMLWizard`_ -- Provides support to convert dataclass instances to/from YAML, using the default ``PyYAML`` parser. @@ -961,6 +962,7 @@ This package was created with Cookiecutter_ and the `rnag/cookiecutter-pypackage .. _`open an issue`: https://github.com/rnag/dataclass-wizard/issues .. _`JSONListWizard`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/wizard_mixins.html#jsonlistwizard .. _`JSONFileWizard`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/wizard_mixins.html#jsonfilewizard +.. _`TOMLWizard`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/wizard_mixins.html#tomlwizard .. _`YAMLWizard`: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/wizard_mixins.html#yamlwizard .. _`Container`: https://dataclass-wizard.readthedocs.io/en/latest/dataclass_wizard.html#dataclass_wizard.Container .. _`Supported Types`: https://dataclass-wizard.readthedocs.io/en/latest/overview.html#supported-types diff --git a/dataclass_wizard/wizard_mixins.py b/dataclass_wizard/wizard_mixins.py index 979de6aa..e57f7c72 100644 --- a/dataclass_wizard/wizard_mixins.py +++ b/dataclass_wizard/wizard_mixins.py @@ -131,8 +131,8 @@ def from_toml(cls: Type[T], Converts a TOML `string` to an instance of the dataclass, or a list of the dataclass instances. - If `header` is passed in, and the value of this key in the parsed - ``dict`` object is a ``list``, then the return type is a ``List[T]``. + 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 @@ -149,11 +149,11 @@ def from_toml_file(cls: Type[T], file: str, *, header: str = 'items', parse_float: ParseFloat = float) -> Union[T, List[T]]: """ - Reads in the TOML file contents and converts to an instance of the - dataclass, or a list of the dataclass instances. + Reads the contents of a TOML file and converts them + into an instance (or list of instances) of the dataclass. - If `header` is passed in, and the value of this key in the parsed - ``dict`` object is a ``list``, then the return type is a ``List[T]``. + 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 @@ -171,7 +171,11 @@ def to_toml(self: T, multiline_strings: bool = False, indent: int = 4) -> AnyStr: """ - Converts the dataclass instance to a TOML `string` representation. + 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 @@ -180,12 +184,14 @@ def to_toml(self: T, multiline_strings=multiline_strings, indent=indent) - def to_toml_file(self: T, file: str, mode: str = 'w', + def to_toml_file(self: T, file: str, mode: str = 'wb', encoder: Optional[FileEncoder] = None, multiline_strings: bool = False, indent: int = 4) -> None: """ - Serializes the instance and writes it to a TOML file. + 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 @@ -202,8 +208,8 @@ def list_to_toml(cls: Type[T], encoder: Optional[Encoder] = None, **encoder_kwargs) -> AnyStr: """ - Converts a ``list`` of dataclass instances to a TOML `string` - representation. + Serializes a ``list`` of dataclass instances into a TOML `string`, + grouped under a specified header. """ if encoder is None: encoder = toml_w.dumps diff --git a/docs/common_use_cases/wizard_mixins.rst b/docs/common_use_cases/wizard_mixins.rst index c9dbf0b9..d1c450db 100644 --- a/docs/common_use_cases/wizard_mixins.rst +++ b/docs/common_use_cases/wizard_mixins.rst @@ -191,3 +191,148 @@ A (mostly) complete example of using the :class:`YAMLWizard` is as follows: # ... .. _PyYAML: https://pypi.org/project/PyYAML/ + +:class:`TOMLWizard` +~~~~~~~~~~~~~~~~~~~ + +The TOML Wizard provides an easy, convenient interface for converting ``dataclass`` instances to/from `TOML`_. This mixin enables simple loading, saving, and flexible serialization of TOML data, including support for custom key casing transforms. + +.. note:: + By default, *NO* key transform is used in the TOML dump process. This means that a `snake_case` field name in Python is saved as `snake_case` in TOML. However, this can be customized without subclassing from :class:`JSONWizard`, as below. + + >>> @dataclass + >>> class MyClass(TOMLWizard, key_transform='CAMEL'): + >>> ... + +Dependencies +------------ +- For reading TOML, `TOMLWizard` uses `Tomli`_ for Python 3.9 and 3.10, and the built-in `tomllib`_ for Python 3.11+. +- For writing TOML, `Tomli-W`_ is used across all Python versions. + +.. _TOML: https://toml.io/en/ +.. _Tomli: https://pypi.org/project/tomli/ +.. _Tomli-W: https://pypi.org/project/tomli-w/ +.. _tomllib: https://docs.python.org/3/library/tomllib.html + +Example +------- + +A (mostly) complete example of using the :class:`TOMLWizard` is as follows: + +.. code:: python3 + + from dataclasses import dataclass, field + from dataclass_wizard import TOMLWizard + + + @dataclass + class InnerData: + my_float: float + my_list: list[str] = field(default_factory=list) + + + @dataclass + class MyData(TOMLWizard): + my_str: str + my_dict: dict[str, int] = field(default_factory=dict) + inner_data: InnerData = field(default_factory=lambda: InnerData(3.14, ["hello", "world"])) + + + # TOML input string with nested tables and lists + toml_string = """ + my_str = 'example' + [my_dict] + key1 = 1 + key2 = '2' + + [inner_data] + my_float = 2.718 + my_list = ['apple', 'banana', 'cherry'] + """ + + # Load from TOML string + data = MyData.from_toml(toml_string) + + # Sample output of `data` after loading from TOML: + #> my_str = 'example' + #> my_dict = {'key1': 1, 'key2': 2} + #> inner_data = InnerData(my_float=2.718, my_list=['apple', 'banana', 'cherry']) + + # Save to TOML file + data.to_toml_file('data.toml') + + # Now read it back from the TOML file + new_data = MyData.from_toml_file('data.toml') + + # Assert we get back the same data + assert data == new_data, "Data read from TOML file does not match the original." + + # Create a list of dataclass instances + data_list = [data, new_data, MyData("another_example", {"key3": 3}, InnerData(1.618, ["one", "two"]))] + + # Serialize the list to a TOML string + toml_output = MyData.list_to_toml(data_list, header='testing') + + print(toml_output) + # [[testing]] + # my_str = "example" + # + # [testing.my_dict] + # key1 = 1 + # key2 = 2 + # + # [testing.inner_data] + # my_float = 2.718 + # my_list = [ + # "apple", + # "banana", + # "cherry", + # ] + # ... + +This approach provides a straightforward way to handle TOML data within Python dataclasses. + +Methods +------- + +.. method:: from_toml(cls, string_or_stream, *, decoder=None, header='items', parse_float=float) + + Parses a TOML `string` or stream and converts it into an instance (or list of instances) of the dataclass. If `header` is provided and the corresponding value in the parsed data is a list, the return type is `List[T]`. + + **Example usage:** + + >>> data_str = '''my_str = "test"\n[inner]\nmy_float = 1.2''' + >>> obj = MyClass.from_toml(data_str) + +.. method:: 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. + + **Example usage:** + + >>> obj = MyClass.from_toml_file('config.toml') + +.. method:: 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. + + **Example usage:** + + >>> toml_str = obj.to_toml() + +.. method:: 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. + + **Example usage:** + + >>> obj.to_toml_file('output.toml') + +.. method:: 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`. + + **Example usage:** + + >>> obj_list = [MyClass(), MyClass(my_str="example")] + >>> toml_str = MyClass.list_to_toml(obj_list) diff --git a/setup.py b/setup.py index 940529a0..543fe110 100644 --- a/setup.py +++ b/setup.py @@ -87,8 +87,11 @@ tests_require=test_requirements, extras_require={ 'timedelta': ['pytimeparse>=1.1.7'], - 'toml': ['tomli>=2,<3; python_version == "3.9" or python_version == "3.10"', - 'tomli-w>=1,<2'], + 'toml': [ + 'tomli>=2,<3; python_version=="3.9"', + 'tomli>=2,<3; python_version=="3.10"', + 'tomli-w>=1,<2' + ], 'yaml': ['PyYAML>=6,<7'], 'dev': dev_requires + doc_requires + test_requirements, },