diff --git a/scripttease/parsers/utils.py b/scripttease/parsers/utils.py index a656ee6..678365f 100644 --- a/scripttease/parsers/utils.py +++ b/scripttease/parsers/utils.py @@ -1,7 +1,9 @@ # Imports -from commonkit import any_list_item +from commonkit import any_list_item, smart_cast, split_csv +from configparser import RawConfigParser import logging +import os from ..constants import LOGGER_NAME from .ini import Config @@ -12,6 +14,10 @@ log = logging.getLogger(LOGGER_NAME) __all__ = ( "filter_commands", "load_commands", + "load_config", + "load_variables", + "Context", + "Variable", ) # Functions @@ -108,3 +114,212 @@ def load_config(path, overlay="ubuntu", **kwargs): return None return _config + + +def load_variables(path, environment=None): + """Load variables from a file. + + :param path: The path to the file. + :type path: str + + :param environment: Filter variables by the given environment name. + :type environment: str + + :rtype: list[scripttease.parsers.utils.Variable] + + """ + if not os.path.exists(path): + log.warning("Path to variables file does not exist: %s" % path) + return list() + + if path.endswith(".ini"): + return _load_variables_ini(path, environment=environment) + else: + log.warning("Variable file format is not currently supports: %s" % path) + return list() + + +def _load_variables_ini(path, environment=None): + """Load variables from an INI file. See ``load_variables()``.""" + + ini = RawConfigParser() + ini.read(path) + + a = list() + for section in ini.sections(): + if ":" in section: + variable_name, _environment = section.split(":") + else: + _environment = None + variable_name = section + + _kwargs = { + 'environment': _environment, + } + for key, value in ini.items(section): + if key == "tags": + value = split_csv(value) + else: + value = smart_cast(value) + + _kwargs[key] = value + + a.append(Variable(variable_name, **_kwargs)) + + if environment is not None: + b = list() + for var in a: + if var.environment and var.environment == environment or var.environment is None: + b.append(var) + + return b + + return a + +# Classes + + +class Context(object): + """A collection of variables.""" + + def __init__(self, **kwargs): + """Initialize the context. + + kwargs are added as variable instances. + + """ + self.variables = dict() + + for key, value in kwargs.items(): + self.add(key, value) + + def __getattr__(self, item): + if item in self.variables: + return self.variables[item].value + + return None + + def __repr__(self): + return "<%s (%s)>" % (self.__class__.__name__, len(self.variables)) + + def add(self, name, value, environment=None, tags=None): + """Add a variable to the context. + + :param name: The name of the variable. + :type name: str + + :param value: The value of the variable in this context. + + :param environment: The environment name to which the variable applies. ``None`` applies to all environments. + :type environment: str + + :param tags: A list of tags that describe the variable. + :type tags: list[str] + + :rtype: scripttease.parsers.utils.Variable + + :raise: RuntimeError + :raises: ``RuntimeError`` if the variable already exists. + + """ + if name in self.variables: + raise RuntimeError("Variable already exists: %s" % name) + + v = Variable(name, value, environment=environment, tags=tags) + self.variables[name] = v + + return v + + def get(self, name, default=None): + """Get a the value of the variable from the context. + + :param name: The name of the variable. + :type name: str + + :param default: The default value to return. + + """ + if not self.has(name): + return default + + return self.variables[name].value + + def has(self, name): + """Indicates whether the named variable exists in this context, and whether the value is not ``None``. + + :rtype: bool + + """ + if name not in self.variables: + return False + + return self.variables[name].value is not None + + def join(self, variables): + """Join a list of variables to the context. + + :param variables: the list of variables to be added. + :type variables: list[scripttease.parsers.utils.Variable] + + .. note:: + This *replaces* a variable if it already exists. + + """ + for v in variables: + self.variables[v.name] = v + + def mapping(self): + """Export the context as a dictionary. + + :rtype: dict + + """ + values = dict() + for key, var in self.variables.items(): + values[key] = var.value or var.default + + return values + + def merge(self, context): + """Merge another context with this one. + + :param context: The context to be merged. + :type context: scripttease.parser.utils.Context + + .. note:: + Variables that exist in the current context are *not* replaced with variables from the provided context. + + """ + for name, var in context.variables.items(): + if not self.has(name): + self.variables[name] = var + + +class Variable(object): + """Represents a variable to be used in the context of pre-processing a config file.""" + + def __init__(self, name, value, **kwargs): + """Initialize a variable. + + :param name: The variable name. + :type name: str + + :param value: The value of the variable. + + kwargs are added as attributes of the instance. + + """ + self.name = name + self.value = value + + kwargs.setdefault("tags", list()) + self._attributes = kwargs + + def __eq__(self, other): + return self.value == other + + def __getattr__(self, item): + return self._attributes.get(item) + + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, self.name) diff --git a/tests/examples/variables.ini b/tests/examples/variables.ini new file mode 100644 index 0000000..9fc7291 --- /dev/null +++ b/tests/examples/variables.ini @@ -0,0 +1,15 @@ +[domain_name] +value = example.com + +[domain_tld] +value = example_com + +[debug_enabled:testing] +value = True + +[postgres_version] +value = 11 +tags = postgres + +[mailgun_domain:live] +value = "mg.example.com diff --git a/tests/test_library_commands_base.py b/tests/test_library_commands_base.py index f6bafb6..e6bbd28 100644 --- a/tests/test_library_commands_base.py +++ b/tests/test_library_commands_base.py @@ -57,6 +57,10 @@ class TestCommand(object): assert c.sudo.user == "root" assert c.sudo.enabled is False + def test_is_itemized(self): + c = Command("ls -ls") + assert c.is_itemized is False + def test_repr(self): c = Command("ls -ls", comment="listing") assert repr(c) == "" @@ -94,6 +98,10 @@ class TestItemizedCommand(object): c = ItemizedCommand(python_pip, ["Pillow", "psycopg2-binary", "django"], "$item") assert c.has_attribute("testing") is False + def test_is_itemized(self): + c = ItemizedCommand(python_pip, ["Pillow", "psycopg2-binary", "django"], "$item") + assert c.is_itemized is True + def test_repr(self): c = ItemizedCommand(python_pip, ["Pillow", "psycopg2-binary", "django"], "$item") assert repr(c) == "" @@ -103,3 +111,20 @@ class TestItemizedCommand(object): assert c.testing is None c.set_attribute("testing", True) assert c.testing is True + + +class TestSudo(object): + + def test_bool(self): + s = Sudo() + assert bool(s) is False + + s = Sudo(True) + assert bool(s) is True + + def test_str(self): + s = Sudo() + assert str(s) == "" + + s = Sudo(True) + assert str(s) == "sudo -u root"