PK ! V idom/__init__.py__version__ = "0.1.0" from .bunch import StaticBunch, DynamicBunch from .element import element, Element from .layout import Layout from .helpers import Events, Var, node from .server import SimpleServer, SimpleWebServer, BaseServer from .display import display from . import nodes __all__ = [ "BaseServer", "display", "DynamicBunch", "element", "Element", "Events", "Layout", "node", "nodes", "SimpleServer", "SimpleWebServer", "State", "StaticBunch", "Var", ] PK ! j idom/bunch.pyfrom collections.abc import Mapping, MutableMapping from typing import Iterator, Any class StaticBunch(Mapping): """An immutable mapping with attribute access.""" def __init__(self, *args, **kwargs): self.__dict__.update(*args, **kwargs) def __len__(self) -> int: return len(self.__dict__) def __iter__(self) -> Iterator[str]: return iter(self.__dict__) def __getitem__(self, name: str) -> Any: return self.__dict__[name] def __repr__(self) -> str: return repr(self.__dict__) def __setattr__(self, name, value): raise TypeError("%r is immutable." % self) class DynamicBunch(MutableMapping, StaticBunch): """A mutable mapping with attribute access.""" def __setitem__(self, name: str, value: Any): self.__dict__[name] = value def __delitem__(self, name: str): del self.__dict__[name] def __setattr__(self, name, value): object.__setattr__(self, name, value) PK ! idom/display.pyimport os import uuid from typing import Any from .utils import STATIC def display(kind: str, *args: Any, **kwargs: Any) -> Any: wtype = {"jupyter": JupyterWigdet}[kind] return wtype(*args, **kwargs) class JupyterWigdet: _shown = False __slots__ = "_url" def __init__(self, url: str): self._url = url def _script(self): JS = os.path.join(STATIC, "jupyter-widget", "static", "js") for filename in os.listdir(JS): if os.path.splitext(filename)[1] == ".js": with open(os.path.join(JS, filename), "r") as f: return f.read() def _repr_html_(self) -> str: """Rich HTML display output.""" mount_id = uuid.uuid4().hex return f"""
{'' if JupyterWigdet._shown else ''} """ def __repr__(self): return "%s(%r)" % (type(self).__name__, self._url) PK ! 5@ @ idom/element.pyimport idom import inspect from functools import wraps from weakref import WeakValueDictionary from typing import Dict, Callable, Any, List, Optional, overload from .utils import to_coroutine, bound_id _ElementConstructor = Callable[..., "Element"] @overload def element(function: Callable) -> _ElementConstructor: ... @overload def element( *, state: Optional[str] = None ) -> Callable[[Callable], _ElementConstructor]: ... def element( function: Optional[Callable] = None, state: Optional[str] = None ) -> Callable: """A decorator for defining an :class:`Element`. Parameters: function: The function that will render a :term:`VDOM` model. """ def setup(func): @wraps(func) def constructor(*args: Any, **kwargs: Any) -> Element: element = Element(func, state) element.update(*args, **kwargs) return element return constructor if function is not None: return setup(function) else: return setup class Element: """An object for rending element models. Rendering element objects is typically done by a :class:`idom.layout.Layout` which will :meth:`Element.mount` itself to the element instance the first time it is rendered. From there an element instance will communicate its needs to the layout. For example when an element wants to re-render it will call :meth:`idom.layout.Layout.element_updated`. The lifecycle of an element typically goes in this order: 1. The element instance is instantiated. 2. The element's layout will mount itself. 3. The layout will call :meth:`Element.render`. 4. The element is dormant until an :meth:`Element.update` occurs. 5. Go back to step **3**. """ _by_id = WeakValueDictionary() # type: WeakValueDictionary[str, "Element"] __slots__ = ( "_dead", "_element_id", "_function", "_function_signature", "_layout", "_state", "_state_parameters", "_update", "__weakref__", ) @classmethod def by_id(self, element_id: str) -> "Element": """Get an element instance given its :attr:`Element.id`.""" return self._by_id[element_id] def __init__(self, function: Callable, state_parameters: Optional[str]): self._dead: bool = False self._element_id = bound_id(self) self._function = to_coroutine(function) self._function_signature = inspect.signature(function) self._layout: Optional["idom.Layout"] = None self._state: Dict[str, Any] = {} self._state_parameters: List[str] = list( map(str.strip, (state_parameters or "").split(",")) ) self._update: Optional[Dict[str, Any]] = None # save self to "by-ID" mapping Element._by_id[self._element_id] = self @property def id(self) -> str: """The unique ID of the element.""" return self._element_id def update(self, *args: Any, **kwargs: Any): """Schedule this element to render with new parameters.""" if self._update is None: # only tell layout to render on first update call self._update = {} if self._layout is not None: self._layout.update(self) bound = self._function_signature.bind_partial(None, *args, **kwargs) self._update.update(list(bound.arguments.items())[1:]) def animate(self, function: Callable): """Schedule this function to run soon, and then render any updates it caused.""" if self._layout is not None: # animating and updating an element is redundant. self._layout.animate(function) return function async def render(self) -> Dict[str, Any]: """Render the element's :term:`VDOM` model.""" # load update and reset for next render update = self._update if update is None: raise RuntimeError(f"{self} cannot render again - no update occured.") for name in self._state_parameters: if name not in update: if name in self._state: update[name] = self._state[name] else: self._state[name] = update[name] self._update = None return await self._function(self, **update) def mount(self, layout: "idom.Layout"): """Mount a layout to the element instance. Occurs just before rendering the element. """ if not self._dead: self._layout = layout def unmount(self): """Unmount a layout from the element instance. Occurs when a parent element has re-rendered and its old children are deleted. """ self._layout = None self._dead = True def __repr__(self) -> str: return "%s(%s)" % (self._function.__qualname__, self.id) PK ! # # idom/helpers.pyimport inspect from collections.abc import Mapping from typing import Any, Callable, Dict, TypeVar, Generic, List from .bunch import DynamicBunch from .utils import to_coroutine, Sentinel, bound_id EMPTY = Sentinel("EMPTY") def node(tag: str, *children, **attributes: Any) -> DynamicBunch: """A helper function for generating :term:`VDOM` dictionaries.""" merged_children: List[Any] = [] for c in children: if isinstance(c, (list, tuple)): merged_children.extend(c) else: merged_children.append(c) model = DynamicBunch(tagName=tag) if merged_children: model.children = merged_children if "eventHandlers" in attributes: model.eventHandlers = attributes.pop("eventHandlers") if attributes: model.attributes = attributes return model def node_constructor(tag, allow_children=True): """Create a constructor for nodes with the given tag name.""" def constructor(*children, **attributes): if not allow_children and children: raise TypeError(f"{tag!r} nodes cannot have children.") return node(tag, *children, **attributes) constructor.__name__ = tag qualname_prefix = constructor.__qualname__.rsplit(".", 1)[0] constructor.__qualname__ = qualname_prefix + f".{tag}" constructor.__doc__ = f"""Create a new ``<{tag}/>`` - returns :term:`VDOM`.""" return constructor class Events(Mapping): """A container for event handlers. Assign this object to the ``"eventHandlers"`` field of an element model. """ __slots__ = "_handlers" def __init__(self): self._handlers = {} def on(self, event: str, where: str = None) -> Callable: """A decorator for adding an event handler. Parameters: event: The camel-case name of the event, the word "on" is automatically prepended. So passing "keyDown" would refer to the event "onKeyDown". where: A string defining what event attribute a parameter refers to if the parameter name does not already refer to it directly. See the :class:`EventHandler` class for more info. Returns: A decorator which accepts an event handler function as its first argument. The parameters of the event handler function may indicate event attributes which should be sent back from the frontend. See :class:`EventHandler` for more info. Examples: Simple "onClick" event handler: .. code-block:: python def clickable_element(): my_button = Events() @events.on("click") def handler(): # do something on a click event ... return idom.node("button", "hello!", eventHandlers=events) Getting an ```` element's current value when it changes: .. code-block:: python def input_element(): events = Events() @events.on("keyDown", where="value=target.value") def handle_input_element_change(value) # do something with the input's new value ... return idon.node("input", "type here...", eventHandlers=events) """ event_name = "on" + event[:1].upper() + event[1:] def setup(function: Callable) -> Callable: self._handlers[event_name] = EventHandler(function, event_name, where) return function return setup def __len__(self): return len(self._handlers) def __iter__(self): return iter(self._handlers) def __getitem__(self, key): return self._handlers[key] def __repr__(self): return repr(self._handlers) class EventHandler: """An object which defines an event handler. Get a serialized reference to the handler via :meth:`EventHandler.serialize`. The event handler object acts like a coroutine even if the given function was not. Parameters: function: The event handler function. Its parameters may indicate event attributes which should be sent back from the fronend unless otherwise specified by the ``where`` parameter. event_name: The camel case name of the event. where: A string defining what event attribute a parameter refers to if the parameter name does not already refer to it directly. For example, accessing the current value of an ```` element might be done by specifying ``where="param=target.value"``. target_id: A unique identifier for the event handler. This is generally used if an element has more than on event handler for the same event type. If no ID is provided one will be generated automatically. """ __slots__ = ( "_function", "_handler", "_event_name", "_target_id", "_props_to_params", "__weakref__", ) def __init__( self, function: Callable, event_name: str, where: str = None, target_id: str = None, ): self._function = function self._handler = to_coroutine(function) self._target_id = target_id or bound_id(self) self._event_name = event_name self._props_to_params: Dict[str, str] = {} for target_key, param in inspect.signature(function).parameters.items(): if param.kind in ( inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD, ): raise TypeError( f"Event handler {function} has variable keyword or positional arguments." ) self._props_to_params.setdefault(target_key, target_key) if where is not None: for part in map(str.strip, where.split(",")): target_key, source_prop = tuple(map(str.strip, part.split("="))) self._props_to_params[source_prop] = target_key try: self._props_to_params.pop(target_key) except KeyError: raise TypeError( f"Event handler {function} has no parameter {target_key!r}." ) async def __call__(self, data): data = {self._props_to_params[k]: v for k, v in data.items()} return await self._handler(**data) def serialize(self): return f"{self._target_id}_{self._event_name}_{';'.join(self._props_to_params.keys())}" def __eq__(self, other): if isinstance(other, EventHandler): return ( self._function == other._function and self._id == other._id and self._event_name == other._event_name and self._props_to_params == other._props_to_params ) else: return other == self._function VarReference = TypeVar("VarReference", bound=Any) class Var(Generic[VarReference]): """A variable for holding a reference to an object. Variables are useful when multiple elements need to share data. This is usually discouraged, but can be useful in certain situations. For example, you might use a reference to keep track of a user's selection from a list of options: .. code-block:: python def option_picker(handler, option_names): selection = Var() options = [option(n, selection) for n in option_names] return idom.node("div", options, picker(handler, selection)) def option(name, selection): events = idom.Events() @events.on("click") def select(): # set the current selection to the option name selection.set(name) return idom.node("button", eventHandlers=events) def picker(handler, selection): events = idom.Events() @events.on("click") def handle(): # passes the current option name to the handler handler(selection.get()) return idom.node("button", "Use" eventHandlers=events) """ __slots__ = ("__current",) empty = Sentinel("Var.empty") def __init__(self, value: Any = empty): self.__current = value def set(self, new): old = self.__current self.__current = new return old def get(self) -> VarReference: return self.__current def __eq__(self, other: Any) -> bool: if isinstance(other, Var): return self.get() == other.get() else: return False def __repr__(self) -> str: return "Var(%r)" % self.get() PK ! Τjyw w idom/layout.pyimport asyncio from collections.abc import Mapping from typing import List, Dict, Tuple, Callable, Union, Any, Set, Optional, AsyncIterator from .element import Element from .helpers import EventHandler from .utils import to_coroutine try: import vdom except ImportError: vdom = None class RenderError(Exception): """An error occured while rendering element models.""" class Layout: """Renders the models generated by :class:`Element` objects.""" __slots__ = ("_render_event", "_update_queue", "_animate_queue", "_root", "_state") def __init__(self, root: "Element"): if not isinstance(root, Element): raise TypeError("Expected an Element, not %r" % root) self._state: Dict[str, Dict] = {} self._root = root self._update_queue: List[Element] = [] self._render_event = asyncio.Event() self._animate_queue: List[Callable] = [] self._create_element_state(root.id, None) self.update(root) @property def root(self) -> str: return self._root.id async def apply(self, target: str, handler: str, data: dict): model_state = self._state[target] event_handler = model_state["event_handlers"][handler] await event_handler(data) def animate(self, function: Callable): self._animate_queue.append(to_coroutine(function)) self._render_event.set() def update(self, element: "Element"): self._update_queue.append(element) self._render_event.set() async def render(self) -> Tuple[List[str], Dict[str, Dict], List[str]]: await self._render_event.wait() self._render_event.clear() # current element ids current: Set[str] = set(self._state) callbacks = self._animate_queue[:] self._animate_queue.clear() await asyncio.gather(*[cb() for cb in callbacks]) # root elements which updated roots: List[str] = [] # all element updates new: Dict[str, Dict] = {} updates = self._update_queue[:] self._update_queue.clear() for element in updates: parent = self._state[element.id]["parent"] async for element_id, model in self._render_element(element, parent): new[element_id] = model roots.append(element.id) # all deleted element ids old: List[str] = list(current.difference(self._state)) for element_id in old: Element.by_id(element_id).unmount() return roots, new, old async def _render_element( self, element: "Element", parent_element_id: str ) -> AsyncIterator[Tuple[str, Dict]]: try: element.mount(self) model = await element.render() if isinstance(model, Element): model = {"tagName": "div", "children": [model]} element_id = element.id if self._has_element_state(element_id): self._reset_element_state(element_id) else: self._create_element_state(element_id, parent_element_id) async for i, m in self._render_model(model, element_id): yield i, m except Exception as error: raise RenderError(f"Failed to render {element}") from error async def _render_model( self, model: Mapping, element_id: str ) -> AsyncIterator[Tuple[str, Dict]]: index = 0 to_visit: List[Union[Mapping, Element]] = [model] while index < len(to_visit): node = to_visit[index] if isinstance(node, Element): async for i, m in self._render_element(node, element_id): yield i, m elif isinstance(node, Mapping): if "children" in node: value = node["children"] if isinstance(value, (list, tuple)): to_visit.extend(value) elif isinstance(value, (Mapping, Element)): to_visit.append(value) elif vdom is not None and isinstance(node, vdom.VDOM): to_visit.append(_from_vdom(node)) index += 1 yield element_id, self._load_model(model, element_id) def _load_model(self, model: Mapping, element_id: str): model = dict(model) if "children" in model: model["children"] = self._load_model_children(model["children"], element_id) if "eventHandlers" in model: model["eventHandlers"] = self._load_event_handlers( model["eventHandlers"], element_id ) return model def _load_model_children( self, children: Union[List, Tuple], element_id: str ) -> List[Dict]: if not isinstance(children, (list, tuple)): children = [children] loaded_children = [] for child in children: if isinstance(child, Mapping): child = {"type": "obj", "data": self._load_model(child, element_id)} elif isinstance(child, Element): child = {"type": "ref", "data": child.id} else: child = {"type": "str", "data": str(child)} loaded_children.append(child) return loaded_children def _load_event_handlers( self, handlers: Dict[str, Callable], element_id: str ) -> Dict[str, str]: event_targets = {} for event, handler in handlers.items(): if not isinstance(handler, EventHandler): handler = EventHandler(handler, event) handler_specification = event_targets[element_id] = handler.serialize() self._state[element_id]["event_handlers"][handler_specification] = handler return event_targets def _has_element_state(self, element_id: str) -> bool: return element_id in self._state def _create_element_state(self, element_id: str, parent_element_id: Optional[str]): if parent_element_id is not None and self._has_element_state(parent_element_id): self._state[parent_element_id]["inner_elements"].add(element_id) self._state[element_id] = { "parent": parent_element_id, "inner_elements": set(), "event_handlers": {}, } def _reset_element_state(self, element_id: str): parent_element_id = self._state[element_id]["parent"] self._delete_element_state(element_id) self._create_element_state(element_id, parent_element_id) def _delete_element_state(self, element_id: str): old = self._state.pop(element_id) parent_element_id = old["parent"] if self._has_element_state(parent_element_id): self._state[parent_element_id]["inner_elements"].remove(element_id) for i in old["inner_elements"]: self._delete_element_state(i) def _from_vdom(node: Any): data = { "tagName": node.tag_name, "children": node.children, "attributes": node.attributes, } if node.style: data["attributes"]["style"] = node.style if node.event_handlers: data["eventHandlers"] = node.event_handlers if node.key: data["key"] = node.key return data PK ! 0 idom/nodes.pyfrom .helpers import node_constructor __all__ = [ # Content sectioning "style", "address", "article", "aside", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "nav", "section", # Text content "blockquote", "blockquote", "dd", "div", "dl", "dt", "figcaption", "figure", "hr", "li", "ol", "p", "pre", "ul", # Inline text semantics "a", "abbr", "b", "br", "cite", "code", "data", "em", "i", "kbd", "mark", "q", "s", "samp", "small", "span", "strong", "sub", "sup", "time", "u", "var", # Image and video "img", "audio", "video", "source", # Table content "caption", "col", "colgroup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", # Forms (only read only aspects) "meter", "output", "progress", "input", "button", "label", # Interactive elements "details", "dialog", "menu", "menuitem", "summary", ] # Content sectioning style = node_constructor("style") address = node_constructor("address") article = node_constructor("article") aside = node_constructor("aside") footer = node_constructor("footer") h1 = node_constructor("h1") h2 = node_constructor("h2") h3 = node_constructor("h3") h4 = node_constructor("h4") h5 = node_constructor("h5") h6 = node_constructor("h6") header = node_constructor("header") hgroup = node_constructor("hgroup") nav = node_constructor("nav") section = node_constructor("section") # Text content blockquote = node_constructor("blockquote") dd = node_constructor("dd") div = node_constructor("div") dl = node_constructor("dl") dt = node_constructor("dt") figcaption = node_constructor("figcaption") figure = node_constructor("figure") hr = node_constructor("hr", allow_children=False) li = node_constructor("li") ol = node_constructor("ol") p = node_constructor("p") pre = node_constructor("pre") ul = node_constructor("ul") # Inline text semantics a = node_constructor("a") abbr = node_constructor("abbr") b = node_constructor("b") br = node_constructor("br", allow_children=False) cite = node_constructor("cite") code = node_constructor("code") data = node_constructor("data") em = node_constructor("em") i = node_constructor("i") kbd = node_constructor("kbd") mark = node_constructor("mark") q = node_constructor("q") s = node_constructor("s") samp = node_constructor("samp") small = node_constructor("small") span = node_constructor("span") strong = node_constructor("strong") sub = node_constructor("sub") sup = node_constructor("sup") time = node_constructor("time") u = node_constructor("u") var = node_constructor("var") # Image and video img = node_constructor("img", allow_children=False) audio = node_constructor("audio") video = node_constructor("video") source = node_constructor("source", allow_children=False) # Table content caption = node_constructor("caption") col = node_constructor("col") colgroup = node_constructor("colgroup") table = node_constructor("table") tbody = node_constructor("tbody") td = node_constructor("td") tfoot = node_constructor("tfoot") th = node_constructor("th") thead = node_constructor("thead") tr = node_constructor("tr") # Forms (only read only aspects) meter = node_constructor("meter") output = node_constructor("output") progress = node_constructor("progress") input = node_constructor("input", allow_children=False) button = node_constructor("button") label = node_constructor("label") # Interactive elements details = node_constructor("details") dialog = node_constructor("dialog") menu = node_constructor("menu") menuitem = node_constructor("menuitem") summary = node_constructor("summary") PK ! +) idom/py.typed# Marker file for PEP 561 PK ! *m idom/server/__init__.pyfrom .base import BaseServer from .simple import SimpleServer, SimpleWebServer __all__ = ["BaseServer", "SimpleServer", "SimpleWebServer"] PK !