pytermgui.serializer

Serializer class to allow dumping and loading Widget-s. This class uses Widget.serialize for each widget.

View Source
"""
`Serializer` class to allow dumping and loading `Widget`-s. This class
uses `Widget.serialize` for each widget.
"""

from __future__ import annotations

import json
from typing import Any, Type, IO, Dict

from . import widgets
from .parser import markup

from .widgets.base import Widget
from .widgets import styles, CharType
from .window_manager import Window

WidgetDict = Dict[str, Type[Widget]]

__all__ = ["serializer", "Serializer"]


class Serializer:
    """A class to facilitate loading & dumping widgets.

    By default it is only aware of pytermgui objects, however
    if needed it can be made aware of custom widgets using
    `Serializer.register`.

    It can dump any widget type, but can only load ones it knows.

    All styles (except for char styles) are converted to markup
    during the dump process. This is done to make the end-result
    more readable, as well as more universally usable. As a result,
    all widgets use `markup_style` for their affected styles."""

    def __init__(self) -> None:
        """Set up known widgets"""

        self.known_widgets = self.get_widgets()
        self.known_boxes = vars(widgets.boxes)
        self.register(Window)

    @staticmethod
    def get_widgets() -> WidgetDict:
        """Get all widgets from the module"""

        known = {}
        for name, item in vars(widgets).items():
            if not isinstance(item, type):
                continue

            if issubclass(item, Widget):
                known[name] = item

        return known

    @staticmethod
    def dump_to_dict(obj: Widget) -> dict[str, Any]:
        """Dump widget to a dict

        Note: This is an alias for `obj.serialize`"""

        return obj.serialize()

    def register_box(self, name: str, box: widgets.boxes.Box) -> None:
        """Register a new Box type"""

        self.known_boxes[name] = box

    def register(self, cls: Type[Widget]) -> None:
        """Make object aware of a custom widget class, so
        it can be serialized.

        Make sure to pass a type here, not an instance."""

        if not isinstance(cls, type):
            raise TypeError("Registered object must be a type.")

        self.known_widgets[cls.__name__] = cls

    def from_dict(  # pylint: disable=too-many-locals
        self, data: dict[str, Any], widget_type: str | None = None
    ) -> Widget:
        """Load a widget from a dictionary"""

        def _apply_markup(value: CharType) -> CharType:
            """Apply markup style to obj's key"""

            formatted: CharType
            if isinstance(value, list):
                formatted = [markup.parse(val) for val in value]
            else:
                formatted = markup.parse(value)

            return formatted

        if widget_type is not None:
            data["type"] = widget_type

        obj_class_name = data.get("type")
        if obj_class_name is None:
            raise ValueError("Object with type None could not be loaded.")

        if obj_class_name not in self.known_widgets:
            raise ValueError(
                f'Object of type "{obj_class_name}" is not known!'
                + f" Register it with `serializer.register({obj_class_name})`."
            )

        del data["type"]

        obj_class = self.known_widgets.get(obj_class_name)
        assert obj_class is not None

        obj = obj_class()

        for key, value in data.items():
            if key.startswith("widgets"):
                for inner in value:
                    name, widget = list(inner.items())[0]
                    new = self.from_dict(widget, widget_type=name)
                    assert hasattr(obj, "__iadd__")

                    # this object can be added to, since
                    # it has an __iadd__ method.
                    obj += new  # type: ignore

                continue

            if key == "chars":
                chars: dict[str, CharType] = {}
                for name, char in value.items():
                    chars[name] = _apply_markup(char)

                setattr(obj, "chars", chars)
                continue

            if key == "styles":
                obj_styles = obj.styles.copy()
                for name, markup_str in value.items():
                    if isinstance(markup_str, str):
                        obj_styles[name] = styles.MarkupFormatter(markup_str)
                        continue

                    obj_styles[name] = markup_str

                setattr(obj, "styles", obj_styles)
                continue

            setattr(obj, key, value)

        return obj

    def from_file(self, file: IO[str]) -> Widget:
        """Load widget from a file object"""

        return self.from_dict(json.load(file))

    def to_file(self, obj: Widget, file: IO[str], **json_args: dict[str, Any]) -> None:
        """Dump widget to a file object"""

        data = self.dump_to_dict(obj)
        if "separators" not in json_args:
            # this is a sub-element of a dict[str, Any], so this
            # should work.
            json_args["separators"] = (",", ":")  # type: ignore

        # ** is supposed to be a dict, not a positional arg
        json.dump(data, file, **json_args)  # type: ignore


serializer = Serializer()
#   serializer = <pytermgui.serializer.Serializer object>
#   class Serializer:
View Source
class Serializer:
    """A class to facilitate loading & dumping widgets.

    By default it is only aware of pytermgui objects, however
    if needed it can be made aware of custom widgets using
    `Serializer.register`.

    It can dump any widget type, but can only load ones it knows.

    All styles (except for char styles) are converted to markup
    during the dump process. This is done to make the end-result
    more readable, as well as more universally usable. As a result,
    all widgets use `markup_style` for their affected styles."""

    def __init__(self) -> None:
        """Set up known widgets"""

        self.known_widgets = self.get_widgets()
        self.known_boxes = vars(widgets.boxes)
        self.register(Window)

    @staticmethod
    def get_widgets() -> WidgetDict:
        """Get all widgets from the module"""

        known = {}
        for name, item in vars(widgets).items():
            if not isinstance(item, type):
                continue

            if issubclass(item, Widget):
                known[name] = item

        return known

    @staticmethod
    def dump_to_dict(obj: Widget) -> dict[str, Any]:
        """Dump widget to a dict

        Note: This is an alias for `obj.serialize`"""

        return obj.serialize()

    def register_box(self, name: str, box: widgets.boxes.Box) -> None:
        """Register a new Box type"""

        self.known_boxes[name] = box

    def register(self, cls: Type[Widget]) -> None:
        """Make object aware of a custom widget class, so
        it can be serialized.

        Make sure to pass a type here, not an instance."""

        if not isinstance(cls, type):
            raise TypeError("Registered object must be a type.")

        self.known_widgets[cls.__name__] = cls

    def from_dict(  # pylint: disable=too-many-locals
        self, data: dict[str, Any], widget_type: str | None = None
    ) -> Widget:
        """Load a widget from a dictionary"""

        def _apply_markup(value: CharType) -> CharType:
            """Apply markup style to obj's key"""

            formatted: CharType
            if isinstance(value, list):
                formatted = [markup.parse(val) for val in value]
            else:
                formatted = markup.parse(value)

            return formatted

        if widget_type is not None:
            data["type"] = widget_type

        obj_class_name = data.get("type")
        if obj_class_name is None:
            raise ValueError("Object with type None could not be loaded.")

        if obj_class_name not in self.known_widgets:
            raise ValueError(
                f'Object of type "{obj_class_name}" is not known!'
                + f" Register it with `serializer.register({obj_class_name})`."
            )

        del data["type"]

        obj_class = self.known_widgets.get(obj_class_name)
        assert obj_class is not None

        obj = obj_class()

        for key, value in data.items():
            if key.startswith("widgets"):
                for inner in value:
                    name, widget = list(inner.items())[0]
                    new = self.from_dict(widget, widget_type=name)
                    assert hasattr(obj, "__iadd__")

                    # this object can be added to, since
                    # it has an __iadd__ method.
                    obj += new  # type: ignore

                continue

            if key == "chars":
                chars: dict[str, CharType] = {}
                for name, char in value.items():
                    chars[name] = _apply_markup(char)

                setattr(obj, "chars", chars)
                continue

            if key == "styles":
                obj_styles = obj.styles.copy()
                for name, markup_str in value.items():
                    if isinstance(markup_str, str):
                        obj_styles[name] = styles.MarkupFormatter(markup_str)
                        continue

                    obj_styles[name] = markup_str

                setattr(obj, "styles", obj_styles)
                continue

            setattr(obj, key, value)

        return obj

    def from_file(self, file: IO[str]) -> Widget:
        """Load widget from a file object"""

        return self.from_dict(json.load(file))

    def to_file(self, obj: Widget, file: IO[str], **json_args: dict[str, Any]) -> None:
        """Dump widget to a file object"""

        data = self.dump_to_dict(obj)
        if "separators" not in json_args:
            # this is a sub-element of a dict[str, Any], so this
            # should work.
            json_args["separators"] = (",", ":")  # type: ignore

        # ** is supposed to be a dict, not a positional arg
        json.dump(data, file, **json_args)  # type: ignore

A class to facilitate loading & dumping widgets.

By default it is only aware of pytermgui objects, however if needed it can be made aware of custom widgets using Serializer.register.

It can dump any widget type, but can only load ones it knows.

All styles (except for char styles) are converted to markup during the dump process. This is done to make the end-result more readable, as well as more universally usable. As a result, all widgets use markup_style for their affected styles.

#   Serializer()
View Source
    def __init__(self) -> None:
        """Set up known widgets"""

        self.known_widgets = self.get_widgets()
        self.known_boxes = vars(widgets.boxes)
        self.register(Window)

Set up known widgets

#  
@staticmethod
def get_widgets() -> Dict[str, Type[pytermgui.widgets.base.Widget]]:
View Source
    @staticmethod
    def get_widgets() -> WidgetDict:
        """Get all widgets from the module"""

        known = {}
        for name, item in vars(widgets).items():
            if not isinstance(item, type):
                continue

            if issubclass(item, Widget):
                known[name] = item

        return known

Get all widgets from the module

#  
@staticmethod
def dump_to_dict(obj: pytermgui.widgets.base.Widget) -> dict[str, typing.Any]:
View Source
    @staticmethod
    def dump_to_dict(obj: Widget) -> dict[str, Any]:
        """Dump widget to a dict

        Note: This is an alias for `obj.serialize`"""

        return obj.serialize()

Dump widget to a dict

Note: This is an alias for obj.serialize

#   def register_box(self, name: str, box: pytermgui.widgets.boxes.Box) -> None:
View Source
    def register_box(self, name: str, box: widgets.boxes.Box) -> None:
        """Register a new Box type"""

        self.known_boxes[name] = box

Register a new Box type

#   def register(self, cls: Type[pytermgui.widgets.base.Widget]) -> None:
View Source
    def register(self, cls: Type[Widget]) -> None:
        """Make object aware of a custom widget class, so
        it can be serialized.

        Make sure to pass a type here, not an instance."""

        if not isinstance(cls, type):
            raise TypeError("Registered object must be a type.")

        self.known_widgets[cls.__name__] = cls

Make object aware of a custom widget class, so it can be serialized.

Make sure to pass a type here, not an instance.

#   def from_dict( self, data: dict[str, typing.Any], widget_type: 'str | None' = None ) -> pytermgui.widgets.base.Widget:
View Source
    def from_dict(  # pylint: disable=too-many-locals
        self, data: dict[str, Any], widget_type: str | None = None
    ) -> Widget:
        """Load a widget from a dictionary"""

        def _apply_markup(value: CharType) -> CharType:
            """Apply markup style to obj's key"""

            formatted: CharType
            if isinstance(value, list):
                formatted = [markup.parse(val) for val in value]
            else:
                formatted = markup.parse(value)

            return formatted

        if widget_type is not None:
            data["type"] = widget_type

        obj_class_name = data.get("type")
        if obj_class_name is None:
            raise ValueError("Object with type None could not be loaded.")

        if obj_class_name not in self.known_widgets:
            raise ValueError(
                f'Object of type "{obj_class_name}" is not known!'
                + f" Register it with `serializer.register({obj_class_name})`."
            )

        del data["type"]

        obj_class = self.known_widgets.get(obj_class_name)
        assert obj_class is not None

        obj = obj_class()

        for key, value in data.items():
            if key.startswith("widgets"):
                for inner in value:
                    name, widget = list(inner.items())[0]
                    new = self.from_dict(widget, widget_type=name)
                    assert hasattr(obj, "__iadd__")

                    # this object can be added to, since
                    # it has an __iadd__ method.
                    obj += new  # type: ignore

                continue

            if key == "chars":
                chars: dict[str, CharType] = {}
                for name, char in value.items():
                    chars[name] = _apply_markup(char)

                setattr(obj, "chars", chars)
                continue

            if key == "styles":
                obj_styles = obj.styles.copy()
                for name, markup_str in value.items():
                    if isinstance(markup_str, str):
                        obj_styles[name] = styles.MarkupFormatter(markup_str)
                        continue

                    obj_styles[name] = markup_str

                setattr(obj, "styles", obj_styles)
                continue

            setattr(obj, key, value)

        return obj

Load a widget from a dictionary

#   def from_file(self, file: IO[str]) -> pytermgui.widgets.base.Widget:
View Source
    def from_file(self, file: IO[str]) -> Widget:
        """Load widget from a file object"""

        return self.from_dict(json.load(file))

Load widget from a file object

#   def to_file( self, obj: pytermgui.widgets.base.Widget, file: IO[str], **json_args: dict[str, typing.Any] ) -> None:
View Source
    def to_file(self, obj: Widget, file: IO[str], **json_args: dict[str, Any]) -> None:
        """Dump widget to a file object"""

        data = self.dump_to_dict(obj)
        if "separators" not in json_args:
            # this is a sub-element of a dict[str, Any], so this
            # should work.
            json_args["separators"] = (",", ":")  # type: ignore

        # ** is supposed to be a dict, not a positional arg
        json.dump(data, file, **json_args)  # type: ignore

Dump widget to a file object