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()
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.
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.
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.
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()
.
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.
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.
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.
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.
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.
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
.