Context - Nautobot Design Builder Documentation

Skip to content

# Context

## `nautobot_design_builder.context`

Module that contains classes and functions for use with Design Builder context available when using Jinja templating.

### `Context`

Bases: `_DictNode`

A context represents a tree of variables that can include templates for values.

The Design Builder context is a tree structure that can be used for a Jinja2 render context. One of the strengths of using the Design Builder context is that context information can be provided both in a python class (as normal properties and methods) as well as in YAML files that can be loaded.

YAML files are loaded in and merged with the context, so many files can be loaded to provide a complete context. This allows the context files to be organized in whatever structure makes sense to the design author.

Another strength of the context is that string values can be Jinja templates that will render native Python types. The template render context is the context tree root. This means that values within the context tree can be used to compute other values at render time.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`data` | `dict` | a dictionary of values to be loaded into the context. This dictionary will be recursively evaluated and each level will be stored as either a _DictNode or _ListNode. Leaves will be stored either as a _TemplateNode or their native type. | `None`

Source code in `nautobot_design_builder/context.py`

    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
    class Context(_DictNode):
        """A context represents a tree of variables that can include templates for values.

        The Design Builder context is a tree structure that can be used for a
        Jinja2 render context. One of the strengths of using the Design Builder
        context is that context information can be provided both in a
        python class (as normal properties and methods) as well as in YAML
        files that can be loaded.

        YAML files are loaded in and merged with the context, so many files
        can be loaded to provide a complete context. This allows the context
        files to be organized in whatever structure makes sense to the
        design author.

        Another strength of the context is that string values can be Jinja
        templates that will render native Python types. The template render
        context is the context tree root. This means that values within the
        context tree can be used to compute other values at render time.

        Args:
            data: a dictionary of values to be loaded into the context. This dictionary
                  will be recursively evaluated and each level will be stored as either
                  a _DictNode or _ListNode. Leaves will be stored either as a _TemplateNode
                  or their native type.
        """

        def __init__(self, data: dict = None, job_result: JobResult = None):
            """Constructor for Context class that creates data nodes from input data."""
            super().__init__(data)
            self.job_result = job_result

            for base, filename in self.base_context_files():
                context = load_design_yaml(base, filename)
                # don't add anything if the file was empty
                if context:
                    self.update(context)

        @classmethod
        def base_context_files(cls):
            """Calculate the complete list of context files for the class."""
            bases = list(inspect.getmro(cls))
            bases.reverse()

            files = []
            for base in bases:
                for filename in base.__dict__.get("__base_contexts", {}):
                    files.append((base, filename))
            return files

        @classmethod
        def base_context(cls) -> "Context":
            """The base context is the combination of any context_files that have been added to a context.

            Calling base_context will merge all of the context trees that have been added by the
            @context_file decorator.

            Returns:
                Context: Merged context tree
            """
            base = Context()
            for context in getattr(cls, "__base_contexts", {}).values():
                base.update(context)
            return base

        @classmethod
        def load(cls, yaml_or_mapping):
            """Load a context from a yaml file or mapping."""
            if isinstance(yaml_or_mapping, dict):
                return cls(data=yaml_or_mapping)

            if isinstance(yaml_or_mapping, list):
                raise ValueError("Can only load mappings or yaml")

            return cls.load(yaml.safe_load(yaml_or_mapping))

        def validate(self):
            """Validate that the context can be used to render a design.

            This method will look for any method names that start with "validate_" and will
            call them successively.

            Raises:
                DesignValidationErrors: raised by the validators will be collected. and a single DesignValidationError will be raised that includes all the error messages.
            """
            methods = [method for method in dir(self) if method.startswith("validate_") and callable(getattr(self, method))]
            errors = []
            for method in methods:
                try:
                    getattr(self, method)()
                except DesignValidationError as ex:
                    errors.append(str(ex))

            if len(errors) > 0:
                raise DesignValidationError("\n".join(errors))

#### `__init__(data=None, job_result=None)`

Constructor for Context class that creates data nodes from input data.

Source code in `nautobot_design_builder/context.py`

    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    def __init__(self, data: dict = None, job_result: JobResult = None):
        """Constructor for Context class that creates data nodes from input data."""
        super().__init__(data)
        self.job_result = job_result

        for base, filename in self.base_context_files():
            context = load_design_yaml(base, filename)
            # don't add anything if the file was empty
            if context:
                self.update(context)

#### `base_context()` `classmethod`

The base context is the combination of any context_files that have been added to a context.

Calling base_context will merge all of the context trees that have been added by the @context_file decorator.

Returns:

Name | Type | Description
--- | --- | ---
`Context` | `Context` | Merged context tree

Source code in `nautobot_design_builder/context.py`

    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    @classmethod
    def base_context(cls) -> "Context":
        """The base context is the combination of any context_files that have been added to a context.

        Calling base_context will merge all of the context trees that have been added by the
        @context_file decorator.

        Returns:
            Context: Merged context tree
        """
        base = Context()
        for context in getattr(cls, "__base_contexts", {}).values():
            base.update(context)
        return base

#### `base_context_files()` `classmethod`

Calculate the complete list of context files for the class.

Source code in `nautobot_design_builder/context.py`

    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    @classmethod
    def base_context_files(cls):
        """Calculate the complete list of context files for the class."""
        bases = list(inspect.getmro(cls))
        bases.reverse()

        files = []
        for base in bases:
            for filename in base.__dict__.get("__base_contexts", {}):
                files.append((base, filename))
        return files

#### `load(yaml_or_mapping)` `classmethod`

Load a context from a yaml file or mapping.

Source code in `nautobot_design_builder/context.py`

    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    @classmethod
    def load(cls, yaml_or_mapping):
        """Load a context from a yaml file or mapping."""
        if isinstance(yaml_or_mapping, dict):
            return cls(data=yaml_or_mapping)

        if isinstance(yaml_or_mapping, list):
            raise ValueError("Can only load mappings or yaml")

        return cls.load(yaml.safe_load(yaml_or_mapping))

#### `validate()`

Validate that the context can be used to render a design.

This method will look for any method names that start with "validate_" and will call them successively.

Raises:

Type | Description
--- | ---
`DesignValidationErrors` | raised by the validators will be collected. and a single DesignValidationError will be raised that includes all the error messages.

Source code in `nautobot_design_builder/context.py`

    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    def validate(self):
        """Validate that the context can be used to render a design.

        This method will look for any method names that start with "validate_" and will
        call them successively.

        Raises:
            DesignValidationErrors: raised by the validators will be collected. and a single DesignValidationError will be raised that includes all the error messages.
        """
        methods = [method for method in dir(self) if method.startswith("validate_") and callable(getattr(self, method))]
        errors = []
        for method in methods:
            try:
                getattr(self, method)()
            except DesignValidationError as ex:
                errors.append(str(ex))

        if len(errors) > 0:
            raise DesignValidationError("\n".join(errors))

### `ContextNodeMixin`

A mixin to help create tree nodes for the Design Builder context.

This mixin provides overridden getitem and setitem magic methods that will automatically get and set the tree node types. The mixin also provides a mechanism for a node within the tree to find the context's root node and root render environment.

Source code in `nautobot_design_builder/context.py`

     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    class ContextNodeMixin:
        """A mixin to help create tree nodes for the Design Builder context.

        This mixin provides overridden __getitem__ and __setitem__ magic
        methods that will automatically get and set the tree node types. The
        mixin also provides a mechanism for a node within the tree to find
        the context's root node and root render environment.
        """

        _parent: "ContextNodeMixin" = None
        _env: NativeEnvironment = None

        @cached_property
        def root(self) -> "ContextNodeMixin":
            """Lookup and return the root node in the context tree.

            Returns:
                ContextNodeMixin: root node
            """
            node: ContextNodeMixin = self
            while node._parent is not None:  # pylint:disable=protected-access
                node = node._parent  # pylint:disable=protected-access

            if node._env is None:  # pylint:disable=protected-access
                node._env = new_template_environment(node, native_environment=True)  # pylint:disable=protected-access
            return node

        @property
        def env(self) -> NativeEnvironment:
            """Lookup the Jinja2 native environment from the root context node."""
            return self.root._env  # pylint:disable=protected-access

        def __repr__(self) -> str:
            """Get the printable representation of the node.

            This will return the `repr` of either the node's container `data`
            attribute (if it exists) or the super class representation.
            """
            if hasattr(self, "data"):
                return repr(getattr(self, "data"))
            return super().__repr__()

        def __setitem__(self, key: "int | str", value: Any) -> "ContextNodeMixin":
            """Store a new value within the node.

            Args:
                key (int | str): Index/key/attribute name
                value (Any): Value of item to store

            Raises:
                KeyError: if the item cannot be stored.

            Returns:
                ContextNodeMixin: _description_
            """
            if not isinstance(value, ContextNodeMixin):
                value = self._create_node(value)

            if hasattr(self, "data") and key in self.data:
                old_value = self.data[key]
                if hasattr(old_value, "update"):
                    old_value.update(value)
                else:
                    self.data[key] = value
            elif isinstance(key, str) and hasattr(self, key):
                setattr(self, key, value)
            else:
                super().__setitem__(key, value)
            return value

        def __getitem__(self, key) -> "ContextNodeMixin":
            """Get the desired item from within the node's children.

            `__getitem__` will first look for items in the context
            node's `data` attribute. If the `data` attribute does
            not exist, than the lookup will default to the superclass
            `__getitem__`. If the found item is a `_TemplateNode` then
            the template is rendered and the resulting native type is
            returned.
            """
            try:
                value = self.data[key]
            except KeyError as ex:
                if isinstance(key, str) and hasattr(self, key):
                    value = getattr(self, key)
                else:
                    raise ex
            except AttributeError:
                value = super().__getitem__(key)

            # Use the _TemplateNode's data descriptor to
            # render the template and get the native value
            if isinstance(value, _TemplateNode):
                value = value.data
            return value

        def _create_node(self, value):
            """`_create_node` is a factory function for context nodes.

            `_create_node` will take a value and create the proper tree
            node type. Python types `list`, `dict` and `str` are converted
            to the associated `_ListNode`, `_DictNode`, and `_TemplateNode`
            with all other types being returned unchanged. If a context
            node is created, than it's parent node is properly set so
            that the root node, and environment, of the context can be
            determined for `_TemplateNode` rendering.

            Args:
                value: a value that needs to be added a parent node

            Returns:
                A new Node. If the value is a list then a new _ListNode is returned
                if the value is a dict then a new _DictNode, if the value is a string
                then a new _TemplateNode, otherwise the original value (a leaf node)
                is returned.
            """
            if isinstance(value, list):
                value = _ListNode(value)

            elif isinstance(value, dict):
                value = _DictNode(value)

            elif isinstance(value, str):
                value = _TemplateNode(self, value)

            if isinstance(value, ContextNodeMixin):
                value._parent = self  # pylint:disable=protected-access

            return value

#### `env: NativeEnvironment` `property`

Lookup the Jinja2 native environment from the root context node.

#### `root: ContextNodeMixin` `cached` `property`

Lookup and return the root node in the context tree.

Returns:

Name | Type | Description
--- | --- | ---
`ContextNodeMixin` | `ContextNodeMixin` | root node

#### `__getitem__(key)`

Get the desired item from within the node's children.

`__getitem__` will first look for items in the context node's `data` attribute. If the `data` attribute does not exist, than the lookup will default to the superclass `__getitem__`. If the found item is a `_TemplateNode` then the template is rendered and the resulting native type is returned.

Source code in `nautobot_design_builder/context.py`

     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    def __getitem__(self, key) -> "ContextNodeMixin":
        """Get the desired item from within the node's children.

        `__getitem__` will first look for items in the context
        node's `data` attribute. If the `data` attribute does
        not exist, than the lookup will default to the superclass
        `__getitem__`. If the found item is a `_TemplateNode` then
        the template is rendered and the resulting native type is
        returned.
        """
        try:
            value = self.data[key]
        except KeyError as ex:
            if isinstance(key, str) and hasattr(self, key):
                value = getattr(self, key)
            else:
                raise ex
        except AttributeError:
            value = super().__getitem__(key)

        # Use the _TemplateNode's data descriptor to
        # render the template and get the native value
        if isinstance(value, _TemplateNode):
            value = value.data
        return value

#### `__repr__()`

Get the printable representation of the node.

This will return the `repr` of either the node's container `data` attribute (if it exists) or the super class representation.

Source code in `nautobot_design_builder/context.py`

    49
    50
    51
    52
    53
    54
    55
    56
    57
    def __repr__(self) -> str:
        """Get the printable representation of the node.

        This will return the `repr` of either the node's container `data`
        attribute (if it exists) or the super class representation.
        """
        if hasattr(self, "data"):
            return repr(getattr(self, "data"))
        return super().__repr__()

#### `__setitem__(key, value)`

Store a new value within the node.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`key` | `int | str` | Index/key/attribute name | required
`value` | `Any` | Value of item to store | required

Raises:

Type | Description
--- | ---
`KeyError` | if the item cannot be stored.

Returns:

Name | Type | Description
--- | --- | ---
`ContextNodeMixin` | `ContextNodeMixin` | description

Source code in `nautobot_design_builder/context.py`

    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    def __setitem__(self, key: "int | str", value: Any) -> "ContextNodeMixin":
        """Store a new value within the node.

        Args:
            key (int | str): Index/key/attribute name
            value (Any): Value of item to store

        Raises:
            KeyError: if the item cannot be stored.

        Returns:
            ContextNodeMixin: _description_
        """
        if not isinstance(value, ContextNodeMixin):
            value = self._create_node(value)

        if hasattr(self, "data") and key in self.data:
            old_value = self.data[key]
            if hasattr(old_value, "update"):
                old_value.update(value)
            else:
                self.data[key] = value
        elif isinstance(key, str) and hasattr(self, key):
            setattr(self, key, value)
        else:
            super().__setitem__(key, value)
        return value

### `context_file(*ctx_files)`

Add a context file to a class.

Context Files are only loaded once per class hierarchy, even if they are specified more than once in a class/subclass tree. Context files must be in YAML format. Each context file will be loaded and parsed and merged together to form a base context.

Source code in `nautobot_design_builder/context.py`

    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    def context_file(*ctx_files):
        """Add a context file to a class.

        Context Files are only loaded once per class hierarchy, even if they are
        specified more than once in a class/subclass tree. Context files must
        be in YAML format. Each context file will be loaded and parsed and
        merged together to form a base context.
        """

        def wrapper(context_cls):
            if "__base_contexts" not in context_cls.__dict__:
                setattr(context_cls, "__base_contexts", set())

            for ctx_file in ctx_files:
                base_context = getattr(context_cls, "__base_contexts")
                base_context.add(ctx_file)

            return context_cls

        return wrapper