Skip to content
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
2 changes: 1 addition & 1 deletion docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ to allow users to enable this behavior early.
- minor doc updates - :commit:`e5511d9`
- add tests for callback identity preservation with keys - :commit:`72e03ec`
- add 'key' to VDOM spec - :commit:`c3236fe`
- Rename validate_serialized_vdom to validate_vdom - :commit:`d04faf9`
- Rename validate_serialized_vdom to validate_vdom_json - :commit:`d04faf9`
- EventHandler should not serialize itself - :commit:`f7a59f2`
- fix docs typos - :commit:`42b2e20`
- fixes: #331 - add roadmap to docs - :commit:`4226c12`
Expand Down
3 changes: 1 addition & 2 deletions src/idom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .core.component import Component, component
from .core.events import EventHandler, event
from .core.layout import Layout
from .core.vdom import VdomDict, vdom
from .core.vdom import vdom
from .server.prefab import run
from .utils import Ref, html_to_vdom
from .widgets import hotswap, multiview
Expand All @@ -28,6 +28,5 @@
"Ref",
"run",
"vdom",
"VdomDict",
"web",
]
3 changes: 1 addition & 2 deletions src/idom/core/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
from functools import wraps
from typing import Any, Callable, Dict, Optional, Tuple, Union

from .proto import ComponentType
from .vdom import VdomDict
from .proto import ComponentType, VdomDict


def component(
Expand Down
3 changes: 1 addition & 2 deletions src/idom/core/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@
from anyio import create_task_group
from jsonpatch import apply_patch, make_patch

from idom.core.vdom import VdomJson
from idom.utils import Ref

from .layout import LayoutEvent, LayoutUpdate
from .proto import LayoutType
from .proto import LayoutType, VdomJson


logger = getLogger(__name__)
Expand Down
6 changes: 3 additions & 3 deletions src/idom/core/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
from idom.utils import Ref

from .hooks import LifeCycleHook
from .proto import ComponentType, EventHandlerDict
from .vdom import VdomJson, validate_vdom
from .proto import ComponentType, EventHandlerDict, VdomJson
from .vdom import validate_vdom_json


logger = getLogger(__name__)
Expand Down Expand Up @@ -154,7 +154,7 @@ async def render(self) -> LayoutUpdate:
# Ensure that the model is valid VDOM on each render
root_id = self._root_life_cycle_state_id
root_model = self._model_states_by_life_cycle_state_id[root_id]
validate_vdom(root_model.model.current)
validate_vdom_json(root_model.model.current)
return result

def _create_layout_update(self, old_state: _ModelState) -> LayoutUpdate:
Expand Down
81 changes: 75 additions & 6 deletions src/idom/core/proto.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,20 @@

from types import TracebackType
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Iterable,
List,
Mapping,
Optional,
Sequence,
Type,
TypeVar,
Union,
)

from typing_extensions import Protocol, runtime_checkable


if TYPE_CHECKING: # pragma: no cover
from .vdom import VdomDict
from typing_extensions import Protocol, TypedDict, runtime_checkable


ComponentConstructor = Callable[..., "ComponentType"]
Expand Down Expand Up @@ -64,6 +62,77 @@ def __exit__(
"""Clean up the view after its final render"""


VdomAttributes = Mapping[str, Any]
"""Describes the attributes of a :class:`VdomDict`"""

VdomChild = Union[ComponentType, "VdomDict", str]
"""A single child element of a :class:`VdomDict`"""

VdomChildren = Sequence[VdomChild]
"""Describes a series of :class:`VdomChild` elements"""

VdomAttributesAndChildren = Union[
Mapping[str, Any], # this describes both VdomDict and VdomAttributes
Iterable[VdomChild],
]
"""Useful for the ``*attributes_and_children`` parameter in :func:`idom.core.vdom.vdom`"""


class _VdomDictOptional(TypedDict, total=False):
key: str
children: Sequence[
# recursive types are not allowed yet:
# https://github.com/python/mypy/issues/731
Union[ComponentType, Dict[str, Any], str]
]
attributes: VdomAttributes
eventHandlers: EventHandlerDict # noqa
importSource: ImportSourceDict # noqa


class _VdomDictRequired(TypedDict, total=True):
tagName: str # noqa


class VdomDict(_VdomDictRequired, _VdomDictOptional):
"""A :ref:`VDOM` dictionary"""


class ImportSourceDict(TypedDict):
source: str
fallback: Any
sourceType: str # noqa
unmountBeforeUpdate: bool # noqa


class _OptionalVdomJson(TypedDict, total=False):
key: str
error: str
children: List[Any]
attributes: Dict[str, Any]
eventHandlers: Dict[str, _JsonEventTarget] # noqa
importSource: _JsonImportSource # noqa


class _RequiredVdomJson(TypedDict, total=True):
tagName: str # noqa


class VdomJson(_RequiredVdomJson, _OptionalVdomJson):
"""A JSON serializable form of :class:`VdomDict` matching the :data:`VDOM_JSON_SCHEMA`"""


class _JsonEventTarget(TypedDict):
target: str
preventDefault: bool # noqa
stopPropagation: bool # noqa


class _JsonImportSource(TypedDict):
source: str
fallback: Any


EventHandlerMapping = Mapping[str, "EventHandlerType"]
"""A generic mapping between event names to their handlers"""

Expand Down
126 changes: 46 additions & 80 deletions src/idom/core/vdom.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,9 @@

import inspect
import logging
from typing import (
Any,
Dict,
Iterable,
List,
Mapping,
Optional,
Sequence,
Tuple,
Union,
cast,
)
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, cast

from fastjsonschema import compile as compile_json_schema
from mypy_extensions import TypedDict
from typing_extensions import Protocol

from idom.config import IDOM_DEBUG_MODE
Expand All @@ -30,7 +18,15 @@
merge_event_handlers,
to_event_handler_function,
)
from idom.core.proto import EventHandlerDict, EventHandlerMapping, EventHandlerType
from idom.core.proto import (
EventHandlerDict,
EventHandlerMapping,
EventHandlerType,
ImportSourceDict,
VdomAttributesAndChildren,
VdomDict,
VdomJson,
)


logger = logging.getLogger()
Expand Down Expand Up @@ -106,23 +102,41 @@
_COMPILED_VDOM_VALIDATOR = compile_json_schema(VDOM_JSON_SCHEMA)


def validate_vdom(value: Any) -> VdomJson:
def validate_vdom_json(value: Any) -> VdomJson:
"""Validate serialized VDOM - see :attr:`VDOM_JSON_SCHEMA` for more info"""
_COMPILED_VDOM_VALIDATOR(value)
return cast(VdomJson, value)


_AttributesAndChildrenArg = Union[Mapping[str, Any], str, Iterable[Any], Any]
_EventHandlersArg = Optional[EventHandlerMapping]
_ImportSourceArg = Optional["ImportSourceDict"]
def is_vdom(value: Any) -> bool:
"""Return whether a value is a :class:`VdomDict`

This employs a very simple heuristic - something is VDOM if:

1. It is a ``dict`` instance
2. It contains the key ``"tagName"``
3. The value of the key ``"tagName"`` is a string

.. note::

Performing an ``isinstance(value, VdomDict)`` check is too restrictive since the
user would be forced to import ``VdomDict`` every time they needed to declare a
VDOM element. Giving the user more flexibility, at the cost of this check's
accuracy, is worth it.
"""
return (
isinstance(value, dict)
and "tagName" in value
and isinstance(value["tagName"], str)
)


def vdom(
tag: str,
*attributes_and_children: _AttributesAndChildrenArg,
*attributes_and_children: VdomAttributesAndChildren,
key: str = "",
event_handlers: _EventHandlersArg = None,
import_source: _ImportSourceArg = None,
event_handlers: Optional[EventHandlerMapping] = None,
import_source: Optional[ImportSourceDict] = None,
) -> VdomDict:
"""A helper function for creating VDOM dictionaries.

Expand Down Expand Up @@ -169,28 +183,31 @@ def vdom(
return model


class VdomDictConstructor(Protocol):
class _VdomDictConstructor(Protocol):
def __call__(
self,
*args: _AttributesAndChildrenArg,
event_handlers: _EventHandlersArg = None,
import_source: _ImportSourceArg = None,
*attributes_and_children: VdomAttributesAndChildren,
key: str = ...,
event_handlers: Optional[EventHandlerMapping] = ...,
import_source: Optional[ImportSourceDict] = ...,
) -> VdomDict:
...


def make_vdom_constructor(tag: str, allow_children: bool = True) -> VdomDictConstructor:
def make_vdom_constructor(
tag: str, allow_children: bool = True
) -> _VdomDictConstructor:
"""Return a constructor for VDOM dictionaries with the given tag name.

The resulting callable will have the same interface as :func:`vdom` but without its
first ``tag`` argument.
"""

def constructor(
*attributes_and_children: _AttributesAndChildrenArg,
*attributes_and_children: VdomAttributesAndChildren,
key: str = "",
event_handlers: _EventHandlersArg = None,
import_source: _ImportSourceArg = None,
event_handlers: Optional[EventHandlerMapping] = None,
import_source: Optional[ImportSourceDict] = None,
) -> VdomDict:
model = vdom(
tag,
Expand Down Expand Up @@ -319,54 +336,3 @@ def _is_single_child(value: Any) -> bool:
logger.error(f"Key not specified for dynamic child {child}")

return False


class _VdomDictOptional(TypedDict, total=False):
key: str
children: Sequence[Any]
attributes: Dict[str, Any]
eventHandlers: EventHandlerDict # noqa
importSource: ImportSourceDict # noqa


class _VdomDictRequired(TypedDict, total=True):
tagName: str # noqa


class VdomDict(_VdomDictRequired, _VdomDictOptional):
"""A :ref:`VDOM` dictionary"""


class ImportSourceDict(TypedDict):
source: str
fallback: Any
sourceType: str # noqa
unmountBeforeUpdate: bool # noqa


class _OptionalVdomJson(TypedDict, total=False):
key: str
error: str
children: List[Any]
attributes: Dict[str, Any]
eventHandlers: Dict[str, _JsonEventTarget] # noqa
importSource: _JsonImportSource # noqa


class _RequiredVdomJson(TypedDict, total=True):
tagName: str # noqa


class VdomJson(_RequiredVdomJson, _OptionalVdomJson):
"""A JSON serializable form of :class:`VdomDict` matching the :data:`VDOM_JSON_SCHEMA`"""


class _JsonEventTarget(TypedDict):
target: str
preventDefault: bool # noqa
stopPropagation: bool # noqa


class _JsonImportSource(TypedDict):
source: str
fallback: Any
Loading