pytermgui.serializer

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

  1"""
  2`Serializer` class to allow dumping and loading `Widget`-s. This class
  3uses `Widget.serialize` for each widget.
  4"""
  5
  6from __future__ import annotations
  7
  8import json
  9from typing import Any, Type, IO, Dict, Callable
 10
 11from . import widgets
 12from .parser import markup
 13
 14from .widgets import CharType
 15from .widgets.base import Widget
 16from .window_manager import Window
 17
 18WidgetDict = Dict[str, Type[Widget]]
 19
 20__all__ = ["serializer", "Serializer"]
 21
 22
 23class Serializer:
 24    """A class to facilitate loading & dumping widgets.
 25
 26    By default it is only aware of pytermgui objects, however
 27    if needed it can be made aware of custom widgets using
 28    `Serializer.register`.
 29
 30    It can dump any widget type, but can only load ones it knows.
 31
 32    All styles (except for char styles) are converted to markup
 33    during the dump process. This is done to make the end-result
 34    more readable, as well as more universally usable. As a result,
 35    all widgets use `markup_style` for their affected styles."""
 36
 37    def __init__(self) -> None:
 38        """Sets up known widgets."""
 39
 40        self.known_widgets = self.get_widgets()
 41        self.known_boxes = vars(widgets.boxes)
 42        self.register(Window)
 43
 44        self.bound_methods: dict[str, Callable[..., Any]] = {}
 45
 46    @staticmethod
 47    def get_widgets() -> WidgetDict:
 48        """Gets all widgets from the module."""
 49
 50        known = {}
 51        for name, item in vars(widgets).items():
 52            if not isinstance(item, type):
 53                continue
 54
 55            if issubclass(item, Widget):
 56                known[name] = item
 57
 58        return known
 59
 60    @staticmethod
 61    def dump_to_dict(obj: Widget) -> dict[str, Any]:
 62        """Dump widget to a dict.
 63
 64        This is an alias for `obj.serialize`.
 65
 66        Args:
 67            obj: The widget to dump.
 68
 69        Returns:
 70            `obj.serialize()`.
 71        """
 72
 73        return obj.serialize()
 74
 75    def register_box(self, name: str, box: widgets.boxes.Box) -> None:
 76        """Registers a new Box type.
 77
 78        Args:
 79            name: The name of the box.
 80            box: The box instance.
 81        """
 82
 83        self.known_boxes[name] = box
 84
 85    def register(self, cls: Type[Widget]) -> None:
 86        """Makes object aware of a custom widget class, so
 87        it can be serialized.
 88
 89        Args:
 90            cls: The widget type to register.
 91
 92        Raises:
 93            TypeError: The object is not a type.
 94        """
 95
 96        if not isinstance(cls, type):
 97            raise TypeError("Registered object must be a type.")
 98
 99        self.known_widgets[cls.__name__] = cls
100
101    def bind(self, name: str, method: Callable[..., Any]) -> None:
102        """Binds a name to a method.
103
104        These method callables are substituted into all fields that follow
105        the `method:<method_name>` syntax. If `method_name` is not bound,
106        an exception will be raised during loading.
107
108        Args:
109            name: The name of the method, as referenced in the loaded
110                files.
111            method: The callable to bind.
112        """
113
114        self.bound_methods[name] = method
115
116    def from_dict(  # pylint: disable=too-many-locals, too-many-branches
117        self, data: dict[str, Any], widget_type: str | None = None
118    ) -> Widget:
119        """Loads a widget from a dictionary.
120
121        Args:
122            data: The data to load from.
123            widget_type: Substitute for when data has no `type` field.
124
125        Returns:
126            A widget from the given data.
127        """
128
129        def _apply_markup(value: CharType) -> CharType:
130            """Apply markup style to obj's key"""
131
132            formatted: CharType
133            if isinstance(value, list):
134                formatted = [markup.parse(val) for val in value]
135            else:
136                formatted = markup.parse(value)
137
138            return formatted
139
140        if widget_type is not None:
141            data["type"] = widget_type
142
143        obj_class_name = data.get("type")
144        if obj_class_name is None:
145            raise ValueError("Object with type None could not be loaded.")
146
147        if obj_class_name not in self.known_widgets:
148            raise ValueError(
149                f'Object of type "{obj_class_name}" is not known!'
150                + f" Register it with `serializer.register({obj_class_name})`."
151            )
152
153        del data["type"]
154
155        obj_class = self.known_widgets.get(obj_class_name)
156        assert obj_class is not None
157
158        obj = obj_class()
159
160        for key, value in data.items():
161            if key.startswith("widgets"):
162                for inner in value:
163                    name, widget = list(inner.items())[0]
164                    new = self.from_dict(widget, widget_type=name)
165                    assert hasattr(obj, "__iadd__")
166
167                    # this object can be added to, since
168                    # it has an __iadd__ method.
169                    obj += new  # type: ignore
170
171                continue
172
173            if isinstance(value, str) and value.startswith("method:"):
174                name = value[7:]
175
176                if name not in self.bound_methods:
177                    raise KeyError(f'Reference to unbound method: "{name}".')
178
179                value = self.bound_methods[name]
180
181            if key == "chars":
182                chars: dict[str, CharType] = {}
183                for name, char in value.items():
184                    chars[name] = _apply_markup(char)
185
186                setattr(obj, "chars", chars)
187                continue
188
189            if key == "styles":
190                for name, markup_str in value.items():
191                    obj.styles[name] = markup_str
192
193                continue
194
195            setattr(obj, key, value)
196
197        return obj
198
199    def from_file(self, file: IO[str]) -> Widget:
200        """Loads widget from a file object.
201
202        Args:
203            file: An IO object.
204
205        Returns:
206            The loaded widget.
207        """
208
209        return self.from_dict(json.load(file))
210
211    def to_file(self, obj: Widget, file: IO[str], **json_args: dict[str, Any]) -> None:
212        """Dumps widget to a file object.
213
214        Args:
215            obj: The widget to dump.
216            file: The file object it gets written to.
217            **json_args: Arguments passed to `json.dump`.
218        """
219
220        data = self.dump_to_dict(obj)
221        if "separators" not in json_args:
222            # this is a sub-element of a dict[str, Any], so this
223            # should work.
224            json_args["separators"] = (",", ":")  # type: ignore
225
226        # ** is supposed to be a dict, not a positional arg
227        json.dump(data, file, **json_args)  # type: ignore
228
229
230serializer = Serializer()
serializer = <pytermgui.serializer.Serializer object>
class Serializer:
 24class Serializer:
 25    """A class to facilitate loading & dumping widgets.
 26
 27    By default it is only aware of pytermgui objects, however
 28    if needed it can be made aware of custom widgets using
 29    `Serializer.register`.
 30
 31    It can dump any widget type, but can only load ones it knows.
 32
 33    All styles (except for char styles) are converted to markup
 34    during the dump process. This is done to make the end-result
 35    more readable, as well as more universally usable. As a result,
 36    all widgets use `markup_style` for their affected styles."""
 37
 38    def __init__(self) -> None:
 39        """Sets up known widgets."""
 40
 41        self.known_widgets = self.get_widgets()
 42        self.known_boxes = vars(widgets.boxes)
 43        self.register(Window)
 44
 45        self.bound_methods: dict[str, Callable[..., Any]] = {}
 46
 47    @staticmethod
 48    def get_widgets() -> WidgetDict:
 49        """Gets all widgets from the module."""
 50
 51        known = {}
 52        for name, item in vars(widgets).items():
 53            if not isinstance(item, type):
 54                continue
 55
 56            if issubclass(item, Widget):
 57                known[name] = item
 58
 59        return known
 60
 61    @staticmethod
 62    def dump_to_dict(obj: Widget) -> dict[str, Any]:
 63        """Dump widget to a dict.
 64
 65        This is an alias for `obj.serialize`.
 66
 67        Args:
 68            obj: The widget to dump.
 69
 70        Returns:
 71            `obj.serialize()`.
 72        """
 73
 74        return obj.serialize()
 75
 76    def register_box(self, name: str, box: widgets.boxes.Box) -> None:
 77        """Registers a new Box type.
 78
 79        Args:
 80            name: The name of the box.
 81            box: The box instance.
 82        """
 83
 84        self.known_boxes[name] = box
 85
 86    def register(self, cls: Type[Widget]) -> None:
 87        """Makes object aware of a custom widget class, so
 88        it can be serialized.
 89
 90        Args:
 91            cls: The widget type to register.
 92
 93        Raises:
 94            TypeError: The object is not a type.
 95        """
 96
 97        if not isinstance(cls, type):
 98            raise TypeError("Registered object must be a type.")
 99
100        self.known_widgets[cls.__name__] = cls
101
102    def bind(self, name: str, method: Callable[..., Any]) -> None:
103        """Binds a name to a method.
104
105        These method callables are substituted into all fields that follow
106        the `method:<method_name>` syntax. If `method_name` is not bound,
107        an exception will be raised during loading.
108
109        Args:
110            name: The name of the method, as referenced in the loaded
111                files.
112            method: The callable to bind.
113        """
114
115        self.bound_methods[name] = method
116
117    def from_dict(  # pylint: disable=too-many-locals, too-many-branches
118        self, data: dict[str, Any], widget_type: str | None = None
119    ) -> Widget:
120        """Loads a widget from a dictionary.
121
122        Args:
123            data: The data to load from.
124            widget_type: Substitute for when data has no `type` field.
125
126        Returns:
127            A widget from the given data.
128        """
129
130        def _apply_markup(value: CharType) -> CharType:
131            """Apply markup style to obj's key"""
132
133            formatted: CharType
134            if isinstance(value, list):
135                formatted = [markup.parse(val) for val in value]
136            else:
137                formatted = markup.parse(value)
138
139            return formatted
140
141        if widget_type is not None:
142            data["type"] = widget_type
143
144        obj_class_name = data.get("type")
145        if obj_class_name is None:
146            raise ValueError("Object with type None could not be loaded.")
147
148        if obj_class_name not in self.known_widgets:
149            raise ValueError(
150                f'Object of type "{obj_class_name}" is not known!'
151                + f" Register it with `serializer.register({obj_class_name})`."
152            )
153
154        del data["type"]
155
156        obj_class = self.known_widgets.get(obj_class_name)
157        assert obj_class is not None
158
159        obj = obj_class()
160
161        for key, value in data.items():
162            if key.startswith("widgets"):
163                for inner in value:
164                    name, widget = list(inner.items())[0]
165                    new = self.from_dict(widget, widget_type=name)
166                    assert hasattr(obj, "__iadd__")
167
168                    # this object can be added to, since
169                    # it has an __iadd__ method.
170                    obj += new  # type: ignore
171
172                continue
173
174            if isinstance(value, str) and value.startswith("method:"):
175                name = value[7:]
176
177                if name not in self.bound_methods:
178                    raise KeyError(f'Reference to unbound method: "{name}".')
179
180                value = self.bound_methods[name]
181
182            if key == "chars":
183                chars: dict[str, CharType] = {}
184                for name, char in value.items():
185                    chars[name] = _apply_markup(char)
186
187                setattr(obj, "chars", chars)
188                continue
189
190            if key == "styles":
191                for name, markup_str in value.items():
192                    obj.styles[name] = markup_str
193
194                continue
195
196            setattr(obj, key, value)
197
198        return obj
199
200    def from_file(self, file: IO[str]) -> Widget:
201        """Loads widget from a file object.
202
203        Args:
204            file: An IO object.
205
206        Returns:
207            The loaded widget.
208        """
209
210        return self.from_dict(json.load(file))
211
212    def to_file(self, obj: Widget, file: IO[str], **json_args: dict[str, Any]) -> None:
213        """Dumps widget to a file object.
214
215        Args:
216            obj: The widget to dump.
217            file: The file object it gets written to.
218            **json_args: Arguments passed to `json.dump`.
219        """
220
221        data = self.dump_to_dict(obj)
222        if "separators" not in json_args:
223            # this is a sub-element of a dict[str, Any], so this
224            # should work.
225            json_args["separators"] = (",", ":")  # type: ignore
226
227        # ** is supposed to be a dict, not a positional arg
228        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()
38    def __init__(self) -> None:
39        """Sets up known widgets."""
40
41        self.known_widgets = self.get_widgets()
42        self.known_boxes = vars(widgets.boxes)
43        self.register(Window)
44
45        self.bound_methods: dict[str, Callable[..., Any]] = {}

Sets up known widgets.

@staticmethod
def get_widgets() -> Dict[str, Type[pytermgui.widgets.base.Widget]]:
47    @staticmethod
48    def get_widgets() -> WidgetDict:
49        """Gets all widgets from the module."""
50
51        known = {}
52        for name, item in vars(widgets).items():
53            if not isinstance(item, type):
54                continue
55
56            if issubclass(item, Widget):
57                known[name] = item
58
59        return known

Gets all widgets from the module.

@staticmethod
def dump_to_dict(obj: pytermgui.widgets.base.Widget) -> dict[str, typing.Any]:
61    @staticmethod
62    def dump_to_dict(obj: Widget) -> dict[str, Any]:
63        """Dump widget to a dict.
64
65        This is an alias for `obj.serialize`.
66
67        Args:
68            obj: The widget to dump.
69
70        Returns:
71            `obj.serialize()`.
72        """
73
74        return obj.serialize()

Dump widget to a dict.

This is an alias for obj.serialize.

Args
  • obj: The widget to dump.
Returns

obj.serialize().

def register_box(self, name: str, box: pytermgui.widgets.boxes.Box) -> None:
76    def register_box(self, name: str, box: widgets.boxes.Box) -> None:
77        """Registers a new Box type.
78
79        Args:
80            name: The name of the box.
81            box: The box instance.
82        """
83
84        self.known_boxes[name] = box

Registers a new Box type.

Args
  • name: The name of the box.
  • box: The box instance.
def register(self, cls: Type[pytermgui.widgets.base.Widget]) -> None:
 86    def register(self, cls: Type[Widget]) -> None:
 87        """Makes object aware of a custom widget class, so
 88        it can be serialized.
 89
 90        Args:
 91            cls: The widget type to register.
 92
 93        Raises:
 94            TypeError: The object is not a type.
 95        """
 96
 97        if not isinstance(cls, type):
 98            raise TypeError("Registered object must be a type.")
 99
100        self.known_widgets[cls.__name__] = cls

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.
def bind(self, name: str, method: Callable[..., Any]) -> None:
102    def bind(self, name: str, method: Callable[..., Any]) -> None:
103        """Binds a name to a method.
104
105        These method callables are substituted into all fields that follow
106        the `method:<method_name>` syntax. If `method_name` is not bound,
107        an exception will be raised during loading.
108
109        Args:
110            name: The name of the method, as referenced in the loaded
111                files.
112            method: The callable to bind.
113        """
114
115        self.bound_methods[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.

Args
  • name: The name of the method, as referenced in the loaded files.
  • method: The callable to bind.
def from_dict( self, data: dict[str, typing.Any], widget_type: str | None = None) -> pytermgui.widgets.base.Widget:
117    def from_dict(  # pylint: disable=too-many-locals, too-many-branches
118        self, data: dict[str, Any], widget_type: str | None = None
119    ) -> Widget:
120        """Loads a widget from a dictionary.
121
122        Args:
123            data: The data to load from.
124            widget_type: Substitute for when data has no `type` field.
125
126        Returns:
127            A widget from the given data.
128        """
129
130        def _apply_markup(value: CharType) -> CharType:
131            """Apply markup style to obj's key"""
132
133            formatted: CharType
134            if isinstance(value, list):
135                formatted = [markup.parse(val) for val in value]
136            else:
137                formatted = markup.parse(value)
138
139            return formatted
140
141        if widget_type is not None:
142            data["type"] = widget_type
143
144        obj_class_name = data.get("type")
145        if obj_class_name is None:
146            raise ValueError("Object with type None could not be loaded.")
147
148        if obj_class_name not in self.known_widgets:
149            raise ValueError(
150                f'Object of type "{obj_class_name}" is not known!'
151                + f" Register it with `serializer.register({obj_class_name})`."
152            )
153
154        del data["type"]
155
156        obj_class = self.known_widgets.get(obj_class_name)
157        assert obj_class is not None
158
159        obj = obj_class()
160
161        for key, value in data.items():
162            if key.startswith("widgets"):
163                for inner in value:
164                    name, widget = list(inner.items())[0]
165                    new = self.from_dict(widget, widget_type=name)
166                    assert hasattr(obj, "__iadd__")
167
168                    # this object can be added to, since
169                    # it has an __iadd__ method.
170                    obj += new  # type: ignore
171
172                continue
173
174            if isinstance(value, str) and value.startswith("method:"):
175                name = value[7:]
176
177                if name not in self.bound_methods:
178                    raise KeyError(f'Reference to unbound method: "{name}".')
179
180                value = self.bound_methods[name]
181
182            if key == "chars":
183                chars: dict[str, CharType] = {}
184                for name, char in value.items():
185                    chars[name] = _apply_markup(char)
186
187                setattr(obj, "chars", chars)
188                continue
189
190            if key == "styles":
191                for name, markup_str in value.items():
192                    obj.styles[name] = markup_str
193
194                continue
195
196            setattr(obj, key, value)
197
198        return obj

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 from_file(self, file: IO[str]) -> pytermgui.widgets.base.Widget:
200    def from_file(self, file: IO[str]) -> Widget:
201        """Loads widget from a file object.
202
203        Args:
204            file: An IO object.
205
206        Returns:
207            The loaded widget.
208        """
209
210        return self.from_dict(json.load(file))

Loads widget from a file object.

Args
  • file: An IO object.
Returns

The loaded widget.

def to_file( self, obj: pytermgui.widgets.base.Widget, file: IO[str], **json_args: dict[str, typing.Any]) -> None:
212    def to_file(self, obj: Widget, file: IO[str], **json_args: dict[str, Any]) -> None:
213        """Dumps widget to a file object.
214
215        Args:
216            obj: The widget to dump.
217            file: The file object it gets written to.
218            **json_args: Arguments passed to `json.dump`.
219        """
220
221        data = self.dump_to_dict(obj)
222        if "separators" not in json_args:
223            # this is a sub-element of a dict[str, Any], so this
224            # should work.
225            json_args["separators"] = (",", ":")  # type: ignore
226
227        # ** is supposed to be a dict, not a positional arg
228        json.dump(data, file, **json_args)  # type: ignore

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.