Skip to content

Commit

Permalink
Added node composition (#685)
Browse files Browse the repository at this point in the history
* Added node composition

* Add test for composite configuration.

* fix tests

---------

Co-authored-by: drodarie <d.rodarie@gmail.com>
  • Loading branch information
Helveg and drodarie committed Mar 1, 2023
1 parent d5d772e commit b882874
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 4 deletions.
3 changes: 2 additions & 1 deletion bsb/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
ConfigurationAttribute,
)
from .._util import ichain
from ._make import walk_node_attributes, walk_nodes
from ._make import walk_node_attributes, walk_nodes, compose_nodes
from ._hooks import on, before, after, run_hook, has_hook
from .. import plugins
from ..exceptions import ConfigTemplateNotFoundError, ParserError, PluginError
Expand Down Expand Up @@ -68,6 +68,7 @@ def __init__(self, name):

walk_node_attributes = staticmethod(walk_node_attributes)
walk_nodes = staticmethod(walk_nodes)
compose_nodes = staticmethod(compose_nodes)
on = staticmethod(on)
after = staticmethod(after)
before = staticmethod(before)
Expand Down
10 changes: 10 additions & 0 deletions bsb/config/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ def __new__(cls, *args, **kwargs):
return NodeMeta


def compose_nodes(*node_classes):
"""
Create a composite mixin class of the given classes. Inherit from the returned class
to inherit from more than one node class.
"""
meta = type("ComposedMetaclass", tuple(type(cls) for cls in node_classes), {})
return meta("CompositionMixin", node_classes, {})


def compile_class(cls):
cls_dict = dict(cls.__dict__)
if "__dict__" in cls_dict:
Expand Down
37 changes: 37 additions & 0 deletions docs/config/nodes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,43 @@ 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.

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:`.config.compose_nodes`:

.. code-block:: python
from bsb import config
from bsb.config import compose_nodes
@config.node
class NodeA:
pass
@config.node
class NodeB:
pass
@config.node
class NodeC(compose_nodes(NodeA, NodeB)):
pass
.. _config_attrs:

Configuration attributes
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ NEURON==8.2.2
nrn-subprocess==1.3.4

# Plugins
bsb-hdf5==0.7.1
bsb-hdf5==0.7.2
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
long_description = fh.read()

requires = [
"bsb-hdf5~=0.7.1",
"bsb-hdf5~=0.7.2",
"h5py~=3.0",
"numpy~=1.19",
"scipy~=1.5",
Expand Down
27 changes: 26 additions & 1 deletion tests/test_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import json
from bsb.core import Scaffold
from bsb import config
from bsb.config import from_json, Configuration, _attrs
from bsb.config import from_json, Configuration, _attrs, compose_nodes
from bsb.config import types
from bsb.exceptions import (
CfgReferenceError,
Expand Down Expand Up @@ -1256,3 +1256,28 @@ def test_booted_root(self):
self.assertIsNone(_attrs._booted_root(cfg.partitions), "shouldnt be booted yet")
Scaffold(cfg)
self.assertIsNotNone(_attrs._booted_root(cfg), "now it should be booted")


class TestNodeComposition(unittest.TestCase):
def setUp(self):
@config.node
class NodeA:
attrA = config.attr()

@config.node
class NodeB:
attrB = config.attr()

@config.node
class NodeC(compose_nodes(NodeA, NodeB)):
attrC = config.attr()

self.tested = NodeC()

def test_composite_node(self):
assert hasattr(self.tested, "attrA")
assert type(self.tested.attrA == config.ConfigurationAttribute)
assert hasattr(self.tested, "attrB")
assert type(self.tested.attrB == config.ConfigurationAttribute)
assert hasattr(self.tested, "attrC")
assert type(self.tested.attrC == config.ConfigurationAttribute)

0 comments on commit b882874

Please sign in to comment.