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
125import json
126from abc import ABC, abstractmethod
127from dataclasses import dataclass, field
128from typing import IO, Any, Callable, Type
129
130from . import widgets as widgets_m
131from .markup import tim
132from .serializer import Serializer
133
134YAML_ERROR = None
135
136try:
137    import yaml
138except ImportError as import_error:
139    # yaml is explicitly checked to be None later
140    yaml = None  # type: ignore
141    YAML_ERROR = import_error
142
143
144__all__ = ["WidgetNamespace", "FileLoader", "YamlLoader", "JsonLoader"]
145
146
147@dataclass
148class WidgetNamespace:
149    """Class to hold data on loaded namespace."""
150
151    # No clue why `widgets` is seen as undefined here,
152    # but not in the code below. It only seems to happen
153    # in certain pylint configs as well.
154    config: dict[
155        Type[widgets_m.Widget], dict[str, Any]  # pylint: disable=undefined-variable
156    ]
157    widgets: dict[str, widgets_m.Widget]
158    boxes: dict[str, widgets_m.boxes.Box] = field(default_factory=dict)
159
160    @classmethod
161    def from_config(cls, data: dict[Any, Any], loader: FileLoader) -> WidgetNamespace:
162        """Creates a namespace from config data.
163
164        Args:
165            data: A dictionary of config data.
166            loader: The `FileLoader` instance that should be used.
167
168        Returns:
169            A new WidgetNamespace with the given config.
170        """
171
172        namespace = WidgetNamespace({}, {})
173        for name, config in data.items():
174            obj = loader.serializer.known_widgets.get(name)
175            if obj is None:
176                raise KeyError(f"Unknown widget type {name}.")
177
178            namespace.config[obj] = {
179                "styles": obj.styles,
180                "chars": obj.chars.copy(),
181            }
182
183            for category, inner in config.items():
184                value: str | widgets_m.styles.MarkupFormatter
185
186                if category not in namespace.config[obj]:
187                    setattr(obj, category, inner)
188                    continue
189
190                for key, value in inner.items():
191                    namespace.config[obj][category][key] = value
192
193        namespace.apply_config()
194        return namespace
195
196    @staticmethod
197    def _apply_section(
198        widget: Type[widgets_m.Widget], title: str, section: dict[str, str]
199    ) -> None:
200        """Applies configuration section to the widget."""
201
202        for key, value in section.items():
203            if title == "styles":
204                widget.set_style(key, value)
205                continue
206
207            widget.set_char(key, value)
208
209    def apply_to(self, widget: widgets_m.Widget) -> None:
210        """Applies namespace config to the widget.
211
212        Args:
213            widget: The widget in question.
214        """
215
216        def _apply_sections(
217            data: dict[str, dict[str, str]], widget: widgets_m.Widget
218        ) -> None:
219            """Applies sections from data to the widget."""
220
221            for title, section in data.items():
222                self._apply_section(type(widget), title, section)
223
224        data = self.config.get(type(widget))
225        if data is None:
226            return
227
228        _apply_sections(data, widget)
229
230        if hasattr(widget, "_widgets"):
231            for inner in widget:
232                inner_section = self.config.get(type(inner))
233
234                if inner_section is None:
235                    continue
236
237                _apply_sections(inner_section, inner)
238
239    def apply_config(self) -> None:
240        """Apply self.config to current namespace."""
241
242        for widget, settings in self.config.items():
243            for title, section in settings.items():
244                self._apply_section(widget, title, section)
245
246    def __getattr__(self, attr: str) -> widgets_m.Widget:
247        """Get widget by name from widget list."""
248
249        if attr in self.widgets:
250            return self.widgets[attr]
251
252        return self.__dict__[attr]
253
254
255class FileLoader(ABC):
256    """Base class for file loader objects.
257
258    These allow users to load pytermgui content from a specific filetype,
259    with each filetype having their own loaders.
260
261    To use custom widgets with children of this class, you need to call `FileLoader.register`."""
262
263    serializer: Serializer
264    """Object-specific serializer instance. In order to use a specific, already created
265    instance you need to pass it on `FileLoader` construction."""
266
267    @abstractmethod
268    def parse(self, data: str) -> dict[Any, Any]:
269        """Parses string into a dictionary used by `pytermgui.serializer.Serializer`.
270
271        This dictionary follows the structure defined above.
272        """
273
274    def __init__(self, serializer: Serializer | None = None) -> None:
275        """Initialize FileLoader.
276
277        Args:
278            serializer: An optional `pytermgui.serializer.Serializer` instance. If not provided, one
279                is instantiated for every FileLoader instance.
280        """
281
282        if serializer is None:
283            serializer = Serializer()
284
285        self.serializer = serializer
286
287    def __enter__(self) -> FileLoader:
288        """Starts context manager."""
289
290        return self
291
292    def __exit__(self, _: Any, exception: Exception, __: Any) -> bool:
293        """Ends context manager."""
294
295        if exception is not None:
296            raise exception
297
298    def register(self, cls: Type[widgets_m.Widget]) -> None:
299        """Registers a widget to the serializer.
300
301        Args:
302            cls: The widget type to register.
303        """
304
305        self.serializer.register(cls)
306
307    def bind(self, name: str, method: Callable[..., Any]) -> None:
308        """Binds a name to a method.
309
310        Args:
311            name: The name of the method, as referenced in the loaded
312                files.
313            method: The callable to bind.
314        """
315
316        self.serializer.bind(name, method)
317
318    def load_str(self, data: str) -> WidgetNamespace:
319        """Creates a `WidgetNamespace` from string data.
320
321        To parse the data, we use `FileLoader.parse`. To implement custom formats,
322        subclass `FileLoader` with your own `parse` implementation.
323
324        Args:
325            data: The data to parse.
326
327        Returns:
328            A WidgetNamespace created from the provided data.
329        """
330
331        parsed = self.parse(data)
332
333        # Get & load config data
334        config_data = parsed.get("config")
335        if config_data is not None:
336            namespace = WidgetNamespace.from_config(config_data, loader=self)
337        else:
338            namespace = WidgetNamespace.from_config({}, loader=self)
339
340        # Create aliases
341        for key, value in (parsed.get("markup") or {}).items():
342            tim.alias(key, value)
343
344        # Create boxes
345        for name, inner in (parsed.get("boxes") or {}).items():
346            self.serializer.register_box(name, widgets_m.boxes.Box(inner))
347
348        # Create widgets
349        for name, inner in (parsed.get("widgets") or {}).items():
350            widget_type = inner.get("type") or name
351
352            box_name = inner.get("box")
353
354            box = None
355            if box_name is not None and box_name in namespace.boxes:
356                box = namespace.boxes[box_name]
357                del inner["box"]
358
359            try:
360                namespace.widgets[name] = self.serializer.from_dict(
361                    inner, widget_type=widget_type
362                )
363            except AttributeError as error:
364                raise ValueError(
365                    f'Could not load "{name}" from data:\n{json.dumps(inner, indent=2)}'
366                ) from error
367
368            if box is not None:
369                namespace.widgets[name].box = box
370
371        return namespace
372
373    def load(self, data: str | IO) -> WidgetNamespace:
374        """Loads data from a string or a file.
375
376        When an IO object is passed, its data is extracted as a string.
377        This string can then be passed to `load_str`.
378
379        Args:
380            data: Either a string or file stream to load data from.
381
382        Returns:
383            A WidgetNamespace with the data loaded.
384        """
385
386        if not isinstance(data, str):
387            data = data.read()
388
389        assert isinstance(data, str)
390        return self.load_str(data)
391
392
393class JsonLoader(FileLoader):
394    """JSON specific loader subclass."""
395
396    def parse(self, data: str) -> dict[Any, Any]:
397        """Parse JSON str.
398
399        Args:
400            data: JSON formatted string.
401
402        Returns:
403            Loadable dictionary.
404        """
405
406        return json.loads(data)
407
408
409class YamlLoader(FileLoader):
410    """YAML specific loader subclass."""
411
412    def __init__(self, serializer: Serializer | None = None) -> None:
413        """Initialize object, check for installation of PyYAML."""
414
415        if YAML_ERROR is not None:
416            raise RuntimeError(
417                "YAML implementation module not found. Please install `PyYAML` to use `YamlLoader`."
418            ) from YAML_ERROR
419
420        super().__init__()
421
422    def parse(self, data: str) -> dict[Any, Any]:
423        """Parse YAML str.
424
425        Args:
426            data: YAML formatted string.
427
428        Returns:
429            Loadable dictionary.
430        """
431
432        assert yaml is not None
433        return yaml.safe_load(data)
@dataclass
class WidgetNamespace:
148@dataclass
149class WidgetNamespace:
150    """Class to hold data on loaded namespace."""
151
152    # No clue why `widgets` is seen as undefined here,
153    # but not in the code below. It only seems to happen
154    # in certain pylint configs as well.
155    config: dict[
156        Type[widgets_m.Widget], dict[str, Any]  # pylint: disable=undefined-variable
157    ]
158    widgets: dict[str, widgets_m.Widget]
159    boxes: dict[str, widgets_m.boxes.Box] = field(default_factory=dict)
160
161    @classmethod
162    def from_config(cls, data: dict[Any, Any], loader: FileLoader) -> WidgetNamespace:
163        """Creates a namespace from config data.
164
165        Args:
166            data: A dictionary of config data.
167            loader: The `FileLoader` instance that should be used.
168
169        Returns:
170            A new WidgetNamespace with the given config.
171        """
172
173        namespace = WidgetNamespace({}, {})
174        for name, config in data.items():
175            obj = loader.serializer.known_widgets.get(name)
176            if obj is None:
177                raise KeyError(f"Unknown widget type {name}.")
178
179            namespace.config[obj] = {
180                "styles": obj.styles,
181                "chars": obj.chars.copy(),
182            }
183
184            for category, inner in config.items():
185                value: str | widgets_m.styles.MarkupFormatter
186
187                if category not in namespace.config[obj]:
188                    setattr(obj, category, inner)
189                    continue
190
191                for key, value in inner.items():
192                    namespace.config[obj][category][key] = value
193
194        namespace.apply_config()
195        return namespace
196
197    @staticmethod
198    def _apply_section(
199        widget: Type[widgets_m.Widget], title: str, section: dict[str, str]
200    ) -> None:
201        """Applies configuration section to the widget."""
202
203        for key, value in section.items():
204            if title == "styles":
205                widget.set_style(key, value)
206                continue
207
208            widget.set_char(key, value)
209
210    def apply_to(self, widget: widgets_m.Widget) -> None:
211        """Applies namespace config to the widget.
212
213        Args:
214            widget: The widget in question.
215        """
216
217        def _apply_sections(
218            data: dict[str, dict[str, str]], widget: widgets_m.Widget
219        ) -> None:
220            """Applies sections from data to the widget."""
221
222            for title, section in data.items():
223                self._apply_section(type(widget), title, section)
224
225        data = self.config.get(type(widget))
226        if data is None:
227            return
228
229        _apply_sections(data, widget)
230
231        if hasattr(widget, "_widgets"):
232            for inner in widget:
233                inner_section = self.config.get(type(inner))
234
235                if inner_section is None:
236                    continue
237
238                _apply_sections(inner_section, inner)
239
240    def apply_config(self) -> None:
241        """Apply self.config to current namespace."""
242
243        for widget, settings in self.config.items():
244            for title, section in settings.items():
245                self._apply_section(widget, title, section)
246
247    def __getattr__(self, attr: str) -> widgets_m.Widget:
248        """Get widget by name from widget list."""
249
250        if attr in self.widgets:
251            return self.widgets[attr]
252
253        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:
161    @classmethod
162    def from_config(cls, data: dict[Any, Any], loader: FileLoader) -> WidgetNamespace:
163        """Creates a namespace from config data.
164
165        Args:
166            data: A dictionary of config data.
167            loader: The `FileLoader` instance that should be used.
168
169        Returns:
170            A new WidgetNamespace with the given config.
171        """
172
173        namespace = WidgetNamespace({}, {})
174        for name, config in data.items():
175            obj = loader.serializer.known_widgets.get(name)
176            if obj is None:
177                raise KeyError(f"Unknown widget type {name}.")
178
179            namespace.config[obj] = {
180                "styles": obj.styles,
181                "chars": obj.chars.copy(),
182            }
183
184            for category, inner in config.items():
185                value: str | widgets_m.styles.MarkupFormatter
186
187                if category not in namespace.config[obj]:
188                    setattr(obj, category, inner)
189                    continue
190
191                for key, value in inner.items():
192                    namespace.config[obj][category][key] = value
193
194        namespace.apply_config()
195        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:
210    def apply_to(self, widget: widgets_m.Widget) -> None:
211        """Applies namespace config to the widget.
212
213        Args:
214            widget: The widget in question.
215        """
216
217        def _apply_sections(
218            data: dict[str, dict[str, str]], widget: widgets_m.Widget
219        ) -> None:
220            """Applies sections from data to the widget."""
221
222            for title, section in data.items():
223                self._apply_section(type(widget), title, section)
224
225        data = self.config.get(type(widget))
226        if data is None:
227            return
228
229        _apply_sections(data, widget)
230
231        if hasattr(widget, "_widgets"):
232            for inner in widget:
233                inner_section = self.config.get(type(inner))
234
235                if inner_section is None:
236                    continue
237
238                _apply_sections(inner_section, inner)

Applies namespace config to the widget.

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

Apply self.config to current namespace.

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

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:
299    def register(self, cls: Type[widgets_m.Widget]) -> None:
300        """Registers a widget to the serializer.
301
302        Args:
303            cls: The widget type to register.
304        """
305
306        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:
308    def bind(self, name: str, method: Callable[..., Any]) -> None:
309        """Binds a name to a method.
310
311        Args:
312            name: The name of the method, as referenced in the loaded
313                files.
314            method: The callable to bind.
315        """
316
317        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:
319    def load_str(self, data: str) -> WidgetNamespace:
320        """Creates a `WidgetNamespace` from string data.
321
322        To parse the data, we use `FileLoader.parse`. To implement custom formats,
323        subclass `FileLoader` with your own `parse` implementation.
324
325        Args:
326            data: The data to parse.
327
328        Returns:
329            A WidgetNamespace created from the provided data.
330        """
331
332        parsed = self.parse(data)
333
334        # Get & load config data
335        config_data = parsed.get("config")
336        if config_data is not None:
337            namespace = WidgetNamespace.from_config(config_data, loader=self)
338        else:
339            namespace = WidgetNamespace.from_config({}, loader=self)
340
341        # Create aliases
342        for key, value in (parsed.get("markup") or {}).items():
343            tim.alias(key, value)
344
345        # Create boxes
346        for name, inner in (parsed.get("boxes") or {}).items():
347            self.serializer.register_box(name, widgets_m.boxes.Box(inner))
348
349        # Create widgets
350        for name, inner in (parsed.get("widgets") or {}).items():
351            widget_type = inner.get("type") or name
352
353            box_name = inner.get("box")
354
355            box = None
356            if box_name is not None and box_name in namespace.boxes:
357                box = namespace.boxes[box_name]
358                del inner["box"]
359
360            try:
361                namespace.widgets[name] = self.serializer.from_dict(
362                    inner, widget_type=widget_type
363                )
364            except AttributeError as error:
365                raise ValueError(
366                    f'Could not load "{name}" from data:\n{json.dumps(inner, indent=2)}'
367                ) from error
368
369            if box is not None:
370                namespace.widgets[name].box = box
371
372        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:
374    def load(self, data: str | IO) -> WidgetNamespace:
375        """Loads data from a string or a file.
376
377        When an IO object is passed, its data is extracted as a string.
378        This string can then be passed to `load_str`.
379
380        Args:
381            data: Either a string or file stream to load data from.
382
383        Returns:
384            A WidgetNamespace with the data loaded.
385        """
386
387        if not isinstance(data, str):
388            data = data.read()
389
390        assert isinstance(data, str)
391        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):
410class YamlLoader(FileLoader):
411    """YAML specific loader subclass."""
412
413    def __init__(self, serializer: Serializer | None = None) -> None:
414        """Initialize object, check for installation of PyYAML."""
415
416        if YAML_ERROR is not None:
417            raise RuntimeError(
418                "YAML implementation module not found. Please install `PyYAML` to use `YamlLoader`."
419            ) from YAML_ERROR
420
421        super().__init__()
422
423    def parse(self, data: str) -> dict[Any, Any]:
424        """Parse YAML str.
425
426        Args:
427            data: YAML formatted string.
428
429        Returns:
430            Loadable dictionary.
431        """
432
433        assert yaml is not None
434        return yaml.safe_load(data)

YAML specific loader subclass.

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

Initialize object, check for installation of PyYAML.

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

Parse YAML str.

Args
  • data: YAML formatted string.
Returns

Loadable dictionary.

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

JSON specific loader subclass.

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

Parse JSON str.

Args
  • data: JSON formatted string.
Returns

Loadable dictionary.