Skip to content

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`.

FileLoader

Bases: ABC

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.

Source code in pytermgui/file_loaders.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
class FileLoader(ABC):
    """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`."""

    serializer: Serializer
    """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[Any, Any]:
        """Parses string into a dictionary used by `pytermgui.serializer.Serializer`.

        This dictionary follows the structure defined above.
        """

    def __init__(self, serializer: Serializer | None = None) -> None:
        """Initialize FileLoader.

        Args:
            serializer: An optional `pytermgui.serializer.Serializer` instance. If not provided, one
                is instantiated for every FileLoader instance.
        """

        if serializer is None:
            serializer = Serializer()

        self.serializer = serializer

    def __enter__(self) -> FileLoader:
        """Starts context manager."""

        return self

    def __exit__(self, _: Any, exception: Exception, __: Any) -> bool:
        """Ends context manager."""

        if exception is not None:
            raise exception

    def register(self, cls: Type[widgets_m.Widget]) -> None:
        """Registers a widget to the serializer.

        Args:
            cls: The widget type to register.
        """

        self.serializer.register(cls)

    def bind(self, name: str, method: Callable[..., Any]) -> None:
        """Binds a name to a method.

        Args:
            name: The name of the method, as referenced in the loaded
                files.
            method: The callable to bind.
        """

        self.serializer.bind(name, method)

    def load_str(self, data: str) -> WidgetNamespace:
        """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.
        """

        parsed = self.parse(data)

        # Get & load config data
        config_data = parsed.get("config")
        if config_data is not None:
            namespace = WidgetNamespace.from_config(config_data, loader=self)
        else:
            namespace = WidgetNamespace.from_config({}, loader=self)

        # Create aliases
        for key, value in (parsed.get("markup") or {}).items():
            tim.alias(key, value)

        # Create boxes
        for name, inner in (parsed.get("boxes") or {}).items():
            self.serializer.register_box(name, widgets_m.boxes.Box(inner))

        # Create widgets
        for name, inner in (parsed.get("widgets") or {}).items():
            widget_type = inner.get("type") or name

            box_name = inner.get("box")

            box = None
            if box_name is not None and box_name in namespace.boxes:
                box = namespace.boxes[box_name]
                del inner["box"]

            try:
                namespace.widgets[name] = self.serializer.from_dict(
                    inner, widget_type=widget_type
                )
            except AttributeError as error:
                raise ValueError(
                    f'Could not load "{name}" from data:\n{json.dumps(inner, indent=2)}'
                ) from error

            if box is not None:
                namespace.widgets[name].box = box

        return namespace

    def load(self, data: str | IO) -> WidgetNamespace:
        """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.
        """

        if not isinstance(data, str):
            data = data.read()

        assert isinstance(data, str)
        return self.load_str(data)

serializer: Serializer = serializer instance-attribute

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

__enter__()

Starts context manager.

Source code in pytermgui/file_loaders.py
287
288
289
290
def __enter__(self) -> FileLoader:
    """Starts context manager."""

    return self

__exit__(_, exception, __)

Ends context manager.

Source code in pytermgui/file_loaders.py
292
293
294
295
296
def __exit__(self, _: Any, exception: Exception, __: Any) -> bool:
    """Ends context manager."""

    if exception is not None:
        raise exception

__init__(serializer=None)

Initialize FileLoader.

Parameters:

Name Type Description Default
serializer Serializer | None

An optional pytermgui.serializer.Serializer instance. If not provided, one is instantiated for every FileLoader instance.

None
Source code in pytermgui/file_loaders.py
274
275
276
277
278
279
280
281
282
283
284
285
def __init__(self, serializer: Serializer | None = None) -> None:
    """Initialize FileLoader.

    Args:
        serializer: An optional `pytermgui.serializer.Serializer` instance. If not provided, one
            is instantiated for every FileLoader instance.
    """

    if serializer is None:
        serializer = Serializer()

    self.serializer = serializer

bind(name, method)

Binds a name to a method.

Parameters:

Name Type Description Default
name str

The name of the method, as referenced in the loaded files.

required
method Callable[..., Any]

The callable to bind.

required
Source code in pytermgui/file_loaders.py
307
308
309
310
311
312
313
314
315
316
def bind(self, name: str, method: Callable[..., Any]) -> None:
    """Binds a name to a method.

    Args:
        name: The name of the method, as referenced in the loaded
            files.
        method: The callable to bind.
    """

    self.serializer.bind(name, method)

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

Parameters:

Name Type Description Default
data str | IO

Either a string or file stream to load data from.

required

Returns:

Type Description
WidgetNamespace

A WidgetNamespace with the data loaded.

Source code in pytermgui/file_loaders.py
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
def load(self, data: str | IO) -> WidgetNamespace:
    """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.
    """

    if not isinstance(data, str):
        data = data.read()

    assert isinstance(data, str)
    return self.load_str(data)

load_str(data)

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.

Parameters:

Name Type Description Default
data str

The data to parse.

required

Returns:

Type Description
WidgetNamespace

A WidgetNamespace created from the provided data.

Source code in pytermgui/file_loaders.py
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
def load_str(self, data: str) -> WidgetNamespace:
    """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.
    """

    parsed = self.parse(data)

    # Get & load config data
    config_data = parsed.get("config")
    if config_data is not None:
        namespace = WidgetNamespace.from_config(config_data, loader=self)
    else:
        namespace = WidgetNamespace.from_config({}, loader=self)

    # Create aliases
    for key, value in (parsed.get("markup") or {}).items():
        tim.alias(key, value)

    # Create boxes
    for name, inner in (parsed.get("boxes") or {}).items():
        self.serializer.register_box(name, widgets_m.boxes.Box(inner))

    # Create widgets
    for name, inner in (parsed.get("widgets") or {}).items():
        widget_type = inner.get("type") or name

        box_name = inner.get("box")

        box = None
        if box_name is not None and box_name in namespace.boxes:
            box = namespace.boxes[box_name]
            del inner["box"]

        try:
            namespace.widgets[name] = self.serializer.from_dict(
                inner, widget_type=widget_type
            )
        except AttributeError as error:
            raise ValueError(
                f'Could not load "{name}" from data:\n{json.dumps(inner, indent=2)}'
            ) from error

        if box is not None:
            namespace.widgets[name].box = box

    return namespace

parse(data) abstractmethod

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

This dictionary follows the structure defined above.

Source code in pytermgui/file_loaders.py
267
268
269
270
271
272
@abstractmethod
def parse(self, data: str) -> dict[Any, Any]:
    """Parses string into a dictionary used by `pytermgui.serializer.Serializer`.

    This dictionary follows the structure defined above.
    """

register(cls)

Registers a widget to the serializer.

Parameters:

Name Type Description Default
cls Type[widgets_m.Widget]

The widget type to register.

required
Source code in pytermgui/file_loaders.py
298
299
300
301
302
303
304
305
def register(self, cls: Type[widgets_m.Widget]) -> None:
    """Registers a widget to the serializer.

    Args:
        cls: The widget type to register.
    """

    self.serializer.register(cls)

JsonLoader

Bases: FileLoader

JSON specific loader subclass.

Source code in pytermgui/file_loaders.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
class JsonLoader(FileLoader):
    """JSON specific loader subclass."""

    def parse(self, data: str) -> dict[Any, Any]:
        """Parse JSON str.

        Args:
            data: JSON formatted string.

        Returns:
            Loadable dictionary.
        """

        return json.loads(data)

parse(data)

Parse JSON str.

Parameters:

Name Type Description Default
data str

JSON formatted string.

required

Returns:

Type Description
dict[Any, Any]

Loadable dictionary.

Source code in pytermgui/file_loaders.py
396
397
398
399
400
401
402
403
404
405
406
def parse(self, data: str) -> dict[Any, Any]:
    """Parse JSON str.

    Args:
        data: JSON formatted string.

    Returns:
        Loadable dictionary.
    """

    return json.loads(data)

WidgetNamespace dataclass

Class to hold data on loaded namespace.

Source code in pytermgui/file_loaders.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
@dataclass
class WidgetNamespace:
    """Class to hold data on loaded namespace."""

    # No clue why `widgets` is seen as undefined here,
    # but not in the code below. It only seems to happen
    # in certain pylint configs as well.
    config: dict[
        Type[widgets_m.Widget], dict[str, Any]  # pylint: disable=undefined-variable
    ]
    widgets: dict[str, widgets_m.Widget]
    boxes: dict[str, widgets_m.boxes.Box] = field(default_factory=dict)

    @classmethod
    def from_config(cls, data: dict[Any, Any], loader: FileLoader) -> WidgetNamespace:
        """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.
        """

        namespace = WidgetNamespace({}, {})
        for name, config in data.items():
            obj = loader.serializer.known_widgets.get(name)
            if obj is None:
                raise KeyError(f"Unknown widget type {name}.")

            namespace.config[obj] = {
                "styles": obj.styles,
                "chars": obj.chars.copy(),
            }

            for category, inner in config.items():
                value: str | widgets_m.styles.MarkupFormatter

                if category not in namespace.config[obj]:
                    setattr(obj, category, inner)
                    continue

                for key, value in inner.items():
                    namespace.config[obj][category][key] = value

        namespace.apply_config()
        return namespace

    @staticmethod
    def _apply_section(
        widget: Type[widgets_m.Widget], title: str, section: dict[str, str]
    ) -> None:
        """Applies configuration section to the widget."""

        for key, value in section.items():
            if title == "styles":
                widget.set_style(key, value)
                continue

            widget.set_char(key, value)

    def apply_to(self, widget: widgets_m.Widget) -> None:
        """Applies namespace config to the widget.

        Args:
            widget: The widget in question.
        """

        def _apply_sections(
            data: dict[str, dict[str, str]], widget: widgets_m.Widget
        ) -> None:
            """Applies sections from data to the widget."""

            for title, section in data.items():
                self._apply_section(type(widget), title, section)

        data = self.config.get(type(widget))
        if data is None:
            return

        _apply_sections(data, widget)

        if hasattr(widget, "_widgets"):
            for inner in widget:
                inner_section = self.config.get(type(inner))

                if inner_section is None:
                    continue

                _apply_sections(inner_section, inner)

    def apply_config(self) -> None:
        """Apply self.config to current namespace."""

        for widget, settings in self.config.items():
            for title, section in settings.items():
                self._apply_section(widget, title, section)

    def __getattr__(self, attr: str) -> widgets_m.Widget:
        """Get widget by name from widget list."""

        if attr in self.widgets:
            return self.widgets[attr]

        return self.__dict__[attr]

__getattr__(attr)

Get widget by name from widget list.

Source code in pytermgui/file_loaders.py
246
247
248
249
250
251
252
def __getattr__(self, attr: str) -> widgets_m.Widget:
    """Get widget by name from widget list."""

    if attr in self.widgets:
        return self.widgets[attr]

    return self.__dict__[attr]

apply_config()

Apply self.config to current namespace.

Source code in pytermgui/file_loaders.py
239
240
241
242
243
244
def apply_config(self) -> None:
    """Apply self.config to current namespace."""

    for widget, settings in self.config.items():
        for title, section in settings.items():
            self._apply_section(widget, title, section)

apply_to(widget)

Applies namespace config to the widget.

Parameters:

Name Type Description Default
widget widgets_m.Widget

The widget in question.

required
Source code in pytermgui/file_loaders.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
def apply_to(self, widget: widgets_m.Widget) -> None:
    """Applies namespace config to the widget.

    Args:
        widget: The widget in question.
    """

    def _apply_sections(
        data: dict[str, dict[str, str]], widget: widgets_m.Widget
    ) -> None:
        """Applies sections from data to the widget."""

        for title, section in data.items():
            self._apply_section(type(widget), title, section)

    data = self.config.get(type(widget))
    if data is None:
        return

    _apply_sections(data, widget)

    if hasattr(widget, "_widgets"):
        for inner in widget:
            inner_section = self.config.get(type(inner))

            if inner_section is None:
                continue

            _apply_sections(inner_section, inner)

from_config(data, loader) classmethod

Creates a namespace from config data.

Parameters:

Name Type Description Default
data dict[Any, Any]

A dictionary of config data.

required
loader FileLoader

The FileLoader instance that should be used.

required

Returns:

Type Description
WidgetNamespace

A new WidgetNamespace with the given config.

Source code in pytermgui/file_loaders.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
@classmethod
def from_config(cls, data: dict[Any, Any], loader: FileLoader) -> WidgetNamespace:
    """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.
    """

    namespace = WidgetNamespace({}, {})
    for name, config in data.items():
        obj = loader.serializer.known_widgets.get(name)
        if obj is None:
            raise KeyError(f"Unknown widget type {name}.")

        namespace.config[obj] = {
            "styles": obj.styles,
            "chars": obj.chars.copy(),
        }

        for category, inner in config.items():
            value: str | widgets_m.styles.MarkupFormatter

            if category not in namespace.config[obj]:
                setattr(obj, category, inner)
                continue

            for key, value in inner.items():
                namespace.config[obj][category][key] = value

    namespace.apply_config()
    return namespace

YamlLoader

Bases: FileLoader

YAML specific loader subclass.

Source code in pytermgui/file_loaders.py
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
class YamlLoader(FileLoader):
    """YAML specific loader subclass."""

    def __init__(self, serializer: Serializer | None = None) -> None:
        """Initialize object, check for installation of PyYAML."""

        if YAML_ERROR is not None:
            raise RuntimeError(
                "YAML implementation module not found. Please install `PyYAML` to use `YamlLoader`."
            ) from YAML_ERROR

        super().__init__()

    def parse(self, data: str) -> dict[Any, Any]:
        """Parse YAML str.

        Args:
            data: YAML formatted string.

        Returns:
            Loadable dictionary.
        """

        assert yaml is not None
        return yaml.safe_load(data)

__init__(serializer=None)

Initialize object, check for installation of PyYAML.

Source code in pytermgui/file_loaders.py
412
413
414
415
416
417
418
419
420
def __init__(self, serializer: Serializer | None = None) -> None:
    """Initialize object, check for installation of PyYAML."""

    if YAML_ERROR is not None:
        raise RuntimeError(
            "YAML implementation module not found. Please install `PyYAML` to use `YamlLoader`."
        ) from YAML_ERROR

    super().__init__()

parse(data)

Parse YAML str.

Parameters:

Name Type Description Default
data str

YAML formatted string.

required

Returns:

Type Description
dict[Any, Any]

Loadable dictionary.

Source code in pytermgui/file_loaders.py
422
423
424
425
426
427
428
429
430
431
432
433
def parse(self, data: str) -> dict[Any, Any]:
    """Parse YAML str.

    Args:
        data: YAML formatted string.

    Returns:
        Loadable dictionary.
    """

    assert yaml is not None
    return yaml.safe_load(data)