Source code for bsb.config

"""
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 contextlib
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) env_paths = () if env_paths is None else env_paths.split(":") plugin_paths = plugins.discover("config.templates") return [*itertools.chain((os.getcwd(),), env_paths, *plugin_paths.values())]
def get_configuration_template(template, path=None): """ Returns the configuration template files matching the provided name. :param str template: name of the configuration template :param list path: list of paths to search for configuration templates :rtype: list[str] """ 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 ) return files
[docs] def copy_configuration_template(template, output="network_configuration.json", path=None): """ Copy the first configuration template file matching the provided name to the provided output filename. :param str template: name of the configuration template :param str output: name of the output file :param list path: list of paths to search for configuration templates """ copy_file(get_configuration_template(template, path)[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: str, tree: dict, meta: dict, file: str = 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): """ Parse a configuration file into a Configuration. :param str file: path to the configuration file :param str path: path from which to parse the configuration file. Use this parameter to mock the configuration file location which can help resolve relative paths in configuration files. :param ConfigurationParser parser: parser to use. If None is provided, the BSB will parse the content based on the file extension defined in `path` and the available ConfigurationParser classes that the BSB could find. :return: the parsed configuration. :rtype: Configuration """ if hasattr(file, "read"): data = file.read() with contextlib.suppress(TypeError): path = str(path) or os.fspath(file) else: file = os.path.abspath(file) path = path or file with open(file) as f: data = f.read() return parse_configuration_content(data, parser, path, **kwargs)
[docs] def parse_configuration_content_to_dict(content, parser=None, path=None, **kwargs): """ Parse the content of a configuration file into a dictionary. :param str content: content of a configuration file :param str path: path to the location of the configuration file :param ConfigurationParser parser: parser to use. If None is provided, the BSB will parse the content based on the file extension defined in `path` and the available ConfigurationParser classes that the BSB could find. :return: a dictionary of the parsed content, a dictionary storing the metadata, and the name of the parser used to obtain the results. :rtype: (dict, dict, str) """ 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 tree, meta, parser_name
[docs] def parse_configuration_content(content, parser=None, path=None, **kwargs): """ Parse the content of a configuration file into a Configuration. :param str content: content of a configuration file :param str path: path to the location of the configuration file :param ConfigurationParser parser: parser to use. If None is provided, the BSB will parse the content based on the file extension defined in `path` and the available ConfigurationParser classes that the BSB could find. :return: the parsed configuration. :rtype: Configuration """ tree, meta, parser_name = parse_configuration_content_to_dict( content, parser, path, **kwargs ) 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_to_dict", "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_to_dict", "parse_configuration_content", "refs", "types", "walk_node_attributes", "walk_nodes", ]