Module nlisim.config

Expand source code
from collections import OrderedDict
from configparser import ConfigParser
from importlib import import_module
from io import StringIO, TextIOBase
from pathlib import PurePath
import re
from typing import TYPE_CHECKING, List, Optional, TextIO, Type, Union

if TYPE_CHECKING:
    from nlisim.module import ModuleModel


class SimulationConfig(ConfigParser):
    """
    Internal representation of a simulation config.

    This is based on the standard Python ConfigParser class.
    """

    def __init__(self, *config_sources: Union[str, PurePath, TextIO, dict]) -> None:
        super().__init__(allow_no_value=False, inline_comment_prefixes=('#',))
        self._modules: OrderedDict[str, 'ModuleModel'] = OrderedDict()

        for config_source in config_sources:
            if isinstance(config_source, dict):
                self.read_dict(config_source)
            elif isinstance(config_source, TextIOBase):
                self.read_file(config_source)
            else:
                self.read(config_source)

        for module_path in self.getlist('simulation', 'modules'):
            self.add_module(module_path)

    @property
    def modules(self) -> List['ModuleModel']:
        """Return a list of instantiated modules connected to this config."""
        return list(self._modules.values())

    def add_module(self, module_ref: Union[str, Type['ModuleModel']]):
        if isinstance(module_ref, str):
            module_func = self.load_module(module_ref)
        else:
            module_func = module_ref
            self.validate_module(module_func)

        module = module_func(self)

        if module.name in self._modules:
            raise ValueError(f'A module named {module.name} already exists')
        self._modules[module.name] = module

    @classmethod
    def load_module(cls, path: str) -> Type['ModuleModel']:
        """Load a module class, returning the class constructor."""
        module_path, func_name = path.rsplit('.', 1)
        module = import_module(module_path)
        func = getattr(module, func_name, None)

        cls.validate_module(func, path)

        return func

    @classmethod
    def validate_module(cls, func: Type['ModuleModel'], path: Optional[str] = None) -> None:
        """Validate basic aspects of a module class."""
        from nlisim.module import ModuleModel  # noqa avoid circular imports

        if path is None:
            path = repr(func)

        if func is None or not issubclass(func, ModuleModel):
            raise TypeError(f'Invalid module class for "{path}"')
        if not func.name.isidentifier() or func.name.startswith('_'):
            raise ValueError(f'Invalid module name "{func.name}" for "{path}')

    # Wrapper so that this fails when a parameter is missing
    def getint(self, section: str, option: str, **kwargs) -> int:
        result = super().getint(section, option, **kwargs)
        assert result is not None, f'Missing parameter {option} in section {section}'
        return result

    # Wrapper so that this fails when a parameter is missing
    def getfloat(self, section, option, **kwargs) -> float:
        result = super().getfloat(section, option, **kwargs)
        assert result is not None, f'Missing parameter {option} in section {section}'
        return result

    # Wrapper so that this fails when a parameter is missing
    def getboolean(self, section, option, **kwargs) -> bool:
        result = super().getboolean(section, option, **kwargs)
        assert result is not None, f'Missing parameter {option} in section {section}'
        return result

    # Wrapper so that this fails when a parameter is missing
    def get(self, section, option, **kwargs):
        result = super(ConfigParser, self).get(section, option, **kwargs)
        assert result is not None, f'Missing parameter {option} in section {section}'
        return result

    # TODO: see if there is a slicker way to do these gettype wrappers.
    # TODO: do checking on 'type-less' get (or implement one for strings)

    def getlist(self, section: str, option: str) -> List[str]:
        """
        Return a list of strings from a configuration value.

        This method reads a string value from the configuration object and splits
        it by: new lines, spaces, and commas.  The values in the returned list are
        stripped of all white space and removed if empty.

        For example, the following values all parse as <code>`['a', 'b', 'c']`</code>:

            value = a,b,c
            value = a b c
            value = a, b, c
            value = a
                    b
                    c
            value = a,
                    b,
                    c
            value = a b
                    ,c
        """
        return self.parselist(self.get(section, option, fallback=''))

    @classmethod
    def parselist(cls, value: str) -> List[str]:
        """
        Return a list of strings from a configuration value.

        This is a helper method for `getlist`, split out for code sharing.
        """
        values = []
        for value in re.split('[\n ,]+', value):
            stripped = value.strip()
            if stripped:
                values.append(stripped)
        return values

    def __str__(self):
        f = StringIO()
        self.write(f)
        return f.getvalue()

Classes

class SimulationConfig (*config_sources: Union[str, pathlib.PurePath, TextIO, dict])

Internal representation of a simulation config.

This is based on the standard Python ConfigParser class.

Expand source code
class SimulationConfig(ConfigParser):
    """
    Internal representation of a simulation config.

    This is based on the standard Python ConfigParser class.
    """

    def __init__(self, *config_sources: Union[str, PurePath, TextIO, dict]) -> None:
        super().__init__(allow_no_value=False, inline_comment_prefixes=('#',))
        self._modules: OrderedDict[str, 'ModuleModel'] = OrderedDict()

        for config_source in config_sources:
            if isinstance(config_source, dict):
                self.read_dict(config_source)
            elif isinstance(config_source, TextIOBase):
                self.read_file(config_source)
            else:
                self.read(config_source)

        for module_path in self.getlist('simulation', 'modules'):
            self.add_module(module_path)

    @property
    def modules(self) -> List['ModuleModel']:
        """Return a list of instantiated modules connected to this config."""
        return list(self._modules.values())

    def add_module(self, module_ref: Union[str, Type['ModuleModel']]):
        if isinstance(module_ref, str):
            module_func = self.load_module(module_ref)
        else:
            module_func = module_ref
            self.validate_module(module_func)

        module = module_func(self)

        if module.name in self._modules:
            raise ValueError(f'A module named {module.name} already exists')
        self._modules[module.name] = module

    @classmethod
    def load_module(cls, path: str) -> Type['ModuleModel']:
        """Load a module class, returning the class constructor."""
        module_path, func_name = path.rsplit('.', 1)
        module = import_module(module_path)
        func = getattr(module, func_name, None)

        cls.validate_module(func, path)

        return func

    @classmethod
    def validate_module(cls, func: Type['ModuleModel'], path: Optional[str] = None) -> None:
        """Validate basic aspects of a module class."""
        from nlisim.module import ModuleModel  # noqa avoid circular imports

        if path is None:
            path = repr(func)

        if func is None or not issubclass(func, ModuleModel):
            raise TypeError(f'Invalid module class for "{path}"')
        if not func.name.isidentifier() or func.name.startswith('_'):
            raise ValueError(f'Invalid module name "{func.name}" for "{path}')

    # Wrapper so that this fails when a parameter is missing
    def getint(self, section: str, option: str, **kwargs) -> int:
        result = super().getint(section, option, **kwargs)
        assert result is not None, f'Missing parameter {option} in section {section}'
        return result

    # Wrapper so that this fails when a parameter is missing
    def getfloat(self, section, option, **kwargs) -> float:
        result = super().getfloat(section, option, **kwargs)
        assert result is not None, f'Missing parameter {option} in section {section}'
        return result

    # Wrapper so that this fails when a parameter is missing
    def getboolean(self, section, option, **kwargs) -> bool:
        result = super().getboolean(section, option, **kwargs)
        assert result is not None, f'Missing parameter {option} in section {section}'
        return result

    # Wrapper so that this fails when a parameter is missing
    def get(self, section, option, **kwargs):
        result = super(ConfigParser, self).get(section, option, **kwargs)
        assert result is not None, f'Missing parameter {option} in section {section}'
        return result

    # TODO: see if there is a slicker way to do these gettype wrappers.
    # TODO: do checking on 'type-less' get (or implement one for strings)

    def getlist(self, section: str, option: str) -> List[str]:
        """
        Return a list of strings from a configuration value.

        This method reads a string value from the configuration object and splits
        it by: new lines, spaces, and commas.  The values in the returned list are
        stripped of all white space and removed if empty.

        For example, the following values all parse as <code>`['a', 'b', 'c']`</code>:

            value = a,b,c
            value = a b c
            value = a, b, c
            value = a
                    b
                    c
            value = a,
                    b,
                    c
            value = a b
                    ,c
        """
        return self.parselist(self.get(section, option, fallback=''))

    @classmethod
    def parselist(cls, value: str) -> List[str]:
        """
        Return a list of strings from a configuration value.

        This is a helper method for `getlist`, split out for code sharing.
        """
        values = []
        for value in re.split('[\n ,]+', value):
            stripped = value.strip()
            if stripped:
                values.append(stripped)
        return values

    def __str__(self):
        f = StringIO()
        self.write(f)
        return f.getvalue()

Ancestors

  • configparser.ConfigParser
  • configparser.RawConfigParser
  • collections.abc.MutableMapping
  • collections.abc.Mapping
  • collections.abc.Collection
  • collections.abc.Sized
  • collections.abc.Iterable
  • collections.abc.Container

Static methods

def load_module(path: str) ‑> Type[ModuleModel]

Load a module class, returning the class constructor.

Expand source code
@classmethod
def load_module(cls, path: str) -> Type['ModuleModel']:
    """Load a module class, returning the class constructor."""
    module_path, func_name = path.rsplit('.', 1)
    module = import_module(module_path)
    func = getattr(module, func_name, None)

    cls.validate_module(func, path)

    return func
def parselist(value: str) ‑> List[str]

Return a list of strings from a configuration value.

This is a helper method for getlist, split out for code sharing.

Expand source code
@classmethod
def parselist(cls, value: str) -> List[str]:
    """
    Return a list of strings from a configuration value.

    This is a helper method for `getlist`, split out for code sharing.
    """
    values = []
    for value in re.split('[\n ,]+', value):
        stripped = value.strip()
        if stripped:
            values.append(stripped)
    return values
def validate_module(func: Type[ForwardRef('ModuleModel')], path: Optional[str] = None)

Validate basic aspects of a module class.

Expand source code
@classmethod
def validate_module(cls, func: Type['ModuleModel'], path: Optional[str] = None) -> None:
    """Validate basic aspects of a module class."""
    from nlisim.module import ModuleModel  # noqa avoid circular imports

    if path is None:
        path = repr(func)

    if func is None or not issubclass(func, ModuleModel):
        raise TypeError(f'Invalid module class for "{path}"')
    if not func.name.isidentifier() or func.name.startswith('_'):
        raise ValueError(f'Invalid module name "{func.name}" for "{path}')

Instance variables

var modules : List[ModuleModel]

Return a list of instantiated modules connected to this config.

Expand source code
@property
def modules(self) -> List['ModuleModel']:
    """Return a list of instantiated modules connected to this config."""
    return list(self._modules.values())

Methods

def add_module(self, module_ref: Union[str, Type[ForwardRef('ModuleModel')]])
Expand source code
def add_module(self, module_ref: Union[str, Type['ModuleModel']]):
    if isinstance(module_ref, str):
        module_func = self.load_module(module_ref)
    else:
        module_func = module_ref
        self.validate_module(module_func)

    module = module_func(self)

    if module.name in self._modules:
        raise ValueError(f'A module named {module.name} already exists')
    self._modules[module.name] = module
def get(self, section, option, **kwargs)

Get an option value for a given section.

If vars' is provided, it must be a dictionary. The option is looked up invars' (if provided), section', and inDEFAULTSECT' in that order. If the key is not found and fallback' is provided, it is used as a fallback value.None' can be provided as a `fallback' value.

If interpolation is enabled and the optional argument `raw' is False, all interpolations are expanded in the return values.

Arguments raw',vars', and `fallback' are keyword only.

The section DEFAULT is special.

Expand source code
def get(self, section, option, **kwargs):
    result = super(ConfigParser, self).get(section, option, **kwargs)
    assert result is not None, f'Missing parameter {option} in section {section}'
    return result
def getboolean(self, section, option, **kwargs) ‑> bool
Expand source code
def getboolean(self, section, option, **kwargs) -> bool:
    result = super().getboolean(section, option, **kwargs)
    assert result is not None, f'Missing parameter {option} in section {section}'
    return result
def getfloat(self, section, option, **kwargs) ‑> float
Expand source code
def getfloat(self, section, option, **kwargs) -> float:
    result = super().getfloat(section, option, **kwargs)
    assert result is not None, f'Missing parameter {option} in section {section}'
    return result
def getint(self, section: str, option: str, **kwargs) ‑> int
Expand source code
def getint(self, section: str, option: str, **kwargs) -> int:
    result = super().getint(section, option, **kwargs)
    assert result is not None, f'Missing parameter {option} in section {section}'
    return result
def getlist(self, section: str, option: str) ‑> List[str]

Return a list of strings from a configuration value.

This method reads a string value from the configuration object and splits it by: new lines, spaces, and commas. The values in the returned list are stripped of all white space and removed if empty.

For example, the following values all parse as ['a', 'b', 'c']:

value = a,b,c
value = a b c
value = a, b, c
value = a
        b
        c
value = a,
        b,
        c
value = a b
        ,c
Expand source code
def getlist(self, section: str, option: str) -> List[str]:
    """
    Return a list of strings from a configuration value.

    This method reads a string value from the configuration object and splits
    it by: new lines, spaces, and commas.  The values in the returned list are
    stripped of all white space and removed if empty.

    For example, the following values all parse as <code>`['a', 'b', 'c']`</code>:

        value = a,b,c
        value = a b c
        value = a, b, c
        value = a
                b
                c
        value = a,
                b,
                c
        value = a b
                ,c
    """
    return self.parselist(self.get(section, option, fallback=''))