##### Nodes ##### .. _config_nodes: Nodes are the recursive backbone of the `Configuration` object. Nodes can contain other nodes as attributes and in that way recurse deeper into the configuration. Node classes should describe how to parametrize them in the configuration file. Here is an example to illustrate: .. code-block:: python from bsb import config @config.node class CellType: name = config.attr(key=True) color = config.attr() radius = config.attr(type=float, required=True) This node class has 3 attributes: - ``name`` string attribute used as the key of the node - ``color`` also a string attribute (not required) - ``radius`` a required float attribute This class description can be translated into the following configuration: .. code-block:: json { "cell_type_name": { "radius": 13.0, "color": "red" } } Root node ========= The root node is the Configuration object and is at the basis of the tree of nodes. Node inheritance ================ Classes decorated with node decorators have their class and metaclass machinery rewritten. Basic inheritance works like this: .. code-block:: python @config.node class NodeA: pass @config.node class NodeB(NodeA): pass However, when inheriting from more than one node class you will run into a metaclass conflict. To solve it, use :func:`bsb:bsb.config.compose_nodes`: .. code-block:: python from bsb import config, compose_nodes @config.node class NodeA: pass @config.node class NodeB: pass @config.node class NodeC(compose_nodes(NodeA, NodeB)): pass Dynamic nodes ============= Dynamic nodes are those whose node class is configurable from inside the configuration node itself. This is done through the use of the ``@dynamic`` decorator instead of the node decorator. This will automatically create a required ``cls`` attribute. The value that is given to this attribute will be used to load the class of the node: .. code-block:: python @config.dynamic class PlacementStrategy: @abc.abstractmethod def place(self): pass And in the configuration: .. code-block:: json { "strategy": "bsb.placement.LayeredRandomWalk" } This would import the ``bsb.placement`` module and use its ``LayeredRandomWalk`` class to further process the node. .. note:: The child class must inherit from the dynamic node class. Configuring the dynamic attribute --------------------------------- The same keyword arguments can be passed to the ``dynamic`` decorator as to regular :ref:`attributes ` to specify the properties of the dynamic attribute. As an example, we specify a new attribute name with ``attr_name="example_type"``, allow the dynamic attribute to be omitted ``required=False``, and specify a fallback class with ``default="Example"``: .. code-block:: python @config.dynamic(attr_name="example_type", required=False, default="Example") class Example: pass @config.node class Explicit(Example): purpose = config.attr(required=True) ``Example`` can then be defined as either: .. code-block:: json { "example_type": "Explicit", "purpose": "show explicit dynamic node" } or, because of the ``default`` kwarg, ``Example`` can be implicitly used by omitting the dynamic attribute: .. code-block:: json { "purpose": "show implicit fallback" } .. _classmap: Class maps ---------- A preset map of shorter entries can be given to be mapped to an absolute or relative class path, or a class object: .. code-block:: python @dynamic(classmap={"short": "pkg.with.a.long.name.DynClass"}) class Example: pass If ``short`` is used the dynamic class will resolve to ``pkg.with.a.long.name.DynClass``. Automatic class maps ~~~~~~~~~~~~~~~~~~~~ Automatic class maps can be generated by setting the ``auto_classmap`` keyword argument. Child classes can then register themselves in the classmap of the parent by providing the ``classmap_entry`` keyword argument in their class definition argument list. .. code-block:: python @dynamic(auto_classmap=True) class Example: pass class MappedChild(Example, classmap_entry="short"): pass This will generate a mapping from ``short`` to the ``my.module.path.MappedChild`` class. If the base class is not supposed to be abstract, it can be added to the classmap as well: .. code-block:: python @dynamic(auto_classmap=True, classmap_entry="self") class Example: pass class MappedChild(Example, classmap_entry="short"): pass Pluggable nodes =============== A part of your configuration file might be using plugins, these plugins can behave quite different from eachother and forcing them all to use the same configuration might hinder their function or cause friction for users to configure them properly. To solve this parts of the configuration are *pluggable*. This means that what needs to be configured in the node can be determined by the plugin that you select for it. Homogeneity can be enforced by defining *slots*. If a slot attribute is defined inside of a then the plugin must provide an attribute with the same name. .. note:: Currently the provided attribute slots enforce just the presence, not any kind of inheritance or deeper inspection. It's up to a plugin author to understand the purpose of the slot and to comply with its intentions. Consider the following example: .. code-block:: python import bsb.plugins, bsb.config @bsb.config.pluggable(key="plugin", plugin_name="puppy generator") class PluginNode: @classmethod def __plugins__(cls): if not hasattr(cls, "_plugins"): cls._plugins = bsb.plugins.discover("puppy_generators") return cls._plugins .. code-block:: json { "plugin": "labradoodle", "labrador_percentage": 110, "poodle_percentage": 60 } The decorator argument ``key`` determines which attribute will be read to find out which plugin the user wants to configure. The class method ``__plugins__`` will be used to fetch the plugins every time a plugin is configured (usually finding these plugins isn't that fast so caching them is recommended). The returned plugin objects should be configuration node classes. These classes will then be used to further handle the given configuration. .. _configuration-casting: Casting ======= When the Configuration object is loaded it is cast from a tree to an object. This happens recursively starting at a configuration root. The default :class:`Configuration ` root is defined in ``scaffold/config/_config.py`` and describes how the scaffold builder will read a configuration tree. You can cast from configuration trees to configuration nodes yourself by using the class method ``__cast__``: .. code-block:: python inventory = { "candies": { "Lollypop": { "sweetness": 12.0 }, "Hardcandy": { "sweetness": 4.5 } } } # The second argument would be the node's parent if it had any. conf = Inventory.__cast__(inventory, None) print(conf.candies.Lollypop.sweetness) >>> 12.0 Casting from a root node also resolves references.