Skip to content

Refactor Dispatchers #353

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 31 additions & 30 deletions docs/source/core-concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@ whose body contains a hook usage. We'll demonstrate that with a simple
import idom


@idom.component
def ClickCount():
def use_counter():
count, set_count = idom.hooks.use_state(0)
return count, lambda: set_count(lambda old_count: old_count + 1)


@idom.component
def ClickCount():
count, increment_count = use_counter()
return idom.html.button(
{"onClick": lambda event: set_count(count + 1)},
{"onClick": lambda event: increment_count()},
[f"Click count: {count}"],
)

Expand All @@ -63,7 +67,7 @@ ever be removed from the model. Then you'll just need to call and await a

.. testcode::

async with idom.Layout(ClickCount()) as layout:
with idom.Layout(ClickCount()) as layout:
patch = await layout.render()

The layout also handles the triggering of event handlers. Normally these are
Expand All @@ -79,16 +83,18 @@ which we can re-render and see what changed:

static_handler = StaticEventHandler()


@idom.component
def ClickCount():
count, set_count = idom.hooks.use_state(0)
count, increment_count = use_counter()

# we do this in order to capture the event handler's target ID
handler = static_handler.use(lambda event: set_count(count + 1))
handler = static_handler.use(lambda event: increment_count())

return idom.html.button({"onClick": handler}, [f"Click count: {count}"])

async with idom.Layout(ClickCount()) as layout:

with idom.Layout(ClickCount()) as layout:
patch_1 = await layout.render()

fake_event = LayoutEvent(target=static_handler.target, data=[{}])
Expand All @@ -111,57 +117,52 @@ which we can re-render and see what changed:
Layout Dispatcher
-----------------

An :class:`~idom.core.dispatcher.AbstractDispatcher` implementation is a relatively thin layer
of logic around a :class:`~idom.core.layout.Layout` which drives the triggering of
events and layout updates by scheduling an asynchronous loop that will run forever -
effectively animating the model. To execute the loop, the dispatcher's
:meth:`~idom.core.dispatcher.AbstractDispatcher.run` method accepts two callbacks. One is a
"send" callback to which the dispatcher passes updates, while the other is "receive"
callback that's called by the dispatcher to events it should execute.
A "dispatcher" implementation is a relatively thin layer of logic around a
:class:`~idom.core.layout.Layout` which drives the triggering of events and updates by
scheduling an asynchronous loop that will run forever - effectively animating the model.
The simplest dispatcher is :func:`~idom.core.dispatcher.dispatch_single_view` which
accepts three arguments. The first is a :class:`~idom.core.layout.Layout`, the second is
a "send" callback to which the dispatcher passes updates, and the third is a "receive"
callback that's called by the dispatcher to collect events it should execute.

.. testcode::

import asyncio

from idom.core import SingleViewDispatcher, EventHandler
from idom.core.layout import LayoutEvent
from idom.core.dispatcher import dispatch_single_view


sent_patches = []

# We need this to simulate a scenario in which events ariving *after* each update
# has been sent to the client. Otherwise the events would all arive at once and we
# would observe one large update rather than many discrete updates.
sempahore = asyncio.Semaphore(0)


async def send(patch):
sent_patches.append(patch)
sempahore.release()
if len(sent_patches) == 5:
# if we didn't cancel the dispatcher would continue forever
raise asyncio.CancelledError()


async def recv():
await sempahore.acquire()
event = LayoutEvent(target=static_handler.target, data=[{}])

# We need this so we don't flood the render loop with events.
# In practice this is never an issue since events won't arrive
# as quickly as in this example.
await asyncio.sleep(0)

return event


async with SingleViewDispatcher(idom.Layout(ClickCount())) as dispatcher:
context = None # see note below
await dispatcher.run(send, recv, context)

await dispatch_single_view(idom.Layout(ClickCount()), send, recv)
assert len(sent_patches) == 5


.. note::

``context`` is information that's specific to the
:class:`~idom.core.dispatcher.AbstractDispatcher` implementation. In the case of
the :class:`~idom.core.dispatcher.SingleViewDispatcher` it doesn't require any
context. On the other hand the :class:`~idom.core.dispatcher.SharedViewDispatcher`
requires a client ID as its piece of contextual information.
The :func:`~idom.core.dispatcher.create_shared_view_dispatcher`, while more complex
in its usage, allows multiple clients to share one synchronized view.


Layout Server
Expand Down
69 changes: 41 additions & 28 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,11 @@
POSARGS_PATTERN = re.compile(r"^(\w+)\[(.+)\]$")


def get_posargs(name: str, session: Session) -> List[str]:
"""Find named positional arguments

Positional args of the form `name[arg1,arg2]` will be parsed as ['arg1', 'arg2'] if
the given `name` matches. Any args not matching that pattern will be added to the
list of args as well. Thus the following:

--param session_1[arg1,arg2] session_2[arg3,arg4]

where `name` is session_1 would produce ['--param', 'arg1', 'arg2']
"""
collected_args: List[str] = []
for arg in session.posargs:
match = POSARGS_PATTERN.match(arg)
if match is not None:
found_name, found_args = match.groups()
if name == found_name:
collected_args.extend(map(str.strip, found_args.split(",")))
else:
collected_args.append(arg)
return collected_args
@nox.session(reuse_venv=True)
def format(session: Session) -> None:
install_requirements_file(session, "check-style")
session.run("black", ".")
session.run("isort", ".")


@nox.session(reuse_venv=True)
Expand All @@ -54,7 +38,7 @@ def example(session: Session) -> None:
@nox.session(reuse_venv=True)
def docs(session: Session) -> None:
"""Build and display documentation in the browser (automatically reloads on change)"""
session.install("-r", "requirements/build-docs.txt")
install_requirements_file(session, "build-docs")
install_idom_dev(session, extras="all")
session.run(
"python",
Expand Down Expand Up @@ -103,7 +87,7 @@ def test(session: Session) -> None:
def test_python(session: Session) -> None:
"""Run the Python-based test suite"""
session.env["IDOM_DEBUG_MODE"] = "1"
session.install("-r", "requirements/test-env.txt")
install_requirements_file(session, "test-env")

pytest_args = get_posargs("pytest", session)
if "--no-cov" in pytest_args:
Expand All @@ -118,16 +102,16 @@ def test_python(session: Session) -> None:
@nox.session
def test_types(session: Session) -> None:
"""Perform a static type analysis of the codebase"""
session.install("-r", "requirements/check-types.txt")
session.install("-r", "requirements/pkg-deps.txt")
session.install("-r", "requirements/pkg-extras.txt")
install_requirements_file(session, "check-types")
install_requirements_file(session, "pkg-deps")
install_requirements_file(session, "pkg-extras")
session.run("mypy", "--strict", "src/idom")


@nox.session
def test_style(session: Session) -> None:
"""Check that style guidelines are being followed"""
session.install("-r", "requirements/check-style.txt")
install_requirements_file(session, "check-style")
session.run("flake8", "src/idom", "tests", "docs")
black_default_exclude = r"\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist"
session.run(
Expand All @@ -143,7 +127,7 @@ def test_style(session: Session) -> None:
@nox.session
def test_docs(session: Session) -> None:
"""Verify that the docs build and that doctests pass"""
session.install("-r", "requirements/build-docs.txt")
install_requirements_file(session, "build-docs")
install_idom_dev(session, extras="all")
session.run("sphinx-build", "-b", "html", "docs/source", "docs/build")
session.run("sphinx-build", "-b", "doctest", "docs/source", "docs/build")
Expand Down Expand Up @@ -181,6 +165,35 @@ def parse_commit_reference(commit_ref: str) -> Tuple[str, str, str]:
print(f"- {msg} - {sha_repr}")


def get_posargs(name: str, session: Session) -> List[str]:
"""Find named positional arguments

Positional args of the form `name[arg1,arg2]` will be parsed as ['arg1', 'arg2'] if
the given `name` matches. Any args not matching that pattern will be added to the
list of args as well. Thus the following:

--param session_1[arg1,arg2] session_2[arg3,arg4]

where `name` is session_1 would produce ['--param', 'arg1', 'arg2']
"""
collected_args: List[str] = []
for arg in session.posargs:
match = POSARGS_PATTERN.match(arg)
if match is not None:
found_name, found_args = match.groups()
if name == found_name:
collected_args.extend(map(str.strip, found_args.split(",")))
else:
collected_args.append(arg)
return collected_args


def install_requirements_file(session: Session, name: str) -> None:
file_path = HERE / "requirements" / (name + ".txt")
assert file_path.exists(), f"requirements file {file_path} does not exist"
session.install("-r", str(file_path))


def install_idom_dev(session: Session, extras: str = "stable") -> None:
session.install("-e", f".[{extras}]")
if "--no-restore" not in session.posargs:
Expand Down
1 change: 0 additions & 1 deletion requirements/pkg-deps.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
typing-extensions >=3.7.4
mypy-extensions >=0.4.3
anyio >=2.0
async_generator >=1.10; python_version<"3.7"
async_exit_stack >=1.0.1; python_version<"3.7"
jsonpatch >=1.26
typer >=0.3.2
Expand Down
23 changes: 0 additions & 23 deletions src/idom/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +0,0 @@
from .component import AbstractComponent, Component, ComponentConstructor, component
from .dispatcher import AbstractDispatcher, SharedViewDispatcher, SingleViewDispatcher
from .events import EventHandler, Events, event
from .layout import Layout
from .vdom import vdom


__all__ = [
"AbstractComponent",
"Layout",
"AbstractDispatcher",
"component",
"Component",
"EventHandler",
"ComponentConstructor",
"event",
"Events",
"hooks",
"Layout",
"vdom",
"SharedViewDispatcher",
"SingleViewDispatcher",
]
Loading