Source code for glotaran.project.dataclass_helpers

"""Contains helper methods for dataclasses."""
from __future__ import annotations

from collections.abc import Mapping
from collections.abc import Sequence
from dataclasses import MISSING
from dataclasses import field
from dataclasses import fields
from dataclasses import is_dataclass
from pathlib import Path
from typing import TYPE_CHECKING

from glotaran.utils.io import relative_posix_path

if TYPE_CHECKING:
    from collections.abc import Callable
    from typing import Any
    from typing import TypeVar

    from _typeshed import DataclassInstance

    from glotaran.typing.protocols import FileLoadable

    DefaultType = TypeVar("DefaultType")
    DataclassInstanceType = TypeVar("DataclassInstanceType", bound=DataclassInstance)


[docs] def exclude_from_dict_field( default: DefaultType = MISSING, # type:ignore[assignment] ) -> DefaultType: """Create a dataclass field with which will be excluded from ``asdict``. Parameters ---------- default : DefaultType The default value of the field. Returns ------- DefaultType The created field. """ return field(default=default, metadata={"exclude_from_dict": True})
[docs] def file_loader_factory( targetClass: type[FileLoadable], *, is_wrapper_class: bool = False ) -> Callable[[FileLoadable | str | Path], FileLoadable]: """Create ``file_loader`` functions to load ``targetClass`` from file. Parameters ---------- targetClass: type[FileLoadable] Class the loader function should return an instance of. is_wrapper_class: bool Whether or not ``targetClass`` is a wrapper class, so the isinstance check will be ignored and instead the responsibility for supported types lies at the implementation of the loader. Returns ------- file_loader: Callable[[FileLoadable | str | Path], FileLoadable] Function to load ``FileLoadable`` from source file or return instance if already loaded. """ def file_loader( source_path: FileLoadable | str | Path, folder: str | Path | None = None ) -> FileLoadable: """Functions to load ``targetClass`` from file. Parameters ---------- source_path : FileLoadable | str | Path Instance to ``targetClass`` or a file path to load it from. folder : str | Path | None Path to the base folder ``source_path`` is a relative path to., by default None Returns ------- FileLoadable Instance of ``targetClass``. Raises ------ ValueError If not an instance of ``targetClass`` or a source path to load from. """ if isinstance(source_path, targetClass): return source_path if isinstance(source_path, (str, Path)): if folder is not None: target_obj = targetClass.loader(Path(folder) / source_path) else: target_obj = targetClass.loader(source_path) target_obj.source_path = str(source_path) return target_obj # type:ignore[return-value] if is_wrapper_class is True: if isinstance(source_path, Sequence) and folder is not None: source_path = [Path(folder) / val for val in source_path] if isinstance(source_path, Mapping) and folder is not None: source_path = {key: Path(folder) / val for key, val in source_path.items()} return targetClass.loader(source_path) # type:ignore[return-value, arg-type] raise ValueError( f"The value of 'source_path' needs to be of class {targetClass.__name__} " "or a file path. If the class is a wrapper class, you can use the argument:\n" "'is_wrapper_class=True'" ) return file_loader
[docs] def file_loadable_field( targetClass: type[FileLoadable], *, is_wrapper_class=False ) -> FileLoadable: """Create a dataclass field which can be and object of type ``targetClass`` or file path. Parameters ---------- targetClass : type[FileLoadable] Class the resulting value should be an instance of. is_wrapper_class: bool Whether or not ``targetClass`` is a wrapper class, so the isinstance check will be ignored and instead the responsibility for supported types lies at the implementation of the loader. Notes ----- This also requires to add ``init_file_loadable_fields`` in the ``__post_init__`` method. Returns ------- FileLoadable Instance of ``targetClass``. See Also -------- init_file_loadable_fields """ return field( metadata={ "file_loader": file_loader_factory(targetClass, is_wrapper_class=is_wrapper_class) } )
[docs] def init_file_loadable_fields(dataclass_instance: DataclassInstance): """Load objects into class when dataclass is initialized with paths. If the class has file_loadable fields, this needs be called in the ``__post_init__`` method of that class. Parameters ---------- dataclass_instance : DataclassInstance Instance of the dataclass being initialized. When used inside of ``__post_init__`` for the class itself use ``self``. See Also -------- file_loadable_field """ for field_item in fields(dataclass_instance): if "file_loader" in field_item.metadata: file_loader = field_item.metadata["file_loader"] value = getattr(dataclass_instance, field_item.name) setattr(dataclass_instance, field_item.name, file_loader(value))
[docs] def asdict(dataclass: DataclassInstance, folder: Path | None = None) -> dict[str, Any]: """Create a dictionary containing all fields of the dataclass. Parameters ---------- dataclass : DataclassInstance A dataclass instance. folder: Path | None Parent folder of :class:`FileLoadable` fields. by default None Returns ------- dict[str, Any] : The dataclass represented as a dictionary. """ dataclass_dict = {} for field_item in fields(dataclass): if "exclude_from_dict" not in field_item.metadata: value = getattr(dataclass, field_item.name) dataclass_dict[field_item.name] = asdict(value) if is_dataclass(value) else value if "file_loader" in field_item.metadata: value = getattr(dataclass, field_item.name) if value.source_path is not None: if isinstance(value.source_path, (str, Path)): dataclass_dict[field_item.name] = relative_posix_path( value.source_path, folder ) elif isinstance(value.source_path, Sequence): dataclass_dict[field_item.name] = [ relative_posix_path(val, folder) for val in value.source_path ] elif isinstance(value.source_path, Mapping): dataclass_dict[field_item.name] = { key: relative_posix_path(val, folder) for key, val in value.source_path.items() } return dataclass_dict
[docs] def fromdict( dataclass_type: type[DataclassInstanceType], dataclass_dict: dict[str, Any], folder: Path | None = None, ) -> DataclassInstanceType: """Create a dataclass instance from a dict and loads all file represented fields. Parameters ---------- dataclass_type : type[DataclassInstanceType] A dataclass type. dataclass_dict : dict[str, Any] A dict for instancing the the dataclass. folder : Path The root folder for file paths. If ``None`` file paths are consider absolute. Returns ------- DataclassInstanceType Created instance of dataclass_type. """ for field_item in fields(dataclass_type): if "file_loader" in field_item.metadata: file_path = dataclass_dict.get(field_item.name) dataclass_dict[field_item.name] = field_item.metadata["file_loader"](file_path, folder) elif is_dataclass(field_item.default) and field_item.name in dataclass_dict: dataclass_dict[field_item.name] = type(field_item.default)( # type:ignore[misc] **dataclass_dict[field_item.name] ) return dataclass_type(**dataclass_dict)