pytermgui.file_loaders

Description

This module provides the library with the capability to load files into Widget-s.

It provides a FileLoader base class, which is then subclassed by various filetype- specific parsers with their own parse method. The job of this method is to take the file contents as a string, and create a valid json tree out of it.

You can "run" a PTG YAML file by calling ptg -f <filename> in your terminal.

To use any YAML related features, the optional dependency PyYAML is required.

Implementation details

The main method of these classes is load, which takes a file-like object or a string, parses it and returns a WidgetNamespace instance. This can then be used to access all custom Widget definitions in the datafile.

This module highly depends on the serializer module. Each file loader uses its own Serializer instance, but optionally take a pre-instantiated Serializer at construction. As with that module, this one depends on it "knowing" all types of Widget-s you are loading. If you have custom Widget subclass you would like to use in file-based definitions, use the FileLoader.register method, passing in your custom class as the sole argument.

File structure

Regardless of filetype, all loaded files must follow a specific structure:

root
|- config
|   |_ custom global widget configuration
|
|- markup
|   |_ custom markup definitions
|
|- boxes
|   |_ custom box definitions
|
|_ widgets
    |_ custom widget definitions

The loading follows the order config -> markup -> boxes -> widgets. It is not necessary to provide all sections.

Example of usage

# -- data.yaml --

markup:
    label-style: '141 @61 bold'

boxes:
    WINDOW_BOX: [
        "left --- right",
        "left x right",
        "left --- right",
    ]

config:
    Window:
        styles:
            border: '[@79]{item}'
        box: SINGLE

    Label:
        styles:
            value: '[label-style]{item}'

widgets:
    MyWindow:
        type: Window
        box: WINDOW_BOX
        widgets:
            Label:
                value: '[210 bold]This is a title'

            Label: {}

            Splitter:
                widgets:
                    - Label:
                        parent_align: 0
                        value: 'This is an option'

                    - Button:
                        label: "Press me!"

            Label: {}
            Label:
                value: '[label-style]{item}'
# -- loader.py --

import pytermgui as ptg

with ptg.YamlLoader() as loader, open("data.yaml", "r") as datafile:
    namespace = loader.load(datafile)

with ptg.WindowManager() as manager:
    manager.add(namespace.MyWindow)
    manager.run()

# Alternatively, one could run `ptg -f "data.yaml"` to display all widgets defined.
# See `ptg -h`.
  1"""
  2Description
  3===========
  4
  5This module provides the library with the capability to load files into Widget-s.
  6
  7It provides a FileLoader base class, which is then subclassed by various filetype-
  8specific parsers with their own `parse` method. The job of this method is to take
  9the file contents as a string, and create a valid json tree out of it.
 10
 11You can "run" a PTG YAML file by calling `ptg -f <filename>` in your terminal.
 12
 13**To use any YAML related features, the optional dependency PyYAML is required.**
 14
 15
 16Implementation details
 17======================
 18
 19The main method of these classes is `load`, which takes a file-like object or a string,
 20parses it and returns a `WidgetNamespace` instance. This can then be used to access all
 21custom `Widget` definitions in the datafile.
 22
 23This module highly depends on the `serializer` module. Each file loader uses its own
 24`Serializer` instance, but optionally take a pre-instantiated Serializer at construction.
 25As with that module, this one depends on it "knowing" all types of Widget-s you are loading.
 26If you have custom Widget subclass you would like to use in file-based definitions, use the
 27`FileLoader.register` method, passing in your custom class as the sole argument.
 28
 29
 30File structure
 31==============
 32
 33Regardless of filetype, all loaded files must follow a specific structure:
 34
 35```
 36root
 37|- config
 38|   |_ custom global widget configuration
 39|
 40|- markup
 41|   |_ custom markup definitions
 42|
 43|- boxes
 44|   |_ custom box definitions
 45|
 46|_ widgets
 47    |_ custom widget definitions
 48```
 49
 50The loading follows the order config -> markup -> boxes -> widgets. It is not necessary to
 51provide all sections.
 52
 53
 54Example of usage
 55================
 56
 57```yaml
 58# -- data.yaml --
 59
 60markup:
 61    label-style: '141 @61 bold'
 62
 63boxes:
 64    WINDOW_BOX: [
 65        "left --- right",
 66        "left x right",
 67        "left --- right",
 68    ]
 69
 70config:
 71    Window:
 72        styles:
 73            border: '[@79]{item}'
 74        box: SINGLE
 75
 76    Label:
 77        styles:
 78            value: '[label-style]{item}'
 79
 80widgets:
 81    MyWindow:
 82        type: Window
 83        box: WINDOW_BOX
 84        widgets:
 85            Label:
 86                value: '[210 bold]This is a title'
 87
 88            Label: {}
 89
 90            Splitter:
 91                widgets:
 92                    - Label:
 93                        parent_align: 0
 94                        value: 'This is an option'
 95
 96                    - Button:
 97                        label: "Press me!"
 98
 99            Label: {}
100            Label:
101                value: '[label-style]{item}'
102```
103
104
105```python3
106# -- loader.py --
107
108import pytermgui as ptg
109
110with ptg.YamlLoader() as loader, open("data.yaml", "r") as datafile:
111    namespace = loader.load(datafile)
112
113with ptg.WindowManager() as manager:
114    manager.add(namespace.MyWindow)
115    manager.run()
116
117# Alternatively, one could run `ptg -f "data.yaml"` to display all widgets defined.
118# See `ptg -h`.
119```
120
121"""
122
123from __future__ import annotations
124
125from typing import Any, Type, IO, Callable
126from dataclasses import dataclass, field
127from abc import abstractmethod, ABC
128
129import json
130
131from . import widgets as widgets_m
132from .parser import markup
133from .serializer import Serializer
134
135
136YAML_ERROR = None
137
138try:
139    import yaml
140except ImportError as import_error:
141    # yaml is explicitly checked to be None later
142    yaml = None  # type: ignore
143    YAML_ERROR = import_error
144
145
146__all__ = ["WidgetNamespace", "FileLoader", "YamlLoader", "JsonLoader"]
147
148
149@dataclass
150class WidgetNamespace:
151    """Class to hold data on loaded namespace."""
152
153    # No clue why `widgets` is seen as undefined here,
154    # but not in the code below. It only seems to happen
155    # in certain pylint configs as well.
156    config: dict[
157        Type[widgets_m.Widget], dict[str, Any]  # pylint: disable=undefined-variable
158    ]
159    widgets: dict[str, widgets_m.Widget]
160    boxes: dict[str, widgets_m.boxes.Box] = field(default_factory=dict)
161
162    @classmethod
163    def from_config(cls, data: dict[Any, Any], loader: FileLoader) -> WidgetNamespace:
164        """Creates a namespace from config data.
165
166        Args:
167            data: A dictionary of config data.
168            loader: The `FileLoader` instance that should be used.
169
170        Returns:
171            A new WidgetNamespace with the given config.
172        """
173
174        namespace = WidgetNamespace({}, {})
175        for name, config in data.items():
176            obj = loader.serializer.known_widgets.get(name)
177            if obj is None:
178                raise KeyError(f"Unknown widget type {name}.")
179
180            namespace.config[obj] = {
181                "styles": obj.styles,
182                "chars": obj.chars.copy(),
183            }
184
185            for category, inner in config.items():
186                value: str | widgets_m.styles.MarkupFormatter
187
188                if category not in namespace.config[obj]:
189                    setattr(obj, category, inner)
190                    continue
191
192                for key, value in inner.items():
193                    namespace.config[obj][category][key] = value
194
195        namespace.apply_config()
196        return namespace
197
198    @staticmethod
199    def _apply_section(
200        widget: Type[widgets_m.Widget], title: str, section: dict[str, str]
201    ) -> None:
202        """Applies configuration section to the widget."""
203
204        for key, value in section.items():
205            if title == "styles":
206                widget.set_style(key, value)
207                continue
208
209            widget.set_char(key, value)
210
211    def apply_to(self, widget: widgets_m.Widget) -> None:
212        """Applies namespace config to the widget.
213
214        Args:
215            widget: The widget in question.
216        """
217
218        def _apply_sections(
219            data: dict[str, dict[str, str]], widget: widgets_m.Widget
220        ) -> None:
221            """Applies sections from data to the widget."""
222
223            for title, section in data.items():
224                self._apply_section(type(widget), title, section)
225
226        data = self.config.get(type(widget))
227        if data is None:
228            return
229
230        _apply_sections(data, widget)
231
232        if hasattr(widget, "_widgets"):
233            for inner in widget:
234                inner_section = self.config.get(type(inner))
235
236                if inner_section is None:
237                    continue
238
239                _apply_sections(inner_section, inner)
240
241    def apply_config(self) -> None:
242        """Apply self.config to current namespace."""
243
244        for widget, settings in self.config.items():
245            for title, section in settings.items():
246                self._apply_section(widget, title, section)
247
248    def __getattr__(self, attr: str) -> widgets_m.Widget:
249        """Get widget by name from widget list."""
250
251        if attr in self.widgets:
252            return self.widgets[attr]
253
254        return self.__dict__[attr]
255
256
257class FileLoader(ABC):
258    """Base class for file loader objects.
259
260    These allow users to load pytermgui content from a specific filetype,
261    with each filetype having their own loaders.
262
263    To use custom widgets with children of this class, you need to call `FileLoader.register`."""
264
265    serializer: Serializer
266    """Object-specific serializer instance. In order to use a specific, already created
267    instance you need to pass it on `FileLoader` construction."""
268
269    @abstractmethod
270    def parse(self, data: str) -> dict[Any, Any]:
271        """Parses string into a dictionary used by `pytermgui.serializer.Serializer`.
272
273        This dictionary follows the structure defined above.
274        """
275
276    def __init__(self, serializer: Serializer | None = None) -> None:
277        """Initialize FileLoader.
278
279        Args:
280            serializer: An optional `pytermgui.serializer.Serializer` instance. If not provided, one
281                is instantiated for every FileLoader instance.
282        """
283
284        if serializer is None:
285            serializer = Serializer()
286
287        self.serializer = serializer
288
289    def __enter__(self) -> FileLoader:
290        """Starts context manager."""
291
292        return self
293
294    def __exit__(self, _: Any, exception: Exception, __: Any) -> bool:
295        """Ends context manager."""
296
297        if exception is not None:
298            raise exception
299
300    def register(self, cls: Type[widgets_m.Widget]) -> None:
301        """Registers a widget to the serializer.
302
303        Args:
304            cls: The widget type to register.
305        """
306
307        self.serializer.register(cls)
308
309    def bind(self, name: str, method: Callable[..., Any]) -> None:
310        """Binds a name to a method.
311
312        Args:
313            name: The name of the method, as referenced in the loaded
314                files.
315            method: The callable to bind.
316        """
317
318        self.serializer.bind(name, method)
319
320    def load_str(self, data: str) -> WidgetNamespace:
321        """Creates a `WidgetNamespace` from string data.
322
323        To parse the data, we use `FileLoader.parse`. To implement custom formats,
324        subclass `FileLoader` with your own `parse` implementation.
325
326        Args:
327            data: The data to parse.
328
329        Returns:
330            A WidgetNamespace created from the provided data.
331        """
332
333        parsed = self.parse(data)
334
335        # Get & load config data
336        config_data = parsed.get("config")
337        if config_data is not None:
338            namespace = WidgetNamespace.from_config(config_data, loader=self)
339        else:
340            namespace = WidgetNamespace.from_config({}, loader=self)
341
342        # Create aliases
343        for key, value in (parsed.get("markup") or {}).items():
344            markup.alias(key, value)
345
346        # Create boxes
347        for name, inner in (parsed.get("boxes") or {}).items():
348            self.serializer.register_box(name, widgets_m.boxes.Box(inner))
349
350        # Create widgets
351        for name, inner in (parsed.get("widgets") or {}).items():
352            widget_type = inner.get("type") or name
353
354            box_name = inner.get("box")
355
356            box = None
357            if box_name is not None and box_name in namespace.boxes:
358                box = namespace.boxes[box_name]
359                del inner["box"]
360
361            try:
362                namespace.widgets[name] = self.serializer.from_dict(
363                    inner, widget_type=widget_type
364                )
365            except AttributeError as error:
366                raise ValueError(
367                    f'Could not load "{name}" from data:\n{json.dumps(inner, indent=2)}'
368                ) from error
369
370            if box is not None:
371                namespace.widgets[name].box = box
372
373        return namespace
374
375    def load(self, data: str | IO) -> WidgetNamespace:
376        """Loads data from a string or a file.
377
378        When an IO object is passed, its data is extracted as a string.
379        This string can then be passed to `load_str`.
380
381        Args:
382            data: Either a string or file stream to load data from.
383
384        Returns:
385            A WidgetNamespace with the data loaded.
386        """
387
388        if not isinstance(data, str):
389            data = data.read()
390
391        assert isinstance(data, str)
392        return self.load_str(data)
393
394
395class JsonLoader(FileLoader):
396    """JSON specific loader subclass."""
397
398    def parse(self, data: str) -> dict[Any, Any]:
399        """Parse JSON str.
400
401        Args:
402            data: JSON formatted string.
403
404        Returns:
405            Loadable dictionary.
406        """
407
408        return json.loads(data)
409
410
411class YamlLoader(FileLoader):
412    """YAML specific loader subclass."""
413
414    def __init__(self, serializer: Serializer | None = None) -> None:
415        """Initialize object, check for installation of PyYAML."""
416
417        if YAML_ERROR is not None:
418            raise RuntimeError(
419                "YAML implementation module not found. Please install `PyYAML` to use `YamlLoader`."
420            ) from YAML_ERROR
421
422        super().__init__()
423
424    def parse(self, data: str) -> dict[Any, Any]:
425        """Parse YAML str.
426
427        Args:
428            data: YAML formatted string.
429
430        Returns:
431            Loadable dictionary.
432        """
433
434        assert yaml is not None
435        return yaml.safe_load(data)
@dataclass
class WidgetNamespace:
150@dataclass
151class WidgetNamespace:
152    """Class to hold data on loaded namespace."""
153
154    # No clue why `widgets` is seen as undefined here,
155    # but not in the code below. It only seems to happen
156    # in certain pylint configs as well.
157    config: dict[
158        Type[widgets_m.Widget], dict[str, Any]  # pylint: disable=undefined-variable
159    ]
160    widgets: dict[str, widgets_m.Widget]
161    boxes: dict[str, widgets_m.boxes.Box] = field(default_factory=dict)
162
163    @classmethod
164    def from_config(cls, data: dict[Any, Any], loader: FileLoader) -> WidgetNamespace:
165        """Creates a namespace from config data.
166
167        Args:
168            data: A dictionary of config data.
169            loader: The `FileLoader` instance that should be used.
170
171        Returns:
172            A new WidgetNamespace with the given config.
173        """
174
175        namespace = WidgetNamespace({}, {})
176        for name, config in data.items():
177            obj = loader.serializer.known_widgets.get(name)
178            if obj is None:
179                raise KeyError(f"Unknown widget type {name}.")
180
181            namespace.config[obj] = {
182                "styles": obj.styles,
183                "chars": obj.chars.copy(),
184            }
185
186            for category, inner in config.items():
187                value: str | widgets_m.styles.MarkupFormatter
188
189                if category not in namespace.config[obj]:
190                    setattr(obj, category, inner)
191                    continue
192
193                for key, value in inner.items():
194                    namespace.config[obj][category][key] = value
195
196        namespace.apply_config()
197        return namespace
198
199    @staticmethod
200    def _apply_section(
201        widget: Type[widgets_m.Widget], title: str, section: dict[str, str]
202    ) -> None:
203        """Applies configuration section to the widget."""
204
205        for key, value in section.items():
206            if title == "styles":
207                widget.set_style(key, value)
208                continue
209
210            widget.set_char(key, value)
211
212    def apply_to(self, widget: widgets_m.Widget) -> None:
213        """Applies namespace config to the widget.
214
215        Args:
216            widget: The widget in question.
217        """
218
219        def _apply_sections(
220            data: dict[str, dict[str, str]], widget: widgets_m.Widget
221        ) -> None:
222            """Applies sections from data to the widget."""
223
224            for title, section in data.items():
225                self._apply_section(type(widget), title, section)
226
227        data = self.config.get(type(widget))
228        if data is None:
229            return
230
231        _apply_sections(data, widget)
232
233        if hasattr(widget, "_widgets"):
234            for inner in widget:
235                inner_section = self.config.get(type(inner))
236
237                if inner_section is None:
238                    continue
239
240                _apply_sections(inner_section, inner)
241
242    def apply_config(self) -> None:
243        """Apply self.config to current namespace."""
244
245        for widget, settings in self.config.items():
246            for title, section in settings.items():
247                self._apply_section(widget, title, section)
248
249    def __getattr__(self, attr: str) -> widgets_m.Widget:
250        """Get widget by name from widget list."""
251
252        if attr in self.widgets:
253            return self.widgets[attr]
254
255        return self.__dict__[attr]

Class to hold data on loaded namespace.

WidgetNamespace( config: dict[typing.Type[pytermgui.widgets.base.Widget], dict[str, typing.Any]], widgets: dict[str, pytermgui.widgets.base.Widget], boxes: dict[str, pytermgui.widgets.boxes.Box] = <factory>)
@classmethod
def from_config( cls, data: dict[typing.Any, typing.Any], loader: pytermgui.file_loaders.FileLoader) -> pytermgui.file_loaders.WidgetNamespace:
163    @classmethod
164    def from_config(cls, data: dict[Any, Any], loader: FileLoader) -> WidgetNamespace:
165        """Creates a namespace from config data.
166
167        Args:
168            data: A dictionary of config data.
169            loader: The `FileLoader` instance that should be used.
170
171        Returns:
172            A new WidgetNamespace with the given config.
173        """
174
175        namespace = WidgetNamespace({}, {})
176        for name, config in data.items():
177            obj = loader.serializer.known_widgets.get(name)
178            if obj is None:
179                raise KeyError(f"Unknown widget type {name}.")
180
181            namespace.config[obj] = {
182                "styles": obj.styles,
183                "chars": obj.chars.copy(),
184            }
185
186            for category, inner in config.items():
187                value: str | widgets_m.styles.MarkupFormatter
188
189                if category not in namespace.config[obj]:
190                    setattr(obj, category, inner)
191                    continue
192
193                for key, value in inner.items():
194                    namespace.config[obj][category][key] = value
195
196        namespace.apply_config()
197        return namespace

Creates a namespace from config data.

Args
  • data: A dictionary of config data.
  • loader: The FileLoader instance that should be used.
Returns

A new WidgetNamespace with the given config.

def apply_to(self, widget: pytermgui.widgets.base.Widget) -> None:
212    def apply_to(self, widget: widgets_m.Widget) -> None:
213        """Applies namespace config to the widget.
214
215        Args:
216            widget: The widget in question.
217        """
218
219        def _apply_sections(
220            data: dict[str, dict[str, str]], widget: widgets_m.Widget
221        ) -> None:
222            """Applies sections from data to the widget."""
223
224            for title, section in data.items():
225                self._apply_section(type(widget), title, section)
226
227        data = self.config.get(type(widget))
228        if data is None:
229            return
230
231        _apply_sections(data, widget)
232
233        if hasattr(widget, "_widgets"):
234            for inner in widget:
235                inner_section = self.config.get(type(inner))
236
237                if inner_section is None:
238                    continue
239
240                _apply_sections(inner_section, inner)

Applies namespace config to the widget.

Args
  • widget: The widget in question.
def apply_config(self) -> None:
242    def apply_config(self) -> None:
243        """Apply self.config to current namespace."""
244
245        for widget, settings in self.config.items():
246            for title, section in settings.items():
247                self._apply_section(widget, title, section)

Apply self.config to current namespace.

class FileLoader(abc.ABC):
258class FileLoader(ABC):
259    """Base class for file loader objects.
260
261    These allow users to load pytermgui content from a specific filetype,
262    with each filetype having their own loaders.
263
264    To use custom widgets with children of this class, you need to call `FileLoader.register`."""
265
266    serializer: Serializer
267    """Object-specific serializer instance. In order to use a specific, already created
268    instance you need to pass it on `FileLoader` construction."""
269
270    @abstractmethod
271    def parse(self, data: str) -> dict[Any, Any]:
272        """Parses string into a dictionary used by `pytermgui.serializer.Serializer`.
273
274        This dictionary follows the structure defined above.
275        """
276
277    def __init__(self, serializer: Serializer | None = None) -> None:
278        """Initialize FileLoader.
279
280        Args:
281            serializer: An optional `pytermgui.serializer.Serializer` instance. If not provided, one
282                is instantiated for every FileLoader instance.
283        """
284
285        if serializer is None:
286            serializer = Serializer()
287
288        self.serializer = serializer
289
290    def __enter__(self) -> FileLoader:
291        """Starts context manager."""
292
293        return self
294
295    def __exit__(self, _: Any, exception: Exception, __: Any) -> bool:
296        """Ends context manager."""
297
298        if exception is not None:
299            raise exception
300
301    def register(self, cls: Type[widgets_m.Widget]) -> None:
302        """Registers a widget to the serializer.
303
304        Args:
305            cls: The widget type to register.
306        """
307
308        self.serializer.register(cls)
309
310    def bind(self, name: str, method: Callable[..., Any]) -> None:
311        """Binds a name to a method.
312
313        Args:
314            name: The name of the method, as referenced in the loaded
315                files.
316            method: The callable to bind.
317        """
318
319        self.serializer.bind(name, method)
320
321    def load_str(self, data: str) -> WidgetNamespace:
322        """Creates a `WidgetNamespace` from string data.
323
324        To parse the data, we use `FileLoader.parse`. To implement custom formats,
325        subclass `FileLoader` with your own `parse` implementation.
326
327        Args:
328            data: The data to parse.
329
330        Returns:
331            A WidgetNamespace created from the provided data.
332        """
333
334        parsed = self.parse(data)
335
336        # Get & load config data
337        config_data = parsed.get("config")
338        if config_data is not None:
339            namespace = WidgetNamespace.from_config(config_data, loader=self)
340        else:
341            namespace = WidgetNamespace.from_config({}, loader=self)
342
343        # Create aliases
344        for key, value in (parsed.get("markup") or {}).items():
345            markup.alias(key, value)
346
347        # Create boxes
348        for name, inner in (parsed.get("boxes") or {}).items():
349            self.serializer.register_box(name, widgets_m.boxes.Box(inner))
350
351        # Create widgets
352        for name, inner in (parsed.get("widgets") or {}).items():
353            widget_type = inner.get("type") or name
354
355            box_name = inner.get("box")
356
357            box = None
358            if box_name is not None and box_name in namespace.boxes:
359                box = namespace.boxes[box_name]
360                del inner["box"]
361
362            try:
363                namespace.widgets[name] = self.serializer.from_dict(
364                    inner, widget_type=widget_type
365                )
366            except AttributeError as error:
367                raise ValueError(
368                    f'Could not load "{name}" from data:\n{json.dumps(inner, indent=2)}'
369                ) from error
370
371            if box is not None:
372                namespace.widgets[name].box = box
373
374        return namespace
375
376    def load(self, data: str | IO) -> WidgetNamespace:
377        """Loads data from a string or a file.
378
379        When an IO object is passed, its data is extracted as a string.
380        This string can then be passed to `load_str`.
381
382        Args:
383            data: Either a string or file stream to load data from.
384
385        Returns:
386            A WidgetNamespace with the data loaded.
387        """
388
389        if not isinstance(data, str):
390            data = data.read()
391
392        assert isinstance(data, str)
393        return self.load_str(data)

Base class for file loader objects.

These allow users to load pytermgui content from a specific filetype, with each filetype having their own loaders.

To use custom widgets with children of this class, you need to call FileLoader.register.

FileLoader(serializer: pytermgui.serializer.Serializer | None = None)
277    def __init__(self, serializer: Serializer | None = None) -> None:
278        """Initialize FileLoader.
279
280        Args:
281            serializer: An optional `pytermgui.serializer.Serializer` instance. If not provided, one
282                is instantiated for every FileLoader instance.
283        """
284
285        if serializer is None:
286            serializer = Serializer()
287
288        self.serializer = serializer

Initialize FileLoader.

Args

Object-specific serializer instance. In order to use a specific, already created instance you need to pass it on FileLoader construction.

@abstractmethod
def parse(self, data: str) -> dict[typing.Any, typing.Any]:
270    @abstractmethod
271    def parse(self, data: str) -> dict[Any, Any]:
272        """Parses string into a dictionary used by `pytermgui.serializer.Serializer`.
273
274        This dictionary follows the structure defined above.
275        """

Parses string into a dictionary used by pytermgui.serializer.Serializer.

This dictionary follows the structure defined above.

def register(self, cls: Type[pytermgui.widgets.base.Widget]) -> None:
301    def register(self, cls: Type[widgets_m.Widget]) -> None:
302        """Registers a widget to the serializer.
303
304        Args:
305            cls: The widget type to register.
306        """
307
308        self.serializer.register(cls)

Registers a widget to the serializer.

Args
  • cls: The widget type to register.
def bind(self, name: str, method: Callable[..., Any]) -> None:
310    def bind(self, name: str, method: Callable[..., Any]) -> None:
311        """Binds a name to a method.
312
313        Args:
314            name: The name of the method, as referenced in the loaded
315                files.
316            method: The callable to bind.
317        """
318
319        self.serializer.bind(name, method)

Binds a name to a method.

Args
  • name: The name of the method, as referenced in the loaded files.
  • method: The callable to bind.
def load_str(self, data: str) -> pytermgui.file_loaders.WidgetNamespace:
321    def load_str(self, data: str) -> WidgetNamespace:
322        """Creates a `WidgetNamespace` from string data.
323
324        To parse the data, we use `FileLoader.parse`. To implement custom formats,
325        subclass `FileLoader` with your own `parse` implementation.
326
327        Args:
328            data: The data to parse.
329
330        Returns:
331            A WidgetNamespace created from the provided data.
332        """
333
334        parsed = self.parse(data)
335
336        # Get & load config data
337        config_data = parsed.get("config")
338        if config_data is not None:
339            namespace = WidgetNamespace.from_config(config_data, loader=self)
340        else:
341            namespace = WidgetNamespace.from_config({}, loader=self)
342
343        # Create aliases
344        for key, value in (parsed.get("markup") or {}).items():
345            markup.alias(key, value)
346
347        # Create boxes
348        for name, inner in (parsed.get("boxes") or {}).items():
349            self.serializer.register_box(name, widgets_m.boxes.Box(inner))
350
351        # Create widgets
352        for name, inner in (parsed.get("widgets") or {}).items():
353            widget_type = inner.get("type") or name
354
355            box_name = inner.get("box")
356
357            box = None
358            if box_name is not None and box_name in namespace.boxes:
359                box = namespace.boxes[box_name]
360                del inner["box"]
361
362            try:
363                namespace.widgets[name] = self.serializer.from_dict(
364                    inner, widget_type=widget_type
365                )
366            except AttributeError as error:
367                raise ValueError(
368                    f'Could not load "{name}" from data:\n{json.dumps(inner, indent=2)}'
369                ) from error
370
371            if box is not None:
372                namespace.widgets[name].box = box
373
374        return namespace

Creates a WidgetNamespace from string data.

To parse the data, we use FileLoader.parse. To implement custom formats, subclass FileLoader with your own parse implementation.

Args
  • data: The data to parse.
Returns

A WidgetNamespace created from the provided data.

def load(self, data: str | typing.IO) -> pytermgui.file_loaders.WidgetNamespace:
376    def load(self, data: str | IO) -> WidgetNamespace:
377        """Loads data from a string or a file.
378
379        When an IO object is passed, its data is extracted as a string.
380        This string can then be passed to `load_str`.
381
382        Args:
383            data: Either a string or file stream to load data from.
384
385        Returns:
386            A WidgetNamespace with the data loaded.
387        """
388
389        if not isinstance(data, str):
390            data = data.read()
391
392        assert isinstance(data, str)
393        return self.load_str(data)

Loads data from a string or a file.

When an IO object is passed, its data is extracted as a string. This string can then be passed to load_str.

Args
  • data: Either a string or file stream to load data from.
Returns

A WidgetNamespace with the data loaded.

class YamlLoader(FileLoader):
412class YamlLoader(FileLoader):
413    """YAML specific loader subclass."""
414
415    def __init__(self, serializer: Serializer | None = None) -> None:
416        """Initialize object, check for installation of PyYAML."""
417
418        if YAML_ERROR is not None:
419            raise RuntimeError(
420                "YAML implementation module not found. Please install `PyYAML` to use `YamlLoader`."
421            ) from YAML_ERROR
422
423        super().__init__()
424
425    def parse(self, data: str) -> dict[Any, Any]:
426        """Parse YAML str.
427
428        Args:
429            data: YAML formatted string.
430
431        Returns:
432            Loadable dictionary.
433        """
434
435        assert yaml is not None
436        return yaml.safe_load(data)

YAML specific loader subclass.

YamlLoader(serializer: pytermgui.serializer.Serializer | None = None)
415    def __init__(self, serializer: Serializer | None = None) -> None:
416        """Initialize object, check for installation of PyYAML."""
417
418        if YAML_ERROR is not None:
419            raise RuntimeError(
420                "YAML implementation module not found. Please install `PyYAML` to use `YamlLoader`."
421            ) from YAML_ERROR
422
423        super().__init__()

Initialize object, check for installation of PyYAML.

def parse(self, data: str) -> dict[typing.Any, typing.Any]:
425    def parse(self, data: str) -> dict[Any, Any]:
426        """Parse YAML str.
427
428        Args:
429            data: YAML formatted string.
430
431        Returns:
432            Loadable dictionary.
433        """
434
435        assert yaml is not None
436        return yaml.safe_load(data)

Parse YAML str.

Args
  • data: YAML formatted string.
Returns

Loadable dictionary.

class JsonLoader(FileLoader):
396class JsonLoader(FileLoader):
397    """JSON specific loader subclass."""
398
399    def parse(self, data: str) -> dict[Any, Any]:
400        """Parse JSON str.
401
402        Args:
403            data: JSON formatted string.
404
405        Returns:
406            Loadable dictionary.
407        """
408
409        return json.loads(data)

JSON specific loader subclass.

def parse(self, data: str) -> dict[typing.Any, typing.Any]:
399    def parse(self, data: str) -> dict[Any, Any]:
400        """Parse JSON str.
401
402        Args:
403            data: JSON formatted string.
404
405        Returns:
406            Loadable dictionary.
407        """
408
409        return json.loads(data)

Parse JSON str.

Args
  • data: JSON formatted string.
Returns

Loadable dictionary.