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