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

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()
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]] = {}

Sets up known widgets.

@staticmethod
def get_widgets() -> Dict[str, Type[pytermgui.widgets.base.Widget]]:
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

Gets all widgets from the module.

@staticmethod
def dump_to_dict(obj: pytermgui.widgets.base.Widget) -> dict[str, typing.Any]:
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()

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:
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

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:
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

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:
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

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:
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 = [tim.parse(val) for val in value]
135            else:
136                formatted = tim.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

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:
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))

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:
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

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.