예제 #1
0
def _add_compute_or_output(reactor, type_, name, function, dependencies,
                           predicates=None, persistent=True):
    """Add a compute or output cell with dependencies.  Note that they don't
    actually have to exist; this will be checked when the 'run()' function is
    invoked which will do a consistency check on the reactor core before
    proceeding.

    The `function` is the item to put in the context.  If predicates is not
    None then multiple functions may be added (via multiple calls to the
    function) and the FIRST to have all predicates as True will be used on a
    first added basis.  `function` is a callable.

    If predicates is not None, then all but one calls to add_compute() with the
    same `name` have to have predicates, except for one, which will be the
    default function for that `name`.

    :param reactor: the reactor to work with
    :param type_: the type of cell, either Type.COMPUTE or Type.OUTPUT
    :param name: the cell's name
    :param function: what to call for the function
    :param dependencies: list of cell names as dependencies that trigger this
        function.
    :param predicates: an OPTIONAL list of predicates to decide which function
        to store in the context.
    :param persistent: boolean, default True, that if False, means that the
        cell is always 'changed' after computation.
    """
    _assert_valid_entry(reactor, name, (predicates is None))
    assert type_ in (Type.COMPUTE, Type.OUTPUT)
    assert callable(function), ("Param function must be callable for key '{}'"
                                .format(name))
    key = maybe_format_key(name)
    dependencies_keys = [maybe_format_key(d) for d in dependencies]
    # look for a simple circular dependency
    if key in dependencies:
        raise KeyExists(
            "The dependency '{}' is the name of the compute.".format(name))
    if key not in reactor:
        reactor[key] = ReactorItem(type_, key, name, [], set())
    elif reactor[key].type_ != type_:
        raise ReactorError(
            "Attempting to add a {} to cell '{}' that is a '{}'"
            .format(
                'compute' if type_ == Type.COMPUTE else 'output',
                key,
                reactor[key].type_))
    reactor[key].variants.append(
        ReactorItemVariant(function,
                           dependencies_keys,
                           predicates or [],
                           bool(persistent)))
예제 #2
0
def context(keys=None, _context=None):
    """The context() is a readonly, lazy, data structure that resolves to
    dictionaries, lists and values.  It uses an OrderedDict() to keep keys
    in order, and when serialized (for comparison purposes only), it sorts the
    keys of dictionaries for consistency.

    If keys is set, and not a string, then it is used to filter the context, to
    provided a restricted set of keys.

    :param keys: if keys is a list or string, then a restricting version of the
        context is returned.
    :param _context: the context to work with; defaults to the global
        __context__
    :returns: read only dictionary-like object
    :raises: AttributeError if the keys are not valid identifiers
    :raises: KeyError if a keys is not None and the key is missing
    """
    if _context is None:
        global __context__
        _context = __context__
    if keys is None:
        return ReadOnlyWrapperDict(_context)
    ctxt = collections.OrderedDict()
    if isinstance(keys, str):
        keys = (keys, )
    elif not isinstance(keys, collections.abc.Sequence):
        raise RuntimeError("keys passed to context is not a string or sequence"
                           ": {}".format(keys))
    for key in keys:
        k = maybe_format_key(key)
        ctxt[k] = _context[k]
    return ReadOnlyWrapperDict(ctxt)
예제 #3
0
def _add_input(reactor, name, storable, predicates=None, persistent=True):
    """Add an input cell to the reactor, with the function 'function'.

    The `storable` is the item to put in the context.  If predicates is not
    None then multiple storables may be added (via multiple calls to add_input)
    and the FIRST to have all predicates as True will be used on a first added
    basis.  `storable` is either a callable (no parameters) or a value.

    If predicates is not None, then all but one calls to add_input() with the
    same `name` have to have predicates, except for one, which will be the
    default storable for that `name`.

    :param reactor: the reactor to work on
    :param name: the name of the input
    :param storable: the item to store in the cell/context
    :param predicates: an OPTIONAL list of predicates to decide which storable
        to store in the context.
    :param persistent: boolean, if True, it is persistent, if False ALWAYS
        changed.
    :raises AssertionError: if multiple defaults are provided.
    """
    _assert_valid_entry(reactor, name, (predicates is None))
    key = maybe_format_key(name)
    if key not in reactor:
        reactor[key] = ReactorItem(Type.INPUT, key, name, [], set())
    elif reactor[key].type_ != Type.INPUT:
        raise ReactorError(
            "Attempting to add an input to cell '{}' that is a '{}'"
            .format(key, reactor[key].type_))
    reactor[key].variants.append(
        ReactorItemVariant(storable, [], predicates or [], bool(persistent)))
예제 #4
0
    def test_maybe_format_key_invalid_patterns(self):
        patterns = (
            '$',
            '#',
            '"',
            "'",
            "0",
            "0a",
            "1234",
            "1_",
            "1/",
            '1\\',
        )

        for p in patterns:
            with self.assertRaises(AttributeError):
                u.maybe_format_key(p)
예제 #5
0
    def __getitem__(self, key):
        """Gets the item using the key, resolves a callable.

        :param key: string, or indexable object
        :returns: value of item
        """
        return maybe_resolve_callable(super().__getitem__(
            maybe_format_key(key)))
예제 #6
0
    def __init__(self, data):
        """Initialise the dictionary, by copying the keys and values.  This
        recurses till the values are simple values, or callables.

        :param data: a dictionary/mapping supporting structure (iter)
        :raises AssertionError: if data is not iterable and mapping
        """
        assert isinstance(data, collections.abc.Mapping)
        for k, v in data.items():
            super().__setitem__(maybe_format_key(k), resolve_value(v))
예제 #7
0
def key_exists(key, _context=None):
    """Returns True if the key exists in the context.

    :param key: str
    :param _context: the context to work with; defaults to the global
        __context__
    :returns: boolean
    """
    if _context is None:
        global __context__
        _context = __context__
    return maybe_format_key(key) in _context
예제 #8
0
def set_context(key, data, _context=None):
    """Set a top level item to some data.

    The top level is the only way of adding data to the context.  If the key
    already exists, a KeyExists error is returned.

    :param key: str for the key.
    :param data: either a dictionary, list, callable or value
    :param _context: the context to work with; defaults to the global
        __context__
    :raises: KeyExists exception if the key already exists
    """
    if _context is None:
        global __context__
        _context = __context__
    key = maybe_format_key(key)
    if key in _context:
        raise KeyExists("Key '{}' already exists".format(key))
    _context[key] = resolve_value(data)
예제 #9
0
def _assert_valid_entry(reactor, name, new_is_default):
    """Check that name, along with the new_is_default flag, will only produce a
    single default or no defaults.  We can't have multiple defaults for a key
    name, and thus we trap it when it happens.

    :param reactor: the reactor to work on
    :param name: (String) key name
    :param new_is_default: the next entry will be a default.
    :raises AssertionError: if there is already a default
    """
    key = maybe_format_key(name)
    # if the new one isn't a default then it's going to be okay
    if not new_is_default or key not in reactor:
        return
    for item in reactor[key].variants:
        if not item.predicates:
            # a default already exists
            raise AssertionError("A default already exists for key '{}'"
                                 .format(name))
예제 #10
0
def copy(key, _context=None):
    """Create a late-binding copy function that copies from a name to another
    context.

    Usage would be something like:

    set_context('copy-to', copy('copy-from'))

    As context is read only, we can use a lambda to lazily evaluate the
    context.get() operation and only do it when 'copy-to' is accessed.

    :param key: str for the key.
    :param _context: the context to work with; defaults to the global
        __context__
    :returns: F() -> copied context entry
    """
    if _context is None:
        global __context__
        _context = __context__
    return lambda: context(_context=_context).get(maybe_format_key(key))
예제 #11
0
    def test_maybe_format_key_valid(self):
        patterns = (
            ('a', 'a'),
            ('aa', 'aa'),
            ('a1', 'a1'),
            ('a_', 'a_'),
            ('_a', '_a'),
            ('a_a', 'a_a'),
            ('-', '_'),
            ('--', '__'),
            ('a-', 'a_'),
            ('-a', '_a'),
            ('a-b', 'a_b'),
            ('a-b-c', 'a_b_c'),
            ('a-1', 'a_1'),
            ('the-attr', 'the_attr'),
            ('/', '__'),
            ('//', '____'),
            ('/the/path.conf', '__the__path_conf'),
            (r'\the\path.conf', '__the__path_conf'),
        )

        for p in patterns:
            self.assertEqual(u.maybe_format_key(p[0]), p[1])
예제 #12
0
def serialize_key(key=None, _context=None):
    """Serialise to a string the context, and optionally, just a key.

    This method resolves the callables, and serialises the context (or a
    top-level key of it) to a compact string.  This is for comparason purposes,
    as the context is NOT supposed to be serialised to a backing store.

    If there is no context at the key then None is returned

    :param key: OPTIONAL top level string key.
    :param _context: the context to work with; defaults to the global
        __context__
    :returns: JSON string repr of the data in the context, optionally at key
    :raises: KeyError if the key is not found (and not None)
    """
    if _context is None:
        global __context__
        _context = __context__
    c = context(_context=_context)
    if key is not None:
        c = c.get(maybe_format_key(key), None)
        if c is None:
            return None
    return ContextJSONEncoder(**JSON_ENCODE_OPTIONS).encode(c)
예제 #13
0
 def test_format_key_is_identity_when_not_passed_string(self):
     self.assertEqual(u.maybe_format_key(1), 1)
예제 #14
0
def key_exists(key):
    return maybe_format_key(key) in __reactor__