Skip to content

serialization

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

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.

Source code in pytermgui/serialization.py
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
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:
        """Sets up known widgets."""

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

        self.bound_methods: dict[str, Callable[..., Any]] = {}

    @staticmethod
    def get_widgets() -> WidgetDict:
        """Gets 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.

        This is an alias for `obj.serialize`.

        Args:
            obj: The widget to dump.

        Returns:
            `obj.serialize()`.
        """

        return obj.serialize()

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

        Args:
            name: The name of the box.
            box: The box instance.
        """

        self.known_boxes[name] = box

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

        Args:
            cls: The widget type to register.

        Raises:
            TypeError: The object is not a type.
        """

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

        self.known_widgets[cls.__name__] = cls

    def bind(self, name: str, method: Callable[..., Any]) -> None:
        """Binds a name to a method.

        These method callables are substituted into all fields that follow
        the `method:<method_name>` syntax. If `method_name` is not bound,
        an exception will be raised during loading.

        Args:
            name: The name of the method, as referenced in the loaded
                files.
            method: The callable to bind.
        """

        self.bound_methods[name] = method

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

        Args:
            data: The data to load from.
            widget_type: Substitute for when data has no `type` field.

        Returns:
            A widget from the given data.
        """

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

            formatted: CharType
            if isinstance(value, list):
                formatted = [tim.parse(val) for val in value]
            else:
                formatted = tim.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 isinstance(value, str) and value.startswith("method:"):
                name = value[7:]

                if name not in self.bound_methods:
                    raise KeyError(f'Reference to unbound method: "{name}".')

                value = self.bound_methods[name]

            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":
                for name, markup_str in value.items():
                    obj.styles[name] = markup_str

                continue

            setattr(obj, key, value)

        return obj

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

        Args:
            file: An IO object.

        Returns:
            The loaded widget.
        """

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

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

        Args:
            obj: The widget to dump.
            file: The file object it gets written to.
            **json_args: Arguments passed to `json.dump`.
        """

        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

__init__()

Sets up known widgets.

Source code in pytermgui/serialization.py
36
37
38
39
40
41
42
43
def __init__(self) -> None:
    """Sets up known widgets."""

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

    self.bound_methods: dict[str, Callable[..., Any]] = {}

bind(name, method)

Binds a name to a method.

These method callables are substituted into all fields that follow the method:<method_name> syntax. If method_name is not bound, an exception will be raised during loading.

Parameters:

Name Type Description Default
name str

The name of the method, as referenced in the loaded files.

required
method Callable[..., Any]

The callable to bind.

required
Source code in pytermgui/serialization.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def bind(self, name: str, method: Callable[..., Any]) -> None:
    """Binds a name to a method.

    These method callables are substituted into all fields that follow
    the `method:<method_name>` syntax. If `method_name` is not bound,
    an exception will be raised during loading.

    Args:
        name: The name of the method, as referenced in the loaded
            files.
        method: The callable to bind.
    """

    self.bound_methods[name] = method

dump_to_dict(obj) staticmethod

Dump widget to a dict.

This is an alias for obj.serialize.

Parameters:

Name Type Description Default
obj Widget

The widget to dump.

required

Returns:

Type Description
dict[str, Any]

obj.serialize().

Source code in pytermgui/serialization.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@staticmethod
def dump_to_dict(obj: Widget) -> dict[str, Any]:
    """Dump widget to a dict.

    This is an alias for `obj.serialize`.

    Args:
        obj: The widget to dump.

    Returns:
        `obj.serialize()`.
    """

    return obj.serialize()

from_dict(data, widget_type=None)

Loads a widget from a dictionary.

Parameters:

Name Type Description Default
data dict[str, Any]

The data to load from.

required
widget_type str | None

Substitute for when data has no type field.

None

Returns:

Type Description
Widget

A widget from the given data.

Source code in pytermgui/serialization.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def from_dict(  # pylint: disable=too-many-locals, too-many-branches
    self, data: dict[str, Any], widget_type: str | None = None
) -> Widget:
    """Loads a widget from a dictionary.

    Args:
        data: The data to load from.
        widget_type: Substitute for when data has no `type` field.

    Returns:
        A widget from the given data.
    """

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

        formatted: CharType
        if isinstance(value, list):
            formatted = [tim.parse(val) for val in value]
        else:
            formatted = tim.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 isinstance(value, str) and value.startswith("method:"):
            name = value[7:]

            if name not in self.bound_methods:
                raise KeyError(f'Reference to unbound method: "{name}".')

            value = self.bound_methods[name]

        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":
            for name, markup_str in value.items():
                obj.styles[name] = markup_str

            continue

        setattr(obj, key, value)

    return obj

from_file(file)

Loads widget from a file object.

Parameters:

Name Type Description Default
file IO[str]

An IO object.

required

Returns:

Type Description
Widget

The loaded widget.

Source code in pytermgui/serialization.py
198
199
200
201
202
203
204
205
206
207
208
def from_file(self, file: IO[str]) -> Widget:
    """Loads widget from a file object.

    Args:
        file: An IO object.

    Returns:
        The loaded widget.
    """

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

get_widgets() staticmethod

Gets all widgets from the module.

Source code in pytermgui/serialization.py
45
46
47
48
49
50
51
52
53
54
55
56
57
@staticmethod
def get_widgets() -> WidgetDict:
    """Gets 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

register(cls)

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

Parameters:

Name Type Description Default
cls Type[Widget]

The widget type to register.

required

Raises:

Type Description
TypeError

The object is not a type.

Source code in pytermgui/serialization.py
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def register(self, cls: Type[Widget]) -> None:
    """Makes object aware of a custom widget class, so
    it can be serialized.

    Args:
        cls: The widget type to register.

    Raises:
        TypeError: The object is not a type.
    """

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

    self.known_widgets[cls.__name__] = cls

register_box(name, box)

Registers a new Box type.

Parameters:

Name Type Description Default
name str

The name of the box.

required
box widgets.boxes.Box

The box instance.

required
Source code in pytermgui/serialization.py
74
75
76
77
78
79
80
81
82
def register_box(self, name: str, box: widgets.boxes.Box) -> None:
    """Registers a new Box type.

    Args:
        name: The name of the box.
        box: The box instance.
    """

    self.known_boxes[name] = box

to_file(obj, file, **json_args)

Dumps widget to a file object.

Parameters:

Name Type Description Default
obj Widget

The widget to dump.

required
file IO[str]

The file object it gets written to.

required
**json_args dict[str, Any]

Arguments passed to json.dump.

{}
Source code in pytermgui/serialization.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
def to_file(self, obj: Widget, file: IO[str], **json_args: dict[str, Any]) -> None:
    """Dumps widget to a file object.

    Args:
        obj: The widget to dump.
        file: The file object it gets written to.
        **json_args: Arguments passed to `json.dump`.
    """

    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