"""
bsb.config module
Contains the dynamic attribute system; Use ``@bsb.config.root/node/dynamic/pluggable`` to
decorate your classes and add class attributes using ``x =
config.attr/dict/list/ref/reflist`` to populate your classes with powerful attributes.
"""
import builtins
import functools
import glob
import itertools
import os
import sys
import traceback
import typing
from shutil import copy2 as copy_file
from .. import plugins
from .._util import ichain
from ..exceptions import ConfigTemplateNotFoundError, ParserError
from . import refs, types
from ._attrs import (
ConfigurationAttribute,
attr,
catch_all,
dict,
dynamic,
file,
list,
node,
pluggable,
property,
provide,
ref,
reflist,
root,
slot,
unset,
)
from ._distributions import Distribution
from ._hooks import after, before, has_hook, on, run_hook
from ._make import (
compose_nodes,
get_config_attributes,
walk_node_attributes,
walk_nodes,
)
from .parsers import get_configuration_parser, get_configuration_parser_classes
if typing.TYPE_CHECKING:
from ._config import Configuration
@functools.cache
def __getattr__(name):
if name == "Configuration":
# Load the Configuration class on demand, not on import, to avoid circular
# dependencies.
from ._config import Configuration
return Configuration
else:
raise object.__getattribute__(sys.modules[__name__], name)
[docs]
def get_config_path():
import os
env_paths = os.environ.get("BSB_CONFIG_PATH", None)
if env_paths is None:
env_paths = ()
else:
env_paths = env_paths.split(":")
plugin_paths = plugins.discover("config.templates")
return [*itertools.chain((os.getcwd(),), env_paths, *plugin_paths.values())]
[docs]
def copy_configuration_template(template, output="network_configuration.json", path=None):
path = [
*map(
os.path.abspath,
itertools.chain(get_config_path(), path or ()),
)
]
for d in path:
if files := glob.glob(os.path.join(d, template)):
break
else:
raise ConfigTemplateNotFoundError(
"'%template%' not found in config path %path%", template, path
)
copy_file(files[0], output)
[docs]
def format_configuration_content(parser_name: str, config: "Configuration", **kwargs):
"""
Convert a configuration object to a string using the given parser.
"""
return get_configuration_parser(parser_name, **kwargs).generate(
config.__tree__(), pretty=True
)
[docs]
def make_configuration_diagram(config):
dot = f'digraph "{config.name or "network"}" {{'
for c in config.cell_types.values():
dot += f'\n {c.name}[label="{c.name}"]'
for name, conn in config.connectivity.items():
for pre in conn.presynaptic.cell_types:
for post in conn.postsynaptic.cell_types:
dot += f'\n {pre.name} -> {post.name}[label="{name}"];'
dot += "\n}\n"
return dot
def _try_parsers(content, classes, ext=None, path=None): # pragma: nocover
if ext is not None:
def file_has_parser_ext(kv):
return ext not in getattr(kv[1], "data_extensions", ())
classes = builtins.dict(sorted(classes.items(), key=file_has_parser_ext))
exc = {}
for name, cls in classes.items():
try:
tree, meta = cls().parse(content, path=path)
except Exception as e:
if getattr(e, "_bsbparser_show_user", False):
raise e from None
exc[name] = e
else:
return (name, tree, meta)
msges = [
(
f"- Can't parse contents with '{n}':\n",
"".join(traceback.format_exception(type(e), e, e.__traceback__)),
)
for n, e in exc.items()
]
if path:
msg = f"Could not parse '{path}'"
else:
msg = f"Could not parse content string ({len(content)} characters long)"
raise ParserError("\n".join(ichain(msges)) + f"\n{msg}")
def _from_parsed(parser_name, tree, meta, file=None):
from ._config import Configuration
conf = Configuration(tree)
conf._parser = parser_name
conf._meta = meta
conf._file = file
return conf
[docs]
def parse_configuration_file(file, parser=None, path=None, **kwargs):
if hasattr(file, "read"):
data = file.read()
try:
path = str(path) or os.fspath(file)
except TypeError:
pass
else:
file = os.path.abspath(file)
path = path or file
with open(file, "r") as f:
data = f.read()
return parse_configuration_content(data, parser, path, **kwargs)
[docs]
def parse_configuration_content(content, parser=None, path=None, **kwargs):
if parser is None:
parser_classes = get_configuration_parser_classes()
ext = path.split(".")[-1] if path is not None else None
parser_name, tree, meta = _try_parsers(content, parser_classes, ext, path=path)
elif isinstance(parser, str):
parser_name = parser
parser = get_configuration_parser(parser_name, **kwargs)
tree, meta = parser.parse(content, path=path)
else:
parser_name = parser.__name__
tree, meta = parser.parse(content, path=path)
return _from_parsed(parser_name, tree, meta, path)
# Static public API
__all__ = [
"Configuration",
"ConfigurationAttribute",
"Distribution",
"after",
"attr",
"before",
"catch_all",
"compose_nodes",
"copy_configuration_template",
"dict",
"dynamic",
"file",
"format_configuration_content",
"get_config_attributes",
"get_config_path",
"has_hook",
"list",
"make_configuration_diagram",
"node",
"on",
"parse_configuration_file",
"parse_configuration_content",
"pluggable",
"property",
"provide",
"ref",
"refs",
"reflist",
"root",
"run_hook",
"slot",
"types",
"unset",
"walk_node_attributes",
"walk_nodes",
]
__api__ = [
"Configuration",
"ConfigurationAttribute",
"Distribution",
"compose_nodes",
"copy_configuration_template",
"format_configuration_content",
"get_config_attributes",
"get_config_path",
"make_config_diagram",
"parse_configuration_file",
"parse_configuration_content",
"refs",
"types",
"walk_node_attributes",
"walk_nodes",
]