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`.
View Source
  0"""
  1Description
  2===========
  3
  4This module provides the library with the capability to load files into Widget-s.
  5
  6It provides a FileLoader base class, which is then subclassed by various filetype-
  7specific parsers with their own `parse` method. The job of this method is to take
  8the file contents as a string, and create a valid json tree out of it.
  9
 10You can "run" a PTG YAML file by calling `ptg -f <filename>` in your terminal.
 11
 12**To use any YAML related features, the optional dependency PyYAML is required.**
 13
 14
 15Implementation details
 16======================
 17
 18The main method of these classes is `load`, which takes a file-like object or a string,
 19parses it and returns a `WidgetNamespace` instance. This can then be used to access all
 20custom `Widget` definitions in the datafile.
 21
 22This module highly depends on the `serializer` module. Each file loader uses its own
 23`Serializer` instance, but optionally take a pre-instantiated Serializer at construction.
 24As with that module, this one depends on it "knowing" all types of Widget-s you are loading.
 25If you have custom Widget subclass you would like to use in file-based definitions, use the
 26`FileLoader.register` method, passing in your custom class as the sole argument.
 27
 28
 29File structure
 30==============
 31
 32Regardless of filetype, all loaded files must follow a specific structure:
 33
 34```
 35root
 36|- config
 37|   |_ custom global widget configuration
 38|
 39|- markup
 40|   |_ custom markup definitions
 41|
 42|- boxes
 43|   |_ custom box definitions
 44|
 45|_ widgets
 46    |_ custom widget definitions
 47```
 48
 49The loading follows the order config -> markup -> boxes -> widgets. It is not necessary to
 50provide all sections.
 51
 52
 53Example of usage
 54================
 55
 56```yaml
 57# -- data.yaml --
 58
 59markup:
 60    label-style: '141 @61 bold'
 61
 62boxes:
 63    WINDOW_BOX: [
 64        "left --- right",
 65        "left x right",
 66        "left --- right",
 67    ]
 68
 69config:
 70    Window:
 71        styles:
 72            border: '[@79]{item}'
 73        box: SINGLE
 74
 75    Label:
 76        styles:
 77            value: '[label-style]{item}'
 78
 79widgets:
 80    MyWindow:
 81        type: Window
 82        box: WINDOW_BOX
 83        widgets:
 84            Label:
 85                value: '[210 bold]This is a title'
 86
 87            Label: {}
 88
 89            Splitter:
 90                widgets:
 91                    - Label:
 92                        parent_align: 0
 93                        value: 'This is an option'
 94
 95                    - Button:
 96                        label: "Press me!"
 97
 98            Label: {}
 99            Label:
100                value: '[label-style]{item}'
101```
102
103
104```python3
105# -- loader.py --
106
107import pytermgui as ptg
108
109with ptg.YamlLoader() as loader, open("data.yaml", "r") as datafile:
110    namespace = loader.load(datafile)
111
112with ptg.WindowManager() as manager:
113    manager.add(namespace.MyWindow)
114    manager.run()
115
116# Alternatively, one could run `ptg -f "data.yaml"` to display all widgets defined.
117# See `ptg -h`.
118```
119
120"""
121
122from __future__ import annotations
123
124from typing import Any, Type, IO, Callable
125from dataclasses import dataclass, field
126from abc import abstractmethod, ABC
127
128import json
129
130from . import widgets as widgets_m
131from .parser import markup
132from .serializer import Serializer
133
134
135YAML_ERROR = None
136
137try:
138    import yaml
139except ImportError as import_error:
140    # yaml is explicitly checked to be None later
141    yaml = None  # type: ignore
142    YAML_ERROR = import_error
143
144
145__all__ = ["WidgetNamespace", "FileLoader", "YamlLoader", "JsonLoader"]
146
147
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]
254
255
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            markup.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)
392
393
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)
408
409
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)
#  
@dataclass
class WidgetNamespace:
View Source
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]

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

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

Applies namespace config to the widget.

Args
  • widget: The widget in question.
#   def apply_config(self) -> None:
View Source
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)

Apply self.config to current namespace.

#   class FileLoader(abc.ABC):
View Source
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)

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)
View Source
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

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]:
View Source
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        """

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

Registers a widget to the serializer.

Args
  • cls: The widget type to register.
#   def bind(self, name: str, method: Callable[..., Any]) -> None:
View Source
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)

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

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

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

YAML specific loader subclass.

#   YamlLoader(serializer: pytermgui.serializer.Serializer | None = None)
View Source
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__()

Initialize object, check for installation of PyYAML.

#   def parse(self, data: str) -> dict[typing.Any, typing.Any]:
View Source
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)

Parse YAML str.

Args
  • data: YAML formatted string.
Returns

Loadable dictionary.

#   class JsonLoader(FileLoader):
View Source
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)

JSON specific loader subclass.

#   def parse(self, data: str) -> dict[typing.Any, typing.Any]:
View Source
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)

Parse JSON str.

Args
  • data: JSON formatted string.
Returns

Loadable dictionary.