Giter Club home page Giter Club logo

coil's People

Contributors

mr-rodgers avatar

Watchers

 avatar  avatar

coil's Issues

Add runtime

Motivation

In order to offer the enhanced syntax for data bindings from #1, coil needs a way to keep track of active cross-property bindings, currently modelled as asyncio tasks. These tasks must necessarily be managed for the duration of the binding. This could have been alternatively handled with a context manager doing something like this:

async with coil.bind((host, prop)) as bindctx:
    bindctx.feed(bind(other_host, prop))

as this is also able to manage the lifetime of the data binding. While being easier to implement, this is still a more cumbersome syntax than proposed in #1. The more natural and readable syntax from #1 is preferred; therefore, a runtime must be created.

Capabilities

The coil runtime is an object which does the following:

  • acts as context manager, during which it registers itself globally
  • it provides a registry of tasks, which can be submitted with the following metadata:
    • a source bound value pair (optional)
    • an "id" which is unique per bound value
  • it allows to lookup tasks by a source bound value, and an id
  • when its context manager is shut down, it cancels all running tasks and awaits them
  • it allows to deregister a task

Example

task1 = asyncio.create_task(...)
task2 = asyncio.create_task(...)

async with coil.runtime() as rt:
    rt.register(task1, "some-task")
    rt.register(task2, "some-task", source=(host, prop))

    assert rt.find("some-task") is task1
    assert rt.find("some-task", source=(host, prop)) is task2

    rt.forget(task1)
    assert rt.find("some-task") is None 

assert task1.done()
assert task2.done()

Requirements

  • coil.Runtime() class should be provided.
    • its exact interface is up to implementor
    • it must however be usable as a context manager; using it as a context manager should:
      • register it in a contextvar as the current runtime
      • when the context exits, it should cancel and cleanup any remaining tasks
    • it should provide an interface to register a task with an id and optional bound value source
    • it should provide an interface to retrieve a task, given an id and optional bound value source
    • it should provide an interface to remove a task
  • coil.runtime() should return a Runtime() instance:
    • if there is a current runtime, it should return it
    • if there is no current runtime:
      • if argument ensure=True (the default), it should create a new runtime and return it
      • if argument ensure=False, it should raise a RuntimeError.

nicer syntax for a tail binding

Problem

Currently, bindings can be tailed into another, essentially allowing a 1-way bind from a bindable property into another. However, the syntax for doing this is particularly cumbersome:

import asyncio
from coil import bind, bindableclass
from coil.utils import tail

@bindableclass
class Box:
    value: int = 0

async def main():
    orig = Box()
    shadow = Box()

    task = tail(bind((orig, "value")), into=bind((shadow, "value"), readonly=False))

    for i in range(10):
        orig.value = i
        await asyncio.sleep(0.5)
        print(shadow.value)

    task.cancel()
    await task

if __name__ == '__main__':
    asyncio.run(main())

This 1-way bind is done using the tail(...) function, which has a somewhat finicky interface:

  • It requires the input of two distinct bound values. Theses are constructed above using the bind(...) function; the bound values could also have been constructed from the property descriptor: tail(Box.value.bind(orig), into=Box.value.bind(shadow, readonly=False)). That improves readability, but it's still clunky.
  • It returns a task, which is essentially used to represent the "lifetime" of the 1-way bind. This task requires additional scaffolding, since it needs to be cancelled and then awaited once the binding is no longer required. This could become even more of hassle if the application needs to maintain several instances of these 1-way bindings.

Abstraction layer for bindings

coil should introduce an abstraction layer for tail bindings:

# under the hood
task = tail(bind((orig, "value")), into=bind((shadow, "value"), readonly=False))
...
task.cancel()
await task

# abstraction
async with coil.runtime():
    ...
    shadow.value = bind((orig, "value"))
    ...

The abstraction allows to create a 1-way binding by assigning a coil-bound value to a bindable property. This binding lasts as long as shadow.value is not reassigned.

In order to manage this tail task in the background, however, coil will require a runtime of some kind. That runtime would need to run for the entire duration of the application (or for as long as these bindings are useful), and one of its functions will be to manage the tasks that are required by this one way binding.

The runtime is not part of this issue, however (see #2).

Two way bindings

So far we have only considered 1-way bindings which are modelled by a tail(...) usage. The abstraction above can be extended to support 2-way bindings, by simply assigning an appropriate bind(...) result:

shadow.value = bind((orig, "value"), readonly=False)

This will, however, require some changes to support:

  • the property descriptors will need to be able to check if a bind result is a TwoWayBound (which setting readonly=False returns). This is currently made difficult, because bind(...) actually returns the same type under the hood for Bound and TwoWayBound, and sort of relies on the type checker to enforce the readonly argument.
  • Event triggering cycles need to be detected and eliminated. This may be accomplished by using the source chain in an event, such that events which can find themselves in the source chain should be ignored.

Requirements

  • split Binding into Binding and TwoWayBinding.
    • Binding should provide no .set(...) function
    • TwoWayBinding should subclass Binding to add it
    • both Bound and TwoWayBound should be @runtime_checkable
  • rename DataEvent["source"] to DataEvent["source_event"]
  • add DataEvent["source"] which tracks the Bound which the event comes from
  • update notify_subscribers() so that it ignores events which have the same bound value in its source chain
  • when assigning to a bindable field:
    • If there was a binding previously assigned, it must be deactivated.
    • If the value is a Bound, then the 1-way tail must be created and tracked with the runtime.
    • If the value is a TwoWayBound, then a 2-way bind is simulated by creating tails in opposite directions, and tracking them with the runtime

add `coil.Runtime.synchronise()`

Motivation

Currently, some framework operations are performed asynchronously (see #1), by wrapping them in tasks which are managed by a runtime. The problem with this, is that due to the synchronous context under which these operations are triggered (mainly member assignments/deletions), even if these operations are performed in an asyncio context, code which uses them has no way of knowing exactly when such operations have truly taken effect.

Clearing of tail bindings

One such example is the clearing of tail bindings. Standard syntax for both setting and clearing tail binding background tasks are necessarily synchronous, since they rely on Python object assignment:

@coil.bindableclass
class Box:
    value: int = 0

async def main():
    async with runtime():
        source = Box()
        target = Box()

        # tails values from source into target
        target.value = Box.value.bind(source)

        # stops tailing values from source into target
        # there is a slight delay before this takes effect
        target.value = Box.value.bind(target)
        

The correct initial linking of the binding usually takes place immediately. This is because the binding is fed by an event stream under the hood, and all of the necessary setup (registration of event hooks, event stream construction, creation of task for consuming event stream and applying the binding) takes place before the synchronous assignment operation returns.

The reverse operation however, is another story. Since bindings are managed by asyncio tasks which consume a value's event stream and applies changes to the binding target, and since task cancellations are asynchronous, a complete cancellation of the task cannot be guaranteed during a synchronous assignment operation. What's more, due to the semantics of this operation in the Python language, there is no way to give feedback to the developer on that task's cancellation through this assignment operation.

The unfortunate consequence then, is that a developer may clear a binding, but have no way to predict when it is safe again to write a new value into the attribute. The framework needs to provide such a facility to the developer, however broad.

Solution

A library function should be added, which can wait until all of it's pending task evictions are done processing. In other words, it waits for all tasks which have as of yet been discarded by the framework to fully cancel. This will provide the developer with a way to wait for the unwinding of tail bindings, before proceeding the setting new values on the target attribute.

The part of code that concerns itself with this is the runtime. Indeed, the background tasks which manage the tail bindings are in turn managed by whichever Runtime is active at the time when the assignment is made. Such a function will need to get the current runtime, and wait for all of its pending cancellations to complete before returning.

Requirements

  • Supply a function (exact name and iterface is up to the implementor) which:
    • Before returning, ensures that every pending task cancellation on the currently active runtime (at the time of calling) has been processed.
    • If there is no active runtime, raise a RuntimeError

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.