From 1854808a801129072404fa5b3dc80780a0e41d61 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Sat, 7 Sep 2024 22:44:01 +0300 Subject: [PATCH 01/23] Add support for Python 3.9 --- .github/workflows/build-node-packages.yml | 2 +- .github/workflows/create-release.yaml | 3 +++ .github/workflows/python-test.yml | 6 ++--- node/build.js | 1 - node/publish.js | 11 +++++++++ pyproject.toml | 2 +- seamless/components/page.py | 28 +++++++++++------------ seamless/components/router/router.py | 8 +++---- seamless/context/context.py | 7 +++--- seamless/core/javascript.py | 6 ++--- seamless/extra/components/__init__.py | 6 ++--- seamless/extra/components/repository.py | 6 ++--- seamless/extra/events/database.py | 8 +++---- seamless/extra/transports/dispatcher.py | 4 ++-- seamless/internal/cookies.py | 7 +++--- seamless/internal/injector.py | 4 +++- seamless/styling/style.py | 6 +++-- seamless/types/events.py | 4 ++-- seamless/types/html/html_element.py | 2 +- seamless/types/html/html_event_props.py | 27 ++++++++++++++++------ seamless/types/styling/css_properties.py | 5 ++-- 21 files changed, 92 insertions(+), 61 deletions(-) create mode 100644 node/publish.js diff --git a/.github/workflows/build-node-packages.yml b/.github/workflows/build-node-packages.yml index 7db52b6..16eb78f 100644 --- a/.github/workflows/build-node-packages.yml +++ b/.github/workflows/build-node-packages.yml @@ -29,7 +29,7 @@ jobs: - name: Publish node packages run: | - node build.js ${{ github.event.release.tag_name }} + node publish.js ${{ github.event.release.tag_name }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/create-release.yaml b/.github/workflows/create-release.yaml index 2bce84f..5029a9b 100644 --- a/.github/workflows/create-release.yaml +++ b/.github/workflows/create-release.yaml @@ -9,9 +9,11 @@ on: permissions: contents: write + pull-requests: write env: VERSION: ${{ github.event.inputs.version }} + GH_TOKEN: ${{ github.token }} jobs: deploy: @@ -45,6 +47,7 @@ jobs: run: | git config --local user.email "${{ github.actor }}@users.noreply.github.com" git config --local user.name "${{ github.actor }}" + git checkout -b release/$VERSION git add . git commit -m "Update version to $VERSION" git push origin release/$VERSION diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 8abba0a..51520c7 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -19,14 +19,14 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ matrix.python-version }} - name: Install dependencies run: | @@ -38,7 +38,7 @@ jobs: working-directory: ${{ github.workspace }} run: | python -m unittest discover -s tests -p 'test_*.py' -t ${{ github.workspace }} - + - name: Run build test working-directory: ${{ github.workspace }} run: | diff --git a/node/build.js b/node/build.js index 94fdc16..f07a1cb 100644 --- a/node/build.js +++ b/node/build.js @@ -17,5 +17,4 @@ packages.forEach((pkg) => { const pkgJson = require(pkgJsonPath); pkgJson.version = nextVersion; fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2)); - execSync(`cd ${pkgPath} && npm run build && npm publish --access public`); }); diff --git a/node/publish.js b/node/publish.js new file mode 100644 index 0000000..9a74c49 --- /dev/null +++ b/node/publish.js @@ -0,0 +1,11 @@ +import { execSync } from 'child_process'; + +const packagesDir = path.join(__dirname, 'packages'); +const packages = fs.readdirSync(packagesDir); +packages.forEach((pkg) => { + const pkgPath = path.join(packagesDir, pkg); + if (!fs.lstatSync(pkgPath).isDirectory()) { + return; + } + execSync(`cd ${pkgPath} && npm run build && npm publish --access public`); +}); diff --git a/pyproject.toml b/pyproject.toml index 3db2ea9..7dec424 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "python-seamless" authors = [{ name = "Xpo Development", email = "dev@xpo.dev" }] description = "A Python package for creating and manipulating HTML components. It is working similar to React.js, but in Python" readme = "README.md" -requires-python = ">=3.11" +requires-python = ">=3.9" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", diff --git a/seamless/components/page.py b/seamless/components/page.py index c59c62b..7b11ecd 100644 --- a/seamless/components/page.py +++ b/seamless/components/page.py @@ -1,4 +1,4 @@ -from typing import overload, Iterable +from typing import Optional, overload, Iterable from pydom import Component from pydom.utils.functions import to_iter @@ -21,29 +21,29 @@ class Page(Component): def __init__( self, *children: ChildType, - title: str | None = None, - html_props: HTMLHtmlElement | None = None, - head_props: HTMLHeadElement | None = None, - body_props: HTMLBodyElement | None = None, + title: Optional[str] = None, + html_props: Optional[HTMLHtmlElement] = None, + head_props: Optional[HTMLHeadElement] = None, + body_props: Optional[HTMLBodyElement] = None, ): ... @overload def __init__( self, *, children: ChildrenType, - title: str | None = None, - html_props: HTMLHtmlElement | None = None, - head_props: HTMLHeadElement | None = None, - body_props: HTMLBodyElement | None = None, + title: Optional[str] = None, + html_props: Optional[HTMLHtmlElement] = None, + head_props: Optional[HTMLHeadElement] = None, + body_props: Optional[HTMLBodyElement] = None, ): ... def __init__( # type: ignore self, *, - title: str | None = None, - html_props: HTMLHtmlElement | None = None, - head_props: HTMLHeadElement | None = None, - body_props: HTMLBodyElement | None = None, + title: Optional[str] = None, + html_props: Optional[HTMLHtmlElement] = None, + head_props: Optional[HTMLHeadElement] = None, + body_props: Optional[HTMLBodyElement] = None, ): self.title = title self._html_props = html_props or {"lang": "en"} @@ -75,7 +75,7 @@ def render(self): ), ) - def __init_subclass__(cls, title: str | None = None, **kwargs) -> None: + def __init_subclass__(cls, title: Optional[str] = None, **kwargs) -> None: super().__init_subclass__(**kwargs) if title is None: diff --git a/seamless/components/router/router.py b/seamless/components/router/router.py index f5b5dea..a79360f 100644 --- a/seamless/components/router/router.py +++ b/seamless/components/router/router.py @@ -1,6 +1,6 @@ from json import dumps from pathlib import Path -from typing import Optional, Tuple, overload, Type +from typing import Optional, Tuple, overload from pydom import Component @@ -16,13 +16,13 @@ class Router(Component): children: Tuple[Route, ...] # type: ignore @overload - def __init__(self, *, loading_component: Optional[Type[Component]] = None): ... + def __init__(self, *, loading_component: Optional[type[Component]] = None): ... @overload def __init__( - self, *routes: Route, loading_component: Optional[Type[Component]] = None + self, *routes: Route, loading_component: Optional[type[Component]] = None ): ... - def __init__(self, *, loading_component: Optional[Type[Component]] = None): # type: ignore + def __init__(self, *, loading_component: Optional[type[Component]] = None): # type: ignore self.loading_component = ( component_name(loading_component) if loading_component else None ) diff --git a/seamless/context/context.py b/seamless/context/context.py index 33170ee..034ceea 100644 --- a/seamless/context/context.py +++ b/seamless/context/context.py @@ -2,13 +2,14 @@ from typing import ( Any, Callable, - Concatenate, Optional, - ParamSpec, TypeVar, + Union, cast, ) +from typing_extensions import Concatenate, ParamSpec + from pydom.context.context import ( Context as _Context, get_context as _get_context, @@ -24,7 +25,7 @@ P = ParamSpec("P") Feature = Callable[Concatenate["Context", P], Any] -PropertyMatcher = Callable[Concatenate[str, Any, P], bool] | str +PropertyMatcher = Union[Callable[Concatenate[str, Any, P], bool], str] PropertyTransformer = Callable[Concatenate[str, Any, "ContextNode", P], None] PostRenderTransformer = Callable[Concatenate["ContextNode", P], None] diff --git a/seamless/core/javascript.py b/seamless/core/javascript.py index ee80bbd..a887f29 100644 --- a/seamless/core/javascript.py +++ b/seamless/core/javascript.py @@ -1,12 +1,12 @@ from os import PathLike -from typing import overload +from typing import Union, overload class JavaScript: @overload def __init__(self, code: str, *, async_: bool = False) -> None: ... @overload - def __init__(self, *, file: str | PathLike, async_: bool = False) -> None: ... + def __init__(self, *, file: Union[str, PathLike], async_: bool = False) -> None: ... def __init__(self, code=None, *, file=None, async_: bool = False) -> None: if file: @@ -19,7 +19,7 @@ def __init__(self, code=None, *, file=None, async_: bool = False) -> None: self.code = code self.async_ = async_ - def __add__(self, other: "JavaScript | str") -> "JavaScript": + def __add__(self, other: Union["JavaScript", str]) -> "JavaScript": if isinstance(other, JavaScript): return JavaScript(self.code + other.code, async_=self.async_ or other.async_) elif isinstance(other, str): diff --git a/seamless/extra/components/__init__.py b/seamless/extra/components/__init__.py index fc4914f..5694de5 100644 --- a/seamless/extra/components/__init__.py +++ b/seamless/extra/components/__init__.py @@ -1,5 +1,5 @@ import inspect -from typing import TYPE_CHECKING, ClassVar, Optional, Type +from typing import TYPE_CHECKING, ClassVar, Optional from pydom import Component from pydom.context import Context @@ -25,7 +25,7 @@ def __init__(self, context: Context) -> None: @classmethod def __init_subclass__( - cls: Type["_Component"], + cls: type["_Component"], *, name: Optional[str] = None, inject_render: bool = False, @@ -71,5 +71,5 @@ async def get_component(self, client_id: str, component_name: str, props=None, * ) -def component_name(component: Type[Component]) -> Optional[str]: +def component_name(component: type[Component]) -> Optional[str]: return getattr(component, "__seamless_name__", None) diff --git a/seamless/extra/components/repository.py b/seamless/extra/components/repository.py index e2d415e..594538d 100644 --- a/seamless/extra/components/repository.py +++ b/seamless/extra/components/repository.py @@ -1,5 +1,3 @@ -from typing import Type - from pydom import Component @@ -7,8 +5,8 @@ class ComponentsRepository: def __init__(self): self.components = {} - def add_component(self, component: Type[Component], name: str): + def add_component(self, component: type[Component], name: str): self.components[name] = component - def get_component(self, component_name: str) -> Type[Component]: + def get_component(self, component_name: str) -> type[Component]: return self.components[component_name] diff --git a/seamless/extra/events/database.py b/seamless/extra/events/database.py index 65a7126..fe2db87 100644 --- a/seamless/extra/events/database.py +++ b/seamless/extra/events/database.py @@ -1,5 +1,5 @@ from inspect import iscoroutinefunction -from typing import Any, Callable, Dict +from typing import Any, Callable, Union from ...internal.utils import is_global @@ -24,9 +24,9 @@ async def __call__(self, *args: Any, **kwargs: Any) -> Any: class EventsDatabase: def __init__(self): - self.events: Dict[str, Action] = {} - self.scoped_events: Dict[str, Dict[str, Action]] = {} - self.actions_ids = dict[str | Callable, Action]() + self.events: dict[str, Action] = {} + self.scoped_events: dict[str, dict[str, Action]] = {} + self.actions_ids: dict[Union[str, Callable], Action] = {} def add_event(self, action: Action, *, scope: str): try: diff --git a/seamless/extra/transports/dispatcher.py b/seamless/extra/transports/dispatcher.py index 515851f..dc6a80f 100644 --- a/seamless/extra/transports/dispatcher.py +++ b/seamless/extra/transports/dispatcher.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Dict, Generic, TYPE_CHECKING, TypeVar, Awaitable +from typing import Any, Callable, Generic, TYPE_CHECKING, TypeVar, Awaitable from pydom import Context from typing_extensions import ParamSpec @@ -14,7 +14,7 @@ class Dispatcher(Generic[P, T]): def __init__(self, context: Context) -> None: self._context = context - self._handlers: Dict[Any, Callable] = {} + self._handlers: dict[Any, Callable] = {} def on(self, name: Any, handler: Callable[P, Awaitable[T]]): if name in self._handlers: diff --git a/seamless/internal/cookies.py b/seamless/internal/cookies.py index 1f5524c..c16d7a4 100644 --- a/seamless/internal/cookies.py +++ b/seamless/internal/cookies.py @@ -1,10 +1,11 @@ # type: ignore -from typing import Iterable +from typing import Iterable, Union + class Cookies: def __init__(self, cookie_string: str): - self.cookies = dict[str, str]() + self.cookies: dict[str, str] = {} self._parse(cookie_string) def _parse(self, cookie_string: str): @@ -22,7 +23,7 @@ def __contains__(self, key: str): return key in self.cookies @staticmethod - def from_request_headers(headers: Iterable[Iterable[bytes]] | dict): + def from_request_headers(headers: Union[Iterable[Iterable[bytes]], dict]): cookie_string = "" if isinstance(headers, dict): return Cookies(headers.get("cookie", "")) diff --git a/seamless/internal/injector.py b/seamless/internal/injector.py index 197cbfa..5b428a4 100644 --- a/seamless/internal/injector.py +++ b/seamless/internal/injector.py @@ -1,6 +1,8 @@ from inspect import iscoroutinefunction from functools import wraps -from typing import TypeVar, Callable, TypeAlias +from typing import TypeVar, Callable + +from typing_extensions import TypeAlias from pydom.utils.injector import Injector as _Injector diff --git a/seamless/styling/style.py b/seamless/styling/style.py index 0eb88e9..7be50a4 100644 --- a/seamless/styling/style.py +++ b/seamless/styling/style.py @@ -1,4 +1,6 @@ -from typing import Generic, TypeVar, Unpack, TYPE_CHECKING +from typing import Generic, TypeVar, Union, TYPE_CHECKING + +from typing_extensions import Unpack if TYPE_CHECKING: from ..types.styling.css_properties import CSSProperties @@ -17,7 +19,7 @@ def __call__(self, value: T): return self.instance def __init__( - self, *styles: "StyleObject | CSSProperties", **kwargs: Unpack["CSSProperties"] + self, *styles: Union["StyleObject", "CSSProperties"], **kwargs: Unpack["CSSProperties"] ): self.style: dict[str, object] = {} for style in styles: diff --git a/seamless/types/events.py b/seamless/types/events.py index a903af1..4357034 100644 --- a/seamless/types/events.py +++ b/seamless/types/events.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Generic, TypeVar +from typing import Generic, Optional, TypeVar try: from pydantic import BaseModel # type: ignore @@ -112,7 +112,7 @@ class PointerEvent(Event): ... class PopStateEvent(Event): - state: dict | None + state: Optional[dict] class ProgressEvent(Event): ... diff --git a/seamless/types/html/html_element.py b/seamless/types/html/html_element.py index e76dc21..825dd51 100644 --- a/seamless/types/html/html_element.py +++ b/seamless/types/html/html_element.py @@ -10,7 +10,7 @@ class HTMLElement(TypedDict, total=False, closed=False): access_key: str auto_capitalize: str - class_name: str | Iterable[str] + class_name: Union[str, Iterable[str]] content_editable: str # data: dict[str, str] # add this if needed in the future dir: Literal["ltr", "rtl", "auto"] diff --git a/seamless/types/html/html_event_props.py b/seamless/types/html/html_event_props.py index 1f44adc..4c6f2ea 100644 --- a/seamless/types/html/html_event_props.py +++ b/seamless/types/html/html_event_props.py @@ -1,9 +1,7 @@ -from typing import TYPE_CHECKING, Callable, Concatenate, TypeVar, Union +import sys +from typing import TYPE_CHECKING, Callable, TypeVar, Union -from typing_extensions import TypedDict - -if TYPE_CHECKING: - from seamless.core.javascript import JS +from typing_extensions import ParamSpec, TypedDict, Concatenate from seamless.types.events import ( CloseEvent, @@ -19,8 +17,23 @@ WheelEvent, ) -EventProps = TypeVar("EventProps", bound=Event) -EventFunction = Union[Callable[Concatenate[EventProps, ...], None], "JS", str] +P = ParamSpec("P") +EventProps = TypeVar("EventProps", bound=Event, contravariant=True) + +if TYPE_CHECKING: + from seamless.core.javascript import JS + + if sys.version_info >= (3, 11): + EventFunction = Union[Callable[Concatenate[EventProps, ...], None], "JS", str] + else: + from typing import Protocol + + class EventCallable(Protocol[EventProps]): + def __call__(self, _: EventProps, /, **kwargs) -> None: ... + + EventFunction = Union[EventCallable[EventProps], JS, str] +else: + EventFunction = Union[Callable[[EventProps], None], "JS", str] class HTMLEventProps(TypedDict, total=False): diff --git a/seamless/types/styling/css_properties.py b/seamless/types/styling/css_properties.py index 10e7018..3d2df7c 100644 --- a/seamless/types/styling/css_properties.py +++ b/seamless/types/styling/css_properties.py @@ -1,5 +1,6 @@ -from typing import Any, Generic, Literal, TypeVar, Union, TypeAlias -from typing_extensions import TypedDict +from typing import Any, Generic, Literal, TypeVar, Union + +from typing_extensions import TypedDict, TypeAlias float_ = float T = TypeVar("T") From a23586cbd3f0c18434bd18ec9b74d2f91545a8cf Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Sun, 8 Sep 2024 01:56:45 +0300 Subject: [PATCH 02/23] Make scripts ES6 compatible --- seamless/components/router/router.js | 12 +++--- .../transports/socketio/socketio.init.js | 43 ++++++++++--------- .../extra/transports/socketio/transport.py | 2 +- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/seamless/components/router/router.js b/seamless/components/router/router.js index 618458c..ead4a23 100644 --- a/seamless/components/router/router.js +++ b/seamless/components/router/router.js @@ -82,10 +82,12 @@ const clearParent = () => { } }; -const loadComponent = async (name, props = {}) => { - return seamless.instance.toDOMElement( - await seamless.getComponent(name, props) - ); +const loadComponent = (name, props = {}) => { + return new Promise((resolve) => { + seamless.getComponent(name, props).then((component) => { + resolve(seamless.instance.toDOMElement(component)); + }); + }); }; window.addEventListener("pageLocationChange", () => { @@ -135,7 +137,7 @@ seamless.navigateTo = function (to) { return false; }; -window.addEventListener("transportsAvailable", async (event) => { +window.addEventListener("transportsInitialized", () => { if (loadingComponentName) { seamless.getComponent(loadingComponentName, {}).then((component) => { loadingComponent = seamless.instance.toDOMElement(component); diff --git a/seamless/extra/transports/socketio/socketio.init.js b/seamless/extra/transports/socketio/socketio.init.js index f86a2d7..4745090 100644 --- a/seamless/extra/transports/socketio/socketio.init.js +++ b/seamless/extra/transports/socketio/socketio.init.js @@ -1,26 +1,27 @@ -await import("https://cdn.socket.io/4.7.5/socket.io.js"); -const socket = io(socketIOConfig); +import("https://cdn.socket.io/4.7.5/socket.io.js").then(() => { + const socket = io(socketIOConfig); -seamless.emit = (event, ...data) => { - socket.emit(event, ...data); -}; + seamless.emit = (event, ...data) => { + socket.emit(event, ...data); + }; -seamless.registerEventListener = (event, callback) => { - socket.on(event, callback); -}; + seamless.registerEventListener = (event, callback) => { + socket.on(event, callback); + }; -seamless.sendWaitResponse = (event, ...args) => { - return new Promise((resolve) => { - socket.emit(event, ...args, resolve); - }); -}; + seamless.sendWaitResponse = (event, ...args) => { + return new Promise((resolve) => { + socket.emit(event, ...args, resolve); + }); + }; -seamless.getComponent = async (name, props = {}) => { - return await seamless.sendWaitResponse("component", name, props); -}; + seamless.getComponent = (name, props = {}) => { + return seamless.sendWaitResponse("component", name, props); + }; -window.dispatchEvent( - new CustomEvent("transportsAvailable", { - detail: { clientId: socketIOConfig.query.client_id }, - }) -); + window.dispatchEvent( + new CustomEvent("transportsInitialized", { + detail: { clientId: socketIOConfig.query.client_id }, + }) + ); +}); diff --git a/seamless/extra/transports/socketio/transport.py b/seamless/extra/transports/socketio/transport.py index 7af9e79..3aea5ca 100644 --- a/seamless/extra/transports/socketio/transport.py +++ b/seamless/extra/transports/socketio/transport.py @@ -51,7 +51,7 @@ async def _client_id(self, sid): @staticmethod def init(config=None): - init_js = JS(file=HERE / "socketio.init.js", async_=True) + init_js = JS(file=HERE / "socketio.init.js") class InitSocketIO(Component, inject_render=True): def render(self, render_state: RenderState): From bf50c58f54bbaaff1f19f5429020d4675178854f Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Sun, 8 Sep 2024 01:57:03 +0300 Subject: [PATCH 03/23] Optimize reading of router.js --- seamless/components/router/router.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/seamless/components/router/router.py b/seamless/components/router/router.py index a79360f..6cc20fd 100644 --- a/seamless/components/router/router.py +++ b/seamless/components/router/router.py @@ -10,6 +10,7 @@ HERE = Path(__file__).parent +ROUTER_JS = JS(file=HERE / "router.js") class Router(Component): @@ -36,10 +37,7 @@ def render(self): for route in self.children ] - with open(HERE / "router.js", "r") as f: - router_js = f.read() - return Empty( - init=JS(f"let routes = {dumps(routes)};{router_js}"), + init=JS(f"let routes = {dumps(routes)};") + ROUTER_JS, loading=self.loading_component, ) From 575c70f1a77fe36904608c01290e1fd9daab3e36 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Sun, 8 Sep 2024 02:06:39 +0300 Subject: [PATCH 04/23] Remove async support from JS class --- node/packages/core/src/constants.ts | 3 +-- node/packages/core/src/index.ts | 15 ++------------- seamless/core/javascript.py | 11 +++++------ 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/node/packages/core/src/constants.ts b/node/packages/core/src/constants.ts index fe0c1d9..892acc8 100644 --- a/node/packages/core/src/constants.ts +++ b/node/packages/core/src/constants.ts @@ -1,4 +1,3 @@ export const SEAMLESS_ELEMENT = "seamless:element"; export const SEAMLESS_EMPTY = "seamless:empty"; -export const SEAMLESS_INIT = "seamless:init"; -export const SEAMLESS_INIT_ASYNC = "seamless:async"; \ No newline at end of file +export const SEAMLESS_INIT = "seamless:init"; \ No newline at end of file diff --git a/node/packages/core/src/index.ts b/node/packages/core/src/index.ts index 99dc73b..a8bd205 100644 --- a/node/packages/core/src/index.ts +++ b/node/packages/core/src/index.ts @@ -6,14 +6,7 @@ import type { } from "./types"; export { SeamlessOptions }; -import { - SEAMLESS_ELEMENT, - SEAMLESS_INIT, - SEAMLESS_EMPTY, - SEAMLESS_INIT_ASYNC, -} from "./constants"; - -const AsyncFunction = new Function("return (async function () {}).constructor")(); +import { SEAMLESS_ELEMENT, SEAMLESS_INIT, SEAMLESS_EMPTY } from "./constants"; class Seamless { private readonly eventObjectTransformer: ( @@ -94,11 +87,7 @@ class Seamless { protected attachInit(element: HTMLElement) { const initCode = element.getAttribute(SEAMLESS_INIT); if (initCode) { - new (element.hasAttribute(SEAMLESS_INIT_ASYNC) - ? AsyncFunction as { new (): Function } - : Function)("seamless", initCode).apply(element, [this.context]); - - element.removeAttribute(SEAMLESS_INIT_ASYNC); + new Function("seamless", initCode).apply(element, [this.context]); element.removeAttribute(SEAMLESS_INIT); } } diff --git a/seamless/core/javascript.py b/seamless/core/javascript.py index a887f29..6830366 100644 --- a/seamless/core/javascript.py +++ b/seamless/core/javascript.py @@ -4,11 +4,11 @@ class JavaScript: @overload - def __init__(self, code: str, *, async_: bool = False) -> None: ... + def __init__(self, code: str) -> None: ... @overload - def __init__(self, *, file: Union[str, PathLike], async_: bool = False) -> None: ... + def __init__(self, *, file: Union[str, PathLike]) -> None: ... - def __init__(self, code=None, *, file=None, async_: bool = False) -> None: + def __init__(self, code=None, *, file=None) -> None: if file: if code: raise ValueError("Cannot specify both code and file") @@ -17,13 +17,12 @@ def __init__(self, code=None, *, file=None, async_: bool = False) -> None: elif not code: raise ValueError("Must specify either code or file") self.code = code - self.async_ = async_ def __add__(self, other: Union["JavaScript", str]) -> "JavaScript": if isinstance(other, JavaScript): - return JavaScript(self.code + other.code, async_=self.async_ or other.async_) + return JavaScript(self.code + other.code) elif isinstance(other, str): - return JavaScript(self.code + other, async_=self.async_) + return JavaScript(self.code + other) else: raise TypeError( f"Cannot concatenate JavaScript with {type(other).__name__}" From eb91a255dd57c2d4f208010516de2d85062adf71 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Sun, 8 Sep 2024 02:06:50 +0300 Subject: [PATCH 05/23] Fix node scripts --- node/packages/core/src/init.ts | 2 +- node/packages/core/src/types.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/node/packages/core/src/init.ts b/node/packages/core/src/init.ts index 2681d14..191da0b 100644 --- a/node/packages/core/src/init.ts +++ b/node/packages/core/src/init.ts @@ -1,3 +1,3 @@ -import Seamless from "."; +import Seamless from "./index"; new Seamless(); \ No newline at end of file diff --git a/node/packages/core/src/types.ts b/node/packages/core/src/types.ts index ee020d6..30aba9f 100644 --- a/node/packages/core/src/types.ts +++ b/node/packages/core/src/types.ts @@ -1,7 +1,4 @@ -import type { SocketOptions, ManagerOptions } from "socket.io-client"; - export interface SeamlessOptions { - socketOptions?: Partial; /** * A function that serializes the event object before sending it to the server * The default behavior is to return the event object as is @@ -24,4 +21,4 @@ export interface SeamlessElement { type: string; props: Record; children: Array | null; -} \ No newline at end of file +} From aec967512300ef38867ca4fe08fcd355827cb2e3 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Sun, 8 Sep 2024 02:07:00 +0300 Subject: [PATCH 06/23] Add middlewares module --- seamless/middlewares/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 seamless/middlewares/__init__.py diff --git a/seamless/middlewares/__init__.py b/seamless/middlewares/__init__.py new file mode 100644 index 0000000..edcf111 --- /dev/null +++ b/seamless/middlewares/__init__.py @@ -0,0 +1,5 @@ +from ..extra.transports.socketio.middleware import SocketIOMiddleware + +__all__ = [ + "SocketIOMiddleware", +] From 8c79bb1b805a91c470a002b67f7d06864851be8f Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Sun, 8 Sep 2024 02:07:12 +0300 Subject: [PATCH 07/23] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 60213b2..5a01d04 100644 --- a/README.md +++ b/README.md @@ -74,20 +74,20 @@ class Person(Component): ``` To call a function on the server include this script in your file ```html - + ``` Import the middleware and mount it to your app ```python from fastapi import FastAPI -from seamless.middlewares import ASGIMiddleware as SeamlessMiddleware +from seamless.middlewares import SocketIOMiddleware app = FastAPI() -app.add_middleware(SeamlessMiddleware) +app.add_middleware(SocketIOMiddleware) ``` You can pass the following config to the middleware to change the socket path of all seamless endpoints. ```python app.add_middleware( - SeamlessMiddleware, + SocketIOMiddleware, socket_path="/my/custom/path" ) ``` From f8e29f1b5b68f6366354f7be1c872380b6febc63 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Sun, 8 Sep 2024 02:09:41 +0300 Subject: [PATCH 08/23] Remove async from js transformer --- seamless/extra/transformers/js_transformer.py | 5 +---- seamless/internal/constants.py | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/seamless/extra/transformers/js_transformer.py b/seamless/extra/transformers/js_transformer.py index 86c0905..d96bdc8 100644 --- a/seamless/extra/transformers/js_transformer.py +++ b/seamless/extra/transformers/js_transformer.py @@ -2,7 +2,6 @@ from ...internal.constants import ( SEAMLESS_ELEMENT_ATTRIBUTE, SEAMLESS_INIT_ATTRIBUTE, - SEAMLESS_INIT_ASYNC_ATTRIBUTE, ) @@ -15,8 +14,6 @@ def transformer(key: str, source: JavaScript, element): element.props[SEAMLESS_INIT_ATTRIBUTE] = ( element.props.get(SEAMLESS_INIT_ATTRIBUTE, "") + source.code ) - if source.async_: - element.props[SEAMLESS_INIT_ASYNC_ATTRIBUTE] = True del element.props[key] return matcher, transformer @@ -32,7 +29,7 @@ def transformer(key: str, source: JavaScript, element): element.props[SEAMLESS_ELEMENT_ATTRIBUTE] = True element.props[SEAMLESS_INIT_ATTRIBUTE] = ( element.props.get(SEAMLESS_INIT_ATTRIBUTE, "") - + f"\nthis.addEventListener('{event_name}', {'async' if source.async_ else ''}(event) => {{{source.code}}});" + + f"\nthis.addEventListener('{event_name}', (event) => {{{source.code}}});" ) del element.props[key] diff --git a/seamless/internal/constants.py b/seamless/internal/constants.py index ecea36b..d363e45 100644 --- a/seamless/internal/constants.py +++ b/seamless/internal/constants.py @@ -1,5 +1,4 @@ SEAMLESS_ELEMENT_ATTRIBUTE = "seamless:element" SEAMLESS_INIT_ATTRIBUTE = "seamless:init" -SEAMLESS_INIT_ASYNC_ATTRIBUTE = "seamless:async" DISABLE_GLOBAL_CONTEXT_ENV = "SEAMLESS_DISABLE_GLOBAL_CONTEXT" \ No newline at end of file From ca3dde99a9f31ffb36d0e388800ece02d1981a4d Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Sun, 8 Sep 2024 02:10:29 +0300 Subject: [PATCH 09/23] Update examples --- examples/simple/main.py | 2 +- examples/simple/pages/base.py | 2 +- examples/spa/main.py | 2 +- examples/spa/pages/base.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/simple/main.py b/examples/simple/main.py index 3b9b925..0763003 100644 --- a/examples/simple/main.py +++ b/examples/simple/main.py @@ -2,7 +2,7 @@ from fastapi import FastAPI from fastapi.responses import HTMLResponse, FileResponse from seamless import render -from seamless.extra.transports.socketio.middleware import SocketIOMiddleware +from seamless.middlewares import SocketIOMiddleware from components.app import App diff --git a/examples/simple/pages/base.py b/examples/simple/pages/base.py index 5971483..ab0865d 100644 --- a/examples/simple/pages/base.py +++ b/examples/simple/pages/base.py @@ -9,7 +9,7 @@ def head(self): rel="stylesheet", href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css", ) - yield Script(src=f"/static/seamless.init.js", defer=True) + yield Script(src=f"https://cdn.jsdelivr.net/npm/python-seamless/umd/seamless.init.js", defer=True) yield Style( "html, body { height: 100%; }" + CSS.to_css_string(minified=True) diff --git a/examples/spa/main.py b/examples/spa/main.py index 3a61dbe..5209b8c 100644 --- a/examples/spa/main.py +++ b/examples/spa/main.py @@ -3,7 +3,7 @@ from fastapi import FastAPI from fastapi.responses import HTMLResponse, FileResponse from seamless import render -from seamless.extra.transports.socketio.middleware import SocketIOMiddleware +from seamless.middlewares import SocketIOMiddleware from components.app import App diff --git a/examples/spa/pages/base.py b/examples/spa/pages/base.py index 5971483..ab0865d 100644 --- a/examples/spa/pages/base.py +++ b/examples/spa/pages/base.py @@ -9,7 +9,7 @@ def head(self): rel="stylesheet", href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css", ) - yield Script(src=f"/static/seamless.init.js", defer=True) + yield Script(src=f"https://cdn.jsdelivr.net/npm/python-seamless/umd/seamless.init.js", defer=True) yield Style( "html, body { height: 100%; }" + CSS.to_css_string(minified=True) From 81d4a1c55177819e933c2c904009fe3dc4b76c8d Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Sun, 8 Sep 2024 02:20:06 +0300 Subject: [PATCH 10/23] Update usage page is SPA example --- examples/spa/components/app.py | 5 ++- examples/spa/components/usage.css | 25 ++++++++++++ examples/spa/components/usage.py | 64 +++++++++++++++++++++---------- 3 files changed, 71 insertions(+), 23 deletions(-) create mode 100644 examples/spa/components/usage.css diff --git a/examples/spa/components/app.py b/examples/spa/components/app.py index 29cf9bc..e590022 100644 --- a/examples/spa/components/app.py +++ b/examples/spa/components/app.py @@ -14,10 +14,11 @@ def render(self): State.init(), SocketIOTransport.init(), Div(class_name="d-flex flex-column h-100")( - Div(class_name="d-flex justify-content-between")( - Nav(class_name="navbar navbar-expand-lg navbar-light bg-light")( + Div(class_name="d-flex justify-content-between bg-light")( + Nav(class_name="navbar navbar-expand-lg navbar-light")( RouterLink(to="/", class_name="navbar-brand")("Home"), RouterLink(to="/counter", class_name="navbar-brand")("Counter"), + RouterLink(to="/usage", class_name="navbar-brand")("Usage"), ), Div( Button(on_click=foo, style=StyleObject(border_radius="5px", background_color="red"))( diff --git a/examples/spa/components/usage.css b/examples/spa/components/usage.css new file mode 100644 index 0000000..413521b --- /dev/null +++ b/examples/spa/components/usage.css @@ -0,0 +1,25 @@ +.table { + width: 100%; + border-collapse: collapse; +} + +.table th { + background-color: #f2f2f2; + border: 1px solid #ddd; + padding: 8px; + text-align: left; +} + +.table td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; +} + +.table tr:nth-child(odd) { + background-color: #f2f2f2; +} + +.table tr:hover { + background-color: #f2f2f2; +} diff --git a/examples/spa/components/usage.py b/examples/spa/components/usage.py index a304fb0..8599455 100644 --- a/examples/spa/components/usage.py +++ b/examples/spa/components/usage.py @@ -1,6 +1,34 @@ -from seamless import Component, Div, Table, Th, Tr, Td, H2 +from seamless import Component, Div, Table, Th, Tr, Td, Span, Details, Summary from seamless.context import Context from seamless.extra.events import EventsFeature +from seamless.extra.events.database import Action +from seamless.styling import CSS + +styles = CSS.module("./usage.css") + + +class ActionTable(Component): + def __init__(self, actions: dict[str, Action]): + self.actions = actions + + def render(self): + return Table(class_name=styles.table)( + Tr( + Th("Index"), + Th("Event ID"), + Th("Module Name"), + Th("Function Name"), + ), + *( + Tr( + Td(index + 1), + Td(event_id), + Td(func.action.__module__), + Td(func.action.__name__), + ) + for index, (event_id, func) in enumerate(self.actions.items()) + ), + ) class Usage(Component, inject_render=True): @@ -9,30 +37,24 @@ def render(self, context: Context): total_scoped = sum(len(scope) for scope in events.DB.scoped_events.values()) return Div( - H2(f"Global Events - Total: {len(events.DB.events)}"), - Table( - Tr( - Th("Event ID"), - ), - *( - Tr( - Td(event_id), + Details( + Summary( + Span(class_name="h2")( + f"Global Events - Total: {len(events.DB.events)}" ) - for event_id in events.DB.events ), + ActionTable(actions=events.DB.events), ), - H2(f"Actions - Total: {total_scoped}"), - Table( - Tr( - Th("Scope ID"), - Th("Actions IDs"), - ), + Details( + Summary(Span(class_name="h2")(f"Actions - Total: {total_scoped}")), *( - Tr( - Td(scope), - Td(", ".join(actions.keys())) + Details( + Summary(Span(class_name="h3")(f"Scope: {scope}")), + Div( + ActionTable(actions=events.DB.scoped_events[scope]), + ), ) - for scope, actions in events.DB.scoped_events.items() - ) + for scope in events.DB.scoped_events + ), ), ) From e1f97b98df02309f8cabbaf4259ece4a3cf4e8aa Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Thu, 12 Sep 2024 10:13:52 +0300 Subject: [PATCH 11/23] Change state to current --- examples/simple/pages/counter.py | 4 ++-- examples/spa/pages/counter.py | 4 ++-- seamless/extra/state/__init__.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/simple/pages/counter.py b/examples/simple/pages/counter.py index de5130a..633bc13 100644 --- a/examples/simple/pages/counter.py +++ b/examples/simple/pages/counter.py @@ -27,14 +27,14 @@ def render(self): Div(class_name="row")( Div(class_name="col-12 text-center")( Div(class_name="btn-group")( - Div(class_name="btn btn-danger", on_click=counter("state - 1"))( + Div(class_name="btn btn-danger", on_click=counter("current - 1"))( "Decrement" ), Div(class_name="btn btn-primary", on_click=counter("0"))( "Reset" ), Div( - class_name="btn btn-success", on_click=counter("state + 1") + class_name="btn btn-success", on_click=counter("current + 1") )("Increment"), ), ), diff --git a/examples/spa/pages/counter.py b/examples/spa/pages/counter.py index 73291ba..a5d31b6 100644 --- a/examples/spa/pages/counter.py +++ b/examples/spa/pages/counter.py @@ -27,14 +27,14 @@ def render(self): Div(class_name="row")( Div(class_name="col-12 text-center")( Div(class_name="btn-group")( - Div(class_name="btn btn-danger", on_click=counter("state - 1"))( + Div(class_name="btn btn-danger", on_click=counter("current - 1"))( "Decrement" ), Div(class_name="btn btn-primary", on_click=counter("0"))( "Reset" ), Div( - class_name="btn btn-success", on_click=counter("state + 1") + class_name="btn btn-success", on_click=counter("current + 1") )("Increment"), ), ), diff --git a/seamless/extra/state/__init__.py b/seamless/extra/state/__init__.py index a0a2e25..5d2de36 100644 --- a/seamless/extra/state/__init__.py +++ b/seamless/extra/state/__init__.py @@ -38,7 +38,7 @@ def get(self): def set(self, value): return JS( - f"""const state = seamless.state.getState('{self.name}');\ + f"""const current = seamless.state.getState('{self.name}');\ seamless.state.setState('{self.name}', {value})""" ) From b3d9a148fd320395359df4727fa9b91a03d9dcec Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Mon, 23 Sep 2024 06:09:43 +0300 Subject: [PATCH 12/23] Update docs --- docs/.gitignore | 4 ++ docs/3-events/1-middleware.rst | 21 ------- docs/Makefile | 20 +++++++ docs/_static/images/seamless.svg | 1 - docs/index.rst | 33 ----------- docs/make.bat | 35 ++++++++++++ docs/package.json | 6 ++ docs/requirements.txt | Bin 47 -> 140 bytes docs/source/_static/css/custom.css | 28 +++++++++ docs/{ => source/_static/images}/favicon.ico | Bin docs/source/_static/images/favicon.png | Bin 0 -> 17032 bytes docs/source/_static/images/favicon.svg | 1 + .../_static/images/quick-start.jpeg | Bin docs/source/_static/images/seamless.svg | 1 + .../api-reference}/1-components/1-base.rst | 0 .../api-reference}/1-components/index.rst | 0 .../api-reference}/1-components/page.rst | 0 .../1-components/router/index.rst | 0 .../1-components/router/route.rst | 0 .../1-components/router/router-link.rst | 0 .../1-components/router/router.rst | 0 .../api-reference}/2-state/index.rst | 0 .../api-reference}/3-core/index.rst | 6 +- .../api-reference}/3-core/rendering.rst | 0 .../api-reference}/99-misc/index.rst | 0 .../api-reference}/index.rst | 8 +-- docs/{ => source}/conf.py | 38 ++++++++++-- docs/source/index.rst | 54 ++++++++++++++++++ .../user-guide}/1-basics/1-syntax.rst | 42 ++++++++------ .../1-basics/2-rendering-components.rst | 3 + .../user-guide}/1-basics/3-dynamic-pages.rst | 2 +- .../user-guide}/1-basics/4-state.rst | 4 +- .../user-guide}/1-basics/index.rst | 0 .../2-components/1-base-component.rst | 3 +- .../user-guide}/2-components/2-page.rst | 5 +- .../user-guide}/2-components/3-fragment.rst | 0 .../user-guide}/2-components/4-router.rst | 1 + .../user-guide}/2-components/index.rst | 0 .../user-guide/3-events/1-transports.rst | 47 +++++++++++++++ .../3-events/2-data-validation.rst | 4 +- .../user-guide}/3-events/index.rst | 29 ++++++---- .../user-guide}/4-styling/1-style-object.rst | 6 ++ .../user-guide}/4-styling/2-css-modules.rst | 19 +++++- .../user-guide}/4-styling/index.rst | 0 .../user-guide}/5-advanced/1-javascript.rst | 0 .../user-guide}/5-advanced/2-empty.rst | 0 .../user-guide}/5-advanced/3-context.rst | 20 +++++-- .../user-guide}/5-advanced/4-transformers.rst | 0 .../user-guide}/5-advanced/5-rendering.rst | 2 +- .../user-guide/5-advanced/6-transports.rst | 18 ++++++ .../user-guide}/5-advanced/index.rst | 1 + docs/source/user-guide/index.rst | 12 ++++ docs/{ => source/user-guide}/quick-start.rst | 4 +- 53 files changed, 365 insertions(+), 113 deletions(-) create mode 100644 docs/.gitignore delete mode 100644 docs/3-events/1-middleware.rst create mode 100644 docs/Makefile delete mode 100644 docs/_static/images/seamless.svg delete mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/package.json create mode 100644 docs/source/_static/css/custom.css rename docs/{ => source/_static/images}/favicon.ico (100%) create mode 100644 docs/source/_static/images/favicon.png create mode 100644 docs/source/_static/images/favicon.svg rename docs/{ => source}/_static/images/quick-start.jpeg (100%) create mode 100644 docs/source/_static/images/seamless.svg rename docs/{99-api-reference => source/api-reference}/1-components/1-base.rst (100%) rename docs/{99-api-reference => source/api-reference}/1-components/index.rst (100%) rename docs/{99-api-reference => source/api-reference}/1-components/page.rst (100%) rename docs/{99-api-reference => source/api-reference}/1-components/router/index.rst (100%) rename docs/{99-api-reference => source/api-reference}/1-components/router/route.rst (100%) rename docs/{99-api-reference => source/api-reference}/1-components/router/router-link.rst (100%) rename docs/{99-api-reference => source/api-reference}/1-components/router/router.rst (100%) rename docs/{99-api-reference => source/api-reference}/2-state/index.rst (100%) rename docs/{99-api-reference => source/api-reference}/3-core/index.rst (73%) rename docs/{99-api-reference => source/api-reference}/3-core/rendering.rst (100%) rename docs/{99-api-reference => source/api-reference}/99-misc/index.rst (100%) rename docs/{99-api-reference => source/api-reference}/index.rst (63%) rename docs/{ => source}/conf.py (57%) create mode 100644 docs/source/index.rst rename docs/{ => source/user-guide}/1-basics/1-syntax.rst (80%) rename docs/{ => source/user-guide}/1-basics/2-rendering-components.rst (95%) rename docs/{ => source/user-guide}/1-basics/3-dynamic-pages.rst (95%) rename docs/{ => source/user-guide}/1-basics/4-state.rst (96%) rename docs/{ => source/user-guide}/1-basics/index.rst (100%) rename docs/{ => source/user-guide}/2-components/1-base-component.rst (97%) rename docs/{ => source/user-guide}/2-components/2-page.rst (97%) rename docs/{ => source/user-guide}/2-components/3-fragment.rst (100%) rename docs/{ => source/user-guide}/2-components/4-router.rst (97%) rename docs/{ => source/user-guide}/2-components/index.rst (100%) create mode 100644 docs/source/user-guide/3-events/1-transports.rst rename docs/{ => source/user-guide}/3-events/2-data-validation.rst (94%) rename docs/{ => source/user-guide}/3-events/index.rst (82%) rename docs/{ => source/user-guide}/4-styling/1-style-object.rst (85%) rename docs/{ => source/user-guide}/4-styling/2-css-modules.rst (64%) rename docs/{ => source/user-guide}/4-styling/index.rst (100%) rename docs/{ => source/user-guide}/5-advanced/1-javascript.rst (100%) rename docs/{ => source/user-guide}/5-advanced/2-empty.rst (100%) rename docs/{ => source/user-guide}/5-advanced/3-context.rst (87%) rename docs/{ => source/user-guide}/5-advanced/4-transformers.rst (100%) rename docs/{ => source/user-guide}/5-advanced/5-rendering.rst (97%) create mode 100644 docs/source/user-guide/5-advanced/6-transports.rst rename docs/{ => source/user-guide}/5-advanced/index.rst (84%) create mode 100644 docs/source/user-guide/index.rst rename docs/{ => source/user-guide}/quick-start.rst (94%) diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..a6e7e9b --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,4 @@ +.lh/ +build/* +.venv/ +seamless/ \ No newline at end of file diff --git a/docs/3-events/1-middleware.rst b/docs/3-events/1-middleware.rst deleted file mode 100644 index 7cfd8f3..0000000 --- a/docs/3-events/1-middleware.rst +++ /dev/null @@ -1,21 +0,0 @@ -.. _asg-middleware: - -############### -ASGI Middleware -############### - -The ``ASGIMiddleware`` class is the middleware that handles all the linked actions between the -frontend components and the backend functions. - -When linking a python function to a frontend component, the ``ASGIMiddleware`` middleware must be -added to the ASGI application. - -.. code-block:: python - :caption: Adding the ASGIMiddleware to the ASGI application - - from fastapi import FastAPI - from seamless.middlewares import ASGIMiddleware as SeamlessMiddleware - - app = FastAPI() - app.add_middleware(SeamlessMiddleware) - diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/images/seamless.svg b/docs/_static/images/seamless.svg deleted file mode 100644 index 57b0f7a..0000000 --- a/docs/_static/images/seamless.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index c3f5744..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,33 +0,0 @@ -######## -Seamless -######## - -.. image:: _static/images/seamless.svg - :alt: Seamless - :align: center - -.. image:: https://img.shields.io/pypi/v/python-seamless.svg - :target: https://pypi.org/project/python-seamless/ - :alt: PyPI version - :align: center - -|br| - -Welcome to Seamless's documentation! -#################################### - -Seamless is a Python library for building web applications with a focus on simplicity and ease of use. -It is designed to be easy to learn and use, while still providing the power and flexibility needed for complex applications. - -.. toctree:: - :maxdepth: 2 - :glob: - - quick-start - */index - - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..1298b1c --- /dev/null +++ b/docs/package.json @@ -0,0 +1,6 @@ +{ + "scripts": { + "dev": "sphinx-autobuild -b html source build --host 0.0.0.0 --port 8080", + "dev:clean": "sphinx-autobuild -a -b html source build --host 0.0.0.0 --port 8080" + } +} \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 71d8116763a05a31dd237359fbd828f39fde3aaf..bb8d2be02f2ad194d492cfb4dd19b5cac5eb5fc2 100644 GIT binary patch literal 140 zcmZ9E(F#CN5Jb2QhmC^_`H_~kx%m4rY literal 47 wcmXRe$jHpA&@C!S(JjeH%}wPhh6ogwCKZ=tmXwxc=I80AR+OaX6$4qt0KDK5-2eap diff --git a/docs/source/_static/css/custom.css b/docs/source/_static/css/custom.css new file mode 100644 index 0000000..b4454fc --- /dev/null +++ b/docs/source/_static/css/custom.css @@ -0,0 +1,28 @@ +html[data-theme="light"] { + --pst-color-primary: #00518a; + --pst-color-secondary: #f7f7f7; + --pst-color-secondary-hover: #e6e6e6; +} + +html[data-theme="dark"] { + --pst-color-primary: #0b7ed1; + --pst-color-secondary: #f7f7f7; + --pst-color-secondary-hover: #e6e6e6; +} + +.navbar-brand { + gap: 1rem; +} + +.navbar-brand img { + height: 85%; +} + +.navbar-item nav { + border-left: 2px solid #5d5d5f; + padding-left: 8px; +} + +#pst-back-to-top:hover { + background-color: var(--pst-color-secondary-hover); +} \ No newline at end of file diff --git a/docs/favicon.ico b/docs/source/_static/images/favicon.ico similarity index 100% rename from docs/favicon.ico rename to docs/source/_static/images/favicon.ico diff --git a/docs/source/_static/images/favicon.png b/docs/source/_static/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..04a253445a0cbc873dd8e8c6bf7ba289100d8582 GIT binary patch literal 17032 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 Wyds)$6t>s^0000 \ No newline at end of file diff --git a/docs/_static/images/quick-start.jpeg b/docs/source/_static/images/quick-start.jpeg similarity index 100% rename from docs/_static/images/quick-start.jpeg rename to docs/source/_static/images/quick-start.jpeg diff --git a/docs/source/_static/images/seamless.svg b/docs/source/_static/images/seamless.svg new file mode 100644 index 0000000..2fec43f --- /dev/null +++ b/docs/source/_static/images/seamless.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/99-api-reference/1-components/1-base.rst b/docs/source/api-reference/1-components/1-base.rst similarity index 100% rename from docs/99-api-reference/1-components/1-base.rst rename to docs/source/api-reference/1-components/1-base.rst diff --git a/docs/99-api-reference/1-components/index.rst b/docs/source/api-reference/1-components/index.rst similarity index 100% rename from docs/99-api-reference/1-components/index.rst rename to docs/source/api-reference/1-components/index.rst diff --git a/docs/99-api-reference/1-components/page.rst b/docs/source/api-reference/1-components/page.rst similarity index 100% rename from docs/99-api-reference/1-components/page.rst rename to docs/source/api-reference/1-components/page.rst diff --git a/docs/99-api-reference/1-components/router/index.rst b/docs/source/api-reference/1-components/router/index.rst similarity index 100% rename from docs/99-api-reference/1-components/router/index.rst rename to docs/source/api-reference/1-components/router/index.rst diff --git a/docs/99-api-reference/1-components/router/route.rst b/docs/source/api-reference/1-components/router/route.rst similarity index 100% rename from docs/99-api-reference/1-components/router/route.rst rename to docs/source/api-reference/1-components/router/route.rst diff --git a/docs/99-api-reference/1-components/router/router-link.rst b/docs/source/api-reference/1-components/router/router-link.rst similarity index 100% rename from docs/99-api-reference/1-components/router/router-link.rst rename to docs/source/api-reference/1-components/router/router-link.rst diff --git a/docs/99-api-reference/1-components/router/router.rst b/docs/source/api-reference/1-components/router/router.rst similarity index 100% rename from docs/99-api-reference/1-components/router/router.rst rename to docs/source/api-reference/1-components/router/router.rst diff --git a/docs/99-api-reference/2-state/index.rst b/docs/source/api-reference/2-state/index.rst similarity index 100% rename from docs/99-api-reference/2-state/index.rst rename to docs/source/api-reference/2-state/index.rst diff --git a/docs/99-api-reference/3-core/index.rst b/docs/source/api-reference/3-core/index.rst similarity index 73% rename from docs/99-api-reference/3-core/index.rst rename to docs/source/api-reference/3-core/index.rst index a13af66..4b4ebeb 100644 --- a/docs/99-api-reference/3-core/index.rst +++ b/docs/source/api-reference/3-core/index.rst @@ -1,6 +1,6 @@ -########## -Components -########## +#### +Core +#### .. py:module:: seamless.core diff --git a/docs/99-api-reference/3-core/rendering.rst b/docs/source/api-reference/3-core/rendering.rst similarity index 100% rename from docs/99-api-reference/3-core/rendering.rst rename to docs/source/api-reference/3-core/rendering.rst diff --git a/docs/99-api-reference/99-misc/index.rst b/docs/source/api-reference/99-misc/index.rst similarity index 100% rename from docs/99-api-reference/99-misc/index.rst rename to docs/source/api-reference/99-misc/index.rst diff --git a/docs/99-api-reference/index.rst b/docs/source/api-reference/index.rst similarity index 63% rename from docs/99-api-reference/index.rst rename to docs/source/api-reference/index.rst index 70a93fe..6211d48 100644 --- a/docs/99-api-reference/index.rst +++ b/docs/source/api-reference/index.rst @@ -6,8 +6,8 @@ API Reference This section contains the API reference for Seamless. -.. toctree:: - :glob: - :maxdepth: 2 +.. autosummary:: + :toctree: _autosummary + :recursive: - */index + seamless \ No newline at end of file diff --git a/docs/conf.py b/docs/source/conf.py similarity index 57% rename from docs/conf.py rename to docs/source/conf.py index eac12d9..d067050 100644 --- a/docs/conf.py +++ b/docs/source/conf.py @@ -1,4 +1,7 @@ -from seamless.version import version as __version__ +import sys + +sys.path.insert(0, "../..") +from seamless import __version__ # Configuration file for the Sphinx documentation builder. # @@ -17,8 +20,9 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ - "sphinx_rtd_theme", + "pydata_sphinx_theme", "sphinx_substitution_extensions", + "sphinx.ext.autodoc", ] templates_path = ["_templates"] @@ -28,14 +32,38 @@ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = "sphinx_rtd_theme" +html_favicon = "_static/images/favicon.ico" +html_logo = "_static/images/favicon.svg" html_static_path = ["_static"] -html_favicon = "favicon.ico" +html_theme = "pydata_sphinx_theme" html_title = "Seamless Documentation" +html_theme_options = { + "icon_links": [ + { + "name": "GitHub", + "url": "https://github.com/xpodev/seamless", + "icon": "fa-brands fa-github", + }, + { + "name": "PyPI", + "url": "https://pypi.org/project/python-seamless", + "icon": "fa-brands fa-python", + }, + ], + "logo": { + "alt_text": "Seamless", + "text": "Seamless", + }, +} + +html_css_files = [ + "css/custom.css", +] + rst_prolog = f""" .. |version| replace:: {version} .. |br| raw:: html
-""" \ No newline at end of file +""" diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..1efad52 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,54 @@ +######## +Seamless +######## + +.. raw:: html + + + +.. image:: /_static/images/seamless.svg + :alt: Seamless + :align: center + :class: dark-light py-4 + +.. raw:: html + +

+ Simple to learn, easy to use, fully-featured UI library for Python +

+ +.. image:: https://img.shields.io/pypi/v/python-seamless.svg + :target: https://pypi.org/project/python-seamless/ + :alt: PyPI version + :align: center + +|br| + + +Seamless is a Python library for building web applications with a focus on simplicity and ease of use. +It is designed to be easy to learn and use, while still providing the power and flexibility needed for +complex applications. + +Seamless provides a simple and intuitive API that makes it easy to create complex web applications with +just a few lines of code. It includes a wide range of components and utilities that make it easy to build +responsive and interactive user interfaces. + +Key features of Seamless include: + +- **Simple API**: Seamless provides a simple and intuitive API that makes it easy to create complex web applications + with just a few lines of code. +- **Familiarity**: Seamless is designed to be familiar to web developers, with a syntax that is similar to React. +- **Extensibility**: Seamless is built on top of `PyDOM `_, a powerful and flexible + library for creating and manipulating HTML elements in Python. +- **Compatibility**: Seamless is compatible with any Python web framework that supports ASGI, including `Starlette `_ + and `FastAPI `_. It can also be used with other ASGI-compatible frameworks such as `Django `_ + and `Flask `_. + +.. toctree:: + :glob: + :hidden: + + user-guide/index + api-reference/index diff --git a/docs/1-basics/1-syntax.rst b/docs/source/user-guide/1-basics/1-syntax.rst similarity index 80% rename from docs/1-basics/1-syntax.rst rename to docs/source/user-guide/1-basics/1-syntax.rst index de7b8bf..5ab83c6 100644 --- a/docs/1-basics/1-syntax.rst +++ b/docs/source/user-guide/1-basics/1-syntax.rst @@ -53,13 +53,15 @@ This is called "Pythonic" because it is similar to how we write Python code. from seamless import Div, render from card import Card - render(Card( - Div( # This is the Card's child - "Hello, World!", # This is the content of the Div - id="card-content" - ), - title="Card Title" - )) + render( + Card( + Div( # This is the Card's child + "Hello, World!", # This is the content of the Div + id="card-content" + ), + title="Card Title" + ) + ) HTML Syntax ########### @@ -74,11 +76,13 @@ This is similar to how we write HTML tags, with the props as attributes and the from seamless import Div, render from card import Card - render(Card(title="Card Title")( - Div(id="card-content")( # This is the Card's child - "Hello, World!" # This is the content of the Div + render( + Card(title="Card Title")( + Div(id="card-content")( # This is the Card's child + "Hello, World!" # This is the content of the Div + ) ) - )) + ) Flutter Syntax ############## @@ -93,13 +97,15 @@ This is similar to how we write Flutter widgets. from seamless import Div, render from card import Card - render(Card( - title="Card Title", - children=[Div( # This is the Card's child - id="card-content". - children=["Hello, World!"] # This is the content of the Div - )] - )) + render( + Card( + title="Card Title", + children=[Div( # This is the Card's child + id="card-content". + children=["Hello, World!"] # This is the content of the Div + )] + ) + ) .. note:: diff --git a/docs/1-basics/2-rendering-components.rst b/docs/source/user-guide/1-basics/2-rendering-components.rst similarity index 95% rename from docs/1-basics/2-rendering-components.rst rename to docs/source/user-guide/1-basics/2-rendering-components.rst index 8717043..4cee85c 100644 --- a/docs/1-basics/2-rendering-components.rst +++ b/docs/source/user-guide/1-basics/2-rendering-components.rst @@ -134,3 +134,6 @@ The full list of prop names and their corresponding HTML attributes is as follow All ``on_`` props that are python functions are converted to event listeners in the HTML representation and will not be rendered as attributes. +By default, all props with underscore in their name and a value of :py:type:`~seamless.types.Primitive` type, +are converted to HTML attributes with dash instead of underscore. + diff --git a/docs/1-basics/3-dynamic-pages.rst b/docs/source/user-guide/1-basics/3-dynamic-pages.rst similarity index 95% rename from docs/1-basics/3-dynamic-pages.rst rename to docs/source/user-guide/1-basics/3-dynamic-pages.rst index 1f67ee9..ad936bf 100644 --- a/docs/1-basics/3-dynamic-pages.rst +++ b/docs/source/user-guide/1-basics/3-dynamic-pages.rst @@ -63,5 +63,5 @@ alternatively, you can add the script at the end of the body tag. def head(self): return ( *super().head(), - Script(src="https://cdn.jsdelivr.net/npm/@python-seamless@|version|/umd/seamless.min.js") + Script(src="https://cdn.jsdelivr.net/npm/python-seamless@|version|/umd/seamless.min.js") ) \ No newline at end of file diff --git a/docs/1-basics/4-state.rst b/docs/source/user-guide/1-basics/4-state.rst similarity index 96% rename from docs/1-basics/4-state.rst rename to docs/source/user-guide/1-basics/4-state.rst index 6ab2198..76fab2d 100644 --- a/docs/1-basics/4-state.rst +++ b/docs/source/user-guide/1-basics/4-state.rst @@ -55,7 +55,7 @@ Using a State To use a state, call the state object with the new value to update the state. Setting the state is done by calling the state object with the new value as a JavaScript expression. -When setting a new value to the state, the current state is available as ``state`` in the expression. +When setting a new value to the state, the current state is available as ``current`` in the expression. Getting the state is done by calling the state object without any arguments. @@ -67,7 +67,7 @@ Getting the state is done by calling the state object without any arguments. return Div( Button( "Increment", - on_click=counter("state + 1") + on_click=counter("current + 1") ), Span(counter()) ) diff --git a/docs/1-basics/index.rst b/docs/source/user-guide/1-basics/index.rst similarity index 100% rename from docs/1-basics/index.rst rename to docs/source/user-guide/1-basics/index.rst diff --git a/docs/2-components/1-base-component.rst b/docs/source/user-guide/2-components/1-base-component.rst similarity index 97% rename from docs/2-components/1-base-component.rst rename to docs/source/user-guide/2-components/1-base-component.rst index f378d33..384cc31 100644 --- a/docs/2-components/1-base-component.rst +++ b/docs/source/user-guide/2-components/1-base-component.rst @@ -82,8 +82,7 @@ The most common way of handling props is to store them as instance variables and class AppButton(Component): def __init__(self, type: str): - self.color = color - self.style = Style(background_color=f"var(--color-{color})") + self.style = Style(background_color=f"var(--color-{type})") def render(self): return Button(style=self.style)( diff --git a/docs/2-components/2-page.rst b/docs/source/user-guide/2-components/2-page.rst similarity index 97% rename from docs/2-components/2-page.rst rename to docs/source/user-guide/2-components/2-page.rst index f822f91..c720d2b 100644 --- a/docs/2-components/2-page.rst +++ b/docs/source/user-guide/2-components/2-page.rst @@ -7,7 +7,7 @@ Page The page component is a the top-level component that represents a web page. It is a container that holds all the other components that will be rendered on the page. -The default page component includes the components for the following HTML structure: +The default page component includes the elements for the following HTML structure: .. code-block:: html :caption: Default page structure @@ -42,6 +42,7 @@ The page component can be used to create a new page by passing the following pro from seamless import Div, P from seamless.components import Page + def my_awesome_page(): return Page(title="My awesome page")( Div(class_name="container mt-5")( @@ -68,6 +69,7 @@ You can create custom pages by extending the page component and overriding the d from seamless import Div, Link from seamless.components import Page + class MyPage(Page): def head(self): return ( @@ -85,6 +87,7 @@ You can create custom pages by extending the page component and overriding the d ) ) + def my_awesome_page(): return MyPage(title="My awesome page")( Div(class_name="container mt-5")( diff --git a/docs/2-components/3-fragment.rst b/docs/source/user-guide/2-components/3-fragment.rst similarity index 100% rename from docs/2-components/3-fragment.rst rename to docs/source/user-guide/2-components/3-fragment.rst diff --git a/docs/2-components/4-router.rst b/docs/source/user-guide/2-components/4-router.rst similarity index 97% rename from docs/2-components/4-router.rst rename to docs/source/user-guide/2-components/4-router.rst index 9d7aa59..9e60fe1 100644 --- a/docs/2-components/4-router.rst +++ b/docs/source/user-guide/2-components/4-router.rst @@ -117,6 +117,7 @@ The supported types are: - ``int`` - ``float`` +- ``path`` - a string that captures the path until the end of the path without query parameters (e.g. ``/user/{path:path}``) The parameter will be passed to the component as a prop with the same name. diff --git a/docs/2-components/index.rst b/docs/source/user-guide/2-components/index.rst similarity index 100% rename from docs/2-components/index.rst rename to docs/source/user-guide/2-components/index.rst diff --git a/docs/source/user-guide/3-events/1-transports.rst b/docs/source/user-guide/3-events/1-transports.rst new file mode 100644 index 0000000..cced813 --- /dev/null +++ b/docs/source/user-guide/3-events/1-transports.rst @@ -0,0 +1,47 @@ +.. _transports: + +########## +Transports +########## + +Transports are the underlying mechanism that the event system uses to send +messages between the server and the client. + +By default, Seamless uses `socket.io `_ as the transport mechanism. This +allows for real-time, bidirectional and event-based communication between the +server and the client. + +.. note:: + The transport mechanism is abstracted away from the user, and you do not + need to interact with it directly. The event system provides a high-level + API that allows you to send and receive messages without worrying about the + underlying transport mechanism. + +To use the event system, you need to add the transport middleware to your ASGI app and +initialize the event system. + +.. code-block:: python + :caption: Adding the transport middleware + + from fastapi import FastAPI + from seamless.middlewares import SocketIOMiddleware + + app = FastAPI() + app.add_middleware(SocketIOMiddleware) + +To initialize the event system, in the root component of your application (can be the base page or the main layout) +call the ``SocketIOTransport.init`` method from the ``seamless.extension`` module. + +.. code-block:: python + :caption: Initializing the event system + + from seamless.extension import SocketIOTransport + + class MyPage(Page): + def body(self): + return ( + SocketIOTransport.init(), + *super().body() + ) + +This will initialize the event system and make it available to the components. \ No newline at end of file diff --git a/docs/3-events/2-data-validation.rst b/docs/source/user-guide/3-events/2-data-validation.rst similarity index 94% rename from docs/3-events/2-data-validation.rst rename to docs/source/user-guide/3-events/2-data-validation.rst index b3c5b7a..16a0a8a 100644 --- a/docs/3-events/2-data-validation.rst +++ b/docs/source/user-guide/3-events/2-data-validation.rst @@ -33,8 +33,8 @@ Here is an example of how to use ``pydantic`` to validate data: ) ) - def on_submit(self, data: SubmitEvent[UserLogin]): - print(data) + def on_submit(self, event: SubmitEvent[UserLogin]): + print(event.data) This will create a form with the fields ``username``, ``password``, and ``remember_me``, and a submit button. diff --git a/docs/3-events/index.rst b/docs/source/user-guide/3-events/index.rst similarity index 82% rename from docs/3-events/index.rst rename to docs/source/user-guide/3-events/index.rst index dfb25f9..d3f7c20 100644 --- a/docs/3-events/index.rst +++ b/docs/source/user-guide/3-events/index.rst @@ -39,11 +39,19 @@ The following example demonstrates how to submit a form to the server: ), ) - def save_email(self, event_data: SubmitEvent): + def save_email(self, event: SubmitEvent): user = get_user(self.email) - user.email = event_data["email"] + user.email = event.data["email"] user.save() +In this example, we bind the ``submit`` event of the form to the ``save_email`` method of the user. + +.. code-block:: python + + Form(on_submit=self.save_email) + +When the form is submitted, the ``save_email`` method is called with the event data. + Scoping ####### @@ -104,7 +112,7 @@ of the element and will register the event listener. :caption: Event function wrapper function (seamless) { - this.addEventListener(EVENT_NAME, function(event) { + this.addEventListener(EVENT_NAME, (event) => { // JavaScript code }); } @@ -118,19 +126,18 @@ You can inject parameters into the event handler by using annotations in the fun The injectable parameters are always passed as keyword arguments. |br| Event functions can not have positional-only arguments. -For example, you can access the socket ID by adding a parameter to the event handler function -with the ``SocketID`` type from the ``seamless.core`` module. - -The socket id can be used to send messages to the client that triggered the event. +For example, you can access the client ID by adding a parameter to the event handler function +with the ``ClientID`` type from the ``seamless.extra.transports`` module. .. code-block:: python :caption: Accessing the socket ID - from seamless.core import SocketID + from seamless.extra.transports import ClientID - def on_event(self, event_data: SubmitEvent, socket_id: SocketID): - print(socket_id) + def on_event(self, event_data: SubmitEvent, cid: ClientID): + print("Client ID:", cid) In the standard context, the following injectable parameters are available: -- **Socket ID**: The unique ID of the socket that triggered the event. Generated by socket.io (`More `_). \ No newline at end of file +- **Client ID**: The unique ID of the client that triggered the event. +- **Context**: The current :ref:`Seamless context `. \ No newline at end of file diff --git a/docs/4-styling/1-style-object.rst b/docs/source/user-guide/4-styling/1-style-object.rst similarity index 85% rename from docs/4-styling/1-style-object.rst rename to docs/source/user-guide/4-styling/1-style-object.rst index b139084..61889ab 100644 --- a/docs/4-styling/1-style-object.rst +++ b/docs/source/user-guide/4-styling/1-style-object.rst @@ -43,3 +43,9 @@ This will create an inline style with the properties from the style object. :caption: Style object output
Hello, world!
+ +Properties +########## + +The ``StyleObject`` class has all the properties from the CSS specification. +The only difference is that the properties are written in snake_case instead of kebab-case. \ No newline at end of file diff --git a/docs/4-styling/2-css-modules.rst b/docs/source/user-guide/4-styling/2-css-modules.rst similarity index 64% rename from docs/4-styling/2-css-modules.rst rename to docs/source/user-guide/4-styling/2-css-modules.rst index 25d5674..aa8e50c 100644 --- a/docs/4-styling/2-css-modules.rst +++ b/docs/source/user-guide/4-styling/2-css-modules.rst @@ -10,7 +10,7 @@ It's a way to write modular CSS that won't conflict with other styles in your ap Usage ##### -To use CSS Modules in a component, import the ``CSS`` object from the ``seamless.styling`` module. +To use CSS Modules in a component, import the ``CSS`` class from the ``seamless.styling`` module. Then, use the ``CSS`` to import your css files. .. code-block:: css @@ -42,3 +42,20 @@ This will generate a class name that is unique to this css file, and apply the s This way, you can be sure that your styles won't conflict with other styles in your app. When importing the same css file in multiple components, the class name will be the same across all components. + +CSS File Lookup +############### + +When importing a css file, unless the path starts with a ``./``, Seamless will look for the css file in +the root folder for CSS Modules. +By default, the root folder for CSS Modules is the current working directory. +You can change the root folder by calling the ``CSS.set_root_folder`` method. + +.. code-block:: python + :caption: Changing the root folder for CSS Modules + + from seamless.styling import CSS + + CSS.set_root_folder("./styles") + + styles = CSS.module("card.css") diff --git a/docs/4-styling/index.rst b/docs/source/user-guide/4-styling/index.rst similarity index 100% rename from docs/4-styling/index.rst rename to docs/source/user-guide/4-styling/index.rst diff --git a/docs/5-advanced/1-javascript.rst b/docs/source/user-guide/5-advanced/1-javascript.rst similarity index 100% rename from docs/5-advanced/1-javascript.rst rename to docs/source/user-guide/5-advanced/1-javascript.rst diff --git a/docs/5-advanced/2-empty.rst b/docs/source/user-guide/5-advanced/2-empty.rst similarity index 100% rename from docs/5-advanced/2-empty.rst rename to docs/source/user-guide/5-advanced/2-empty.rst diff --git a/docs/5-advanced/3-context.rst b/docs/source/user-guide/5-advanced/3-context.rst similarity index 87% rename from docs/5-advanced/3-context.rst rename to docs/source/user-guide/5-advanced/3-context.rst index 97f48ec..1977be0 100644 --- a/docs/5-advanced/3-context.rst +++ b/docs/source/user-guide/5-advanced/3-context.rst @@ -8,16 +8,26 @@ Context is an advanced feature of Seamless that allows you to customize the rend The default context is created using the ``Context.standard`` method and has the following features in order: +- **SocketIO Feature**: This feature allows you to connect between the client and server using SocketIO. + + .. code-block:: python + + from seamless.context import Context + from seamless.extra.state import SocketIOTransport + + context = Context() + context.add_feature(SocketIOTransport) + - **Component Repository**: A repository for storing components - this handles the ``component`` event and allows you to store and retrieve components after the initial render. .. code-block:: python from seamless.context import Context - from seamless.extra.components import init_components + from seamless.extra.components import ComponentsFeature context = Context() - context.add_feature(init_components) + context.add_feature(ComponentsFeature) - **Events Feature**: This feature allows you to connect between HTML events and Python functions. @@ -34,10 +44,10 @@ The default context is created using the ``Context.standard`` method and has the .. code-block:: python from seamless.context import Context - from seamless.extra.state import init_state + from seamless.extra.state import StateFeature context = Context() - context.add_feature(init_state) + context.add_feature(StateFeature) The standard context also comes with the following :ref:`property transformers` in order: @@ -114,4 +124,4 @@ The standard context also comes with the following :ref:`property transformers

` - The Seamless Context - customizing the rendering process. - :ref:`Transformers ` - The property and post-render transformers. - :ref:`Rendering ` - The rendering process in Seamless. +- :ref:`Transports ` - Creating custom transports for the event system. diff --git a/docs/source/user-guide/index.rst b/docs/source/user-guide/index.rst new file mode 100644 index 0000000..d5f6317 --- /dev/null +++ b/docs/source/user-guide/index.rst @@ -0,0 +1,12 @@ +.. _user-guide: + +########## +User Guide +########## + +.. toctree:: + :maxdepth: 2 + :glob: + + quick-start + */index diff --git a/docs/quick-start.rst b/docs/source/user-guide/quick-start.rst similarity index 94% rename from docs/quick-start.rst rename to docs/source/user-guide/quick-start.rst index 3369094..8667a04 100644 --- a/docs/quick-start.rst +++ b/docs/source/user-guide/quick-start.rst @@ -22,7 +22,7 @@ Seamless provides a default page component that is the minimal structure for a w Since we want bootstrap to be included in all of our pages, we will create a new page component that extends the default page component and adds a link to the bootstrap stylesheet. -More information about the default page component can be found `here `_. +More information about the default page component can be found :ref:`here `. .. code-block:: python :caption: Creating a custom page component @@ -82,6 +82,6 @@ That's it! Now you can run the app and access it at `http://localhost:8000/ Date: Mon, 23 Sep 2024 06:10:32 +0300 Subject: [PATCH 13/23] Don't dispatch state change is setting the same value --- seamless/extra/state/state.init.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/seamless/extra/state/state.init.js b/seamless/extra/state/state.init.js index 9a55237..9074b73 100644 --- a/seamless/extra/state/state.init.js +++ b/seamless/extra/state/state.init.js @@ -4,6 +4,9 @@ class SeamlessState { } setState(key, value) { + if (this.state[key] === value) { + return; + } const oldValue = this.state[key]; this.state[key] = value; const stateChangeEvent = new CustomEvent(`stateChange:${key}`, { detail: { oldValue, currentValue: this.state[key] } }); From db7f729358d63acc7c076e72983ba1a5b41244d0 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Mon, 23 Sep 2024 06:11:18 +0300 Subject: [PATCH 14/23] Remove css_properites --- seamless/types/styling/css_properties.py | 281 ----------------------- 1 file changed, 281 deletions(-) delete mode 100644 seamless/types/styling/css_properties.py diff --git a/seamless/types/styling/css_properties.py b/seamless/types/styling/css_properties.py deleted file mode 100644 index 3d2df7c..0000000 --- a/seamless/types/styling/css_properties.py +++ /dev/null @@ -1,281 +0,0 @@ -from typing import Any, Generic, Literal, TypeVar, Union - -from typing_extensions import TypedDict, TypeAlias - -float_ = float -T = TypeVar("T") - -AlignContent: TypeAlias = Literal[ - "flex-start", "flex-end", "center", "space-between", "space-around", "stretch" -] - - -class StyleProperty(Generic[T]): - def __call__(self, value: T) -> Any: ... - - -class CSSProperties(TypedDict, total=False, closed=False): - align_content: AlignContent - align_items: Literal["flex-start", "flex-end", "center", "baseline", "stretch"] - align_self: Union[ - str, Literal["auto", "flex-start", "flex-end", "center", "baseline", "stretch"] - ] - animation: str - animation_delay: str - animation_direction: Union[ - str, Literal["normal", "reverse", "alternate", "alternate-reverse"] - ] - animation_duration: str - animation_fill_mode: Union[str, Literal["none", "forwards", "backwards", "both"]] - animation_iteration_count: Union[str, Literal["infinite", "n"]] - animation_name: str - animation_play_state: Union[str, Literal["running", "paused"]] - animation_timing_function: str - backface_visibility: Union[str, Literal["visible", "hidden"]] - background: str - background_attachment: Union[str, Literal["scroll", "fixed", "local"]] - background_blend_mode: str - background_clip: Union[str, Literal["border-box", "padding-box", "content-box"]] - background_color: str - background_image: str - background_origin: Union[str, Literal["padding-box", "border-box", "content-box"]] - background_position: str - background_repeat: Union[ - str, Literal["repeat", "repeat-x", "repeat-y", "no-repeat", "space", "round"] - ] - background_size: str - border: str - border_bottom: str - border_bottom_color: str - border_bottom_left_radius: str - border_bottom_right_radius: str - border_bottom_style: str - border_bottom_width: str - border_collapse: Union[str, Literal["collapse", "separate"]] - border_color: str - border_image: str - border_image_outset: str - border_image_repeat: Union[str, Literal["stretch", "repeat", "round"]] - border_image_slice: str - border_image_source: str - border_image_width: str - border_left: str - border_left_color: str - border_left_style: str - border_left_width: str - border_radius: str - border_right: str - border_right_color: str - border_right_style: str - border_right_width: str - border_spacing: str - border_style: str - border_top: str - border_top_color: str - border_top_left_radius: str - border_top_right_radius: str - border_top_style: str - border_top_width: str - border_width: str - bottom: str - box_shadow: str - box_sizing: Union[str, Literal["content-box", "border-box"]] - caption_side: Union[str, Literal["top", "bottom"]] - clear: Union[str, Literal["none", "left", "right", "both"]] - clip: str - color: str - column_count: Union[str, int] - column_fill: Union[str, Literal["balance", "auto"]] - column_gap: str - column_rule: str - column_rule_color: str - column_rule_style: str - column_rule_width: str - column_span: Union[str, Literal["none", "all"]] - column_width: Union[str, int] - columns: str - content: str - counter_increment: str - counter_reset: str - cursor: str - direction: Union[str, Literal["ltr", "rtl"]] - display: Union[ - str, - Literal[ - "block", - "inline", - "inline-block", - "flex", - "inline-flex", - "grid", - "inline-grid", - "table", - "table-row", - "table-cell", - "none", - ], - ] - empty_cells: Union[str, Literal["show", "hide"]] - filter: str - flex: str - flex_basis: str - flex_direction: Union[str, Literal["row", "row-reverse", "column", "column-reverse"]] - flex_flow: str - flex_grow: str - flex_shrink: str - flex_wrap: Union[str, Literal["nowrap", "wrap", "wrap-reverse"]] - float: Union[str, Literal["left", "right", "none"]] - font: str - font_family: str - font_feature_settings: str - font_kerning: Union[str, Literal["auto", "normal", "none"]] - font_language_override: str - font_size: str - font_size_adjust: Union[str, Literal["none"]] - font_stretch: str - font_style: Union[str, Literal["normal", "italic", "oblique"]] - font_synthesis: str - font_variant: str - font_variant_alternates: str - font_variant_caps: Union[str, Literal["normal", "small-caps"]] - font_variant_east_asian: str - font_variant_ligatures: str - font_variant_numeric: str - font_variant_position: Union[str, Literal["normal", "sub", "super"]] - font_weight: Union[ - str, - Literal[ - "normal", - "bold", - "bolder", - "lighter", - "100", - "200", - "300", - "400", - "500", - "600", - "700", - "800", - "900", - ], - ] - grid: str - grid_area: str - grid_auto_columns: str - grid_auto_flow: str - grid_auto_rows: str - grid_column: str - grid_column_end: str - grid_column_gap: str - grid_column_start: str - grid_gap: str - grid_row: str - grid_row_end: str - grid_row_gap: str - grid_row_start: str - grid_template: str - grid_template_areas: str - grid_template_columns: str - grid_template_rows: str - height: str - hyphens: Union[str, Literal["none", "manual", "auto"]] - image_rendering: str - isolation: Union[str, Literal["auto", "isolate"]] - justify_content: Union[ - str, - Literal[ - "flex-start", - "flex-end", - "center", - "space-between", - "space-around", - "space-evenly", - ], - ] - left: str - letter_spacing: str - line_break: Union[str, Literal["auto", "loose", "normal", "strict"]] - line_height: Union[str, int] - list_style: str - list_style_image: str - list_style_position: Union[str, Literal["inside", "outside"]] - list_style_type: str - margin: str - margin_bottom: str - margin_left: str - margin_right: str - margin_top: str - max_height: str - max_width: str - min_height: str - min_width: str - mix_blend_mode: str - object_fit: Union[str, Literal["fill", "contain", "cover", "none", "scale-down"]] - object_position: str - opacity: Union[str, float_] - order: Union[str, int] - outline: str - outline_color: str - outline_offset: str - outline_style: str - outline_width: str - overflow: Union[str, Literal["auto", "hidden", "scroll", "visible"]] - overflow_wrap: Union[str, Literal["normal", "break-word", "anywhere"]] - overflow_x: Union[str, Literal["auto", "hidden", "scroll", "visible"]] - overflow_y: Union[str, Literal["auto", "hidden", "scroll", "visible"]] - padding: str - padding_bottom: str - padding_left: str - padding_right: str - padding_top: str - page_break_after: Union[str, Literal["auto", "always", "avoid", "left", "right"]] - page_break_before: Union[str, Literal["auto", "always", "avoid", "left", "right"]] - page_break_inside: Union[str, Literal["auto", "avoid"]] - perspective: str - perspective_origin: str - position: Union[str, Literal["static", "relative", "absolute", "fixed", "sticky"]] - quotes: str - resize: Union[str, Literal["none", "both", "horizontal", "vertical"]] - right: str - scroll_behavior: Union[str, Literal["auto", "smooth"]] - tab_size: Union[str, int] - table_layout: Union[str, Literal["auto", "fixed"]] - text_align: Union[str, Literal["left", "right", "center", "justify", "start", "end"]] - text_align_last: Union[str, Literal["auto", "left", "right", "center", "justify", "start", "end"]] - text_decoration: str - text_decoration_color: str - text_decoration_line: str - text_decoration_style: str - text_indent: str - text_justify: Union[str, Literal["auto", "inter-word", "inter-character", "none"]] - text_overflow: Union[str, Literal["clip", "ellipsis"]] - text_shadow: str - text_transform: Union[str, Literal["none", "capitalize", "uppercase", "lowercase", "full-width"]] - text_underline_position: str - top: str - transform: str - transform_origin: str - transform_style: Union[str, Literal["flat", "preserve-3d"]] - transition: str - transition_delay: str - transition_duration: str - transition_property: str - transition_timing_function: str - unicode_bidi: Union[str, Literal["normal", "embed", "isolate", "bidi-override"]] - user_select: Union[str, Literal["auto", "text", "none", "contain", "all"]] - vertical_align: str - visibility: Union[str, Literal["visible", "hidden", "collapse"]] - white_space: Union[str, Literal["normal", "nowrap", "pre", "pre-line", "pre-wrap"]] - widows: Union[str, int] - width: str - will_change: str - word_break: Union[str, Literal["normal", "break-all", "keep-all", "break-word"]] - word_spacing: str - writing_mode: Union[ - str, - Literal[ - "horizontal-tb", "vertical-rl", "vertical-lr", "sideways-rl", "sideways-lr" - ], - ] - z_index: Union[str, int] From d9ea3cffe7aefad4758b8a6073e322300775416a Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Mon, 23 Sep 2024 06:14:04 +0300 Subject: [PATCH 15/23] Add check if client id exists --- seamless/extra/transports/socketio/transport.py | 17 ++++++++++++----- seamless/extra/transports/transport.py | 16 +++++++++++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/seamless/extra/transports/socketio/transport.py b/seamless/extra/transports/socketio/transport.py index 3aea5ca..7e938ff 100644 --- a/seamless/extra/transports/socketio/transport.py +++ b/seamless/extra/transports/socketio/transport.py @@ -30,6 +30,12 @@ async def on_connect(self, sid, env): try: query = parse_qs(env.get("QUERY_STRING", "")) client_id = query.get("client_id", [None])[0] + + try: + TransportFeature.claim_client_id(client_id) + except KeyError: + raise TransportConnectionRefused("Client ID does not exist") + await self.connect(client_id, env) await self.server.save_session(sid, {"client_id": client_id}) except TransportConnectionRefused: @@ -45,7 +51,7 @@ async def on_disconnect(self, sid): client_id = await self._client_id(sid) await self.disconnect(client_id) - async def _client_id(self, sid): + async def _client_id(self, sid) -> str: session = await self.server.get_session(sid) return session["client_id"] @@ -54,10 +60,11 @@ def init(config=None): init_js = JS(file=HERE / "socketio.init.js") class InitSocketIO(Component, inject_render=True): - def render(self, render_state: RenderState): - client_id = render_state.custom_data.setdefault( - "transports.client_id", random_string(24) - ) + def render(self, render_state: RenderState, context: Context): + transport = context.get_feature(SocketIOTransport) + client_id = transport.create_client_id() + + render_state.custom_data["transports.client_id"] = client_id socket_options = config or {} socket_options.setdefault("query", {})["client_id"] = client_id diff --git a/seamless/extra/transports/transport.py b/seamless/extra/transports/transport.py index 6430313..6bf89c0 100644 --- a/seamless/extra/transports/transport.py +++ b/seamless/extra/transports/transport.py @@ -1,6 +1,7 @@ -from typing import Any +from typing import Any, Set from pydom.context import Context +from pydom.utils.functions import random_string from ..feature import Feature from .dispatcher import dispatcher @@ -30,3 +31,16 @@ def error(error: Exception) -> None: @staticmethod def event(client_id: str, /, *args: Any) -> Any: pass + + _client_ids: Set[str] = set() + + @staticmethod + def create_client_id() -> str: + client_id = random_string(24) + TransportFeature._client_ids.add(client_id) + return client_id + + @staticmethod + def claim_client_id(client_id: str): + TransportFeature._client_ids.remove(client_id) + \ No newline at end of file From d299787e14e5fbbd50e6280cb10ed16f2e16db19 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Mon, 23 Sep 2024 06:14:33 +0300 Subject: [PATCH 16/23] Add imports in extra.transports modules --- seamless/extra/transports/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 seamless/extra/transports/__init__.py diff --git a/seamless/extra/transports/__init__.py b/seamless/extra/transports/__init__.py new file mode 100644 index 0000000..070e532 --- /dev/null +++ b/seamless/extra/transports/__init__.py @@ -0,0 +1,4 @@ +from .client_id import ClientID +from .transport import TransportFeature + +__all__ = ["ClientID", "TransportFeature"] From 2269283b23faad18a440c453498fcbbde40bfe6c Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Mon, 23 Sep 2024 22:56:39 +0300 Subject: [PATCH 17/23] Update docs conf.py path --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 1243c12..b7f02d8 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,7 +6,7 @@ build: python: "3.12" sphinx: - configuration: docs/conf.py + configuration: docs/source/conf.py python: install: From 7023203aa9c74dc4992dcbb883f934fd7fc7dd89 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Mon, 23 Sep 2024 23:01:16 +0300 Subject: [PATCH 18/23] Update .readthedocs.yaml --- .readthedocs.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b7f02d8..74daac1 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,4 +10,6 @@ sphinx: python: install: - - requirements: docs/requirements.txt \ No newline at end of file + - requirements: docs/requirements.txt + - method: pip + path: . From af43715624040f5c5930824427b232eac35c278b Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Fri, 25 Oct 2024 06:46:32 +0300 Subject: [PATCH 19/23] Update docs --- .readthedocs.yaml | 2 +- .vscode/settings.json | 10 +- docs/{source => }/_static/css/custom.css | 0 docs/{source => }/_static/images/favicon.ico | Bin docs/{source => }/_static/images/favicon.png | Bin docs/{source => }/_static/images/favicon.svg | 0 .../_static/images/quick-start.jpeg | Bin docs/{source => }/_static/images/seamless.svg | 0 docs/api-reference/index.rst | 11 ++ docs/{source => }/conf.py | 21 +++- docs/{source => }/index.rst | 0 docs/package.json | 4 +- .../api-reference/1-components/1-base.rst | 26 ----- .../api-reference/1-components/index.rst | 10 -- .../api-reference/1-components/page.rst | 34 ------ .../1-components/router/index.rst | 11 -- .../1-components/router/route.rst | 20 ---- .../1-components/router/router-link.rst | 20 ---- .../1-components/router/router.rst | 22 ---- docs/source/api-reference/2-state/index.rst | 11 -- docs/source/api-reference/3-core/index.rst | 12 -- .../source/api-reference/3-core/rendering.rst | 6 - docs/source/api-reference/99-misc/index.rst | 25 ----- docs/source/api-reference/index.rst | 13 --- .../user-guide/1-basics/1-syntax.rst | 0 .../1-basics/2-rendering-components.rst | 0 .../user-guide/1-basics/3-dynamic-pages.rst | 0 .../user-guide/1-basics/4-state.rst | 0 .../user-guide/1-basics/index.rst | 0 .../2-components/1-base-component.rst | 0 .../user-guide/2-components/2-page.rst | 0 .../user-guide/2-components/3-fragment.rst | 0 .../user-guide/2-components/4-router.rst | 0 .../user-guide/2-components/index.rst | 0 .../user-guide/3-events/1-transports.rst | 0 .../user-guide/3-events/2-data-validation.rst | 0 .../user-guide/3-events/index.rst | 0 .../user-guide/4-styling/1-style-object.rst | 0 .../user-guide/4-styling/2-css-modules.rst | 0 .../user-guide/4-styling/index.rst | 0 .../user-guide/5-advanced/1-javascript.rst | 0 .../user-guide/5-advanced/2-empty.rst | 0 .../user-guide/5-advanced/3-context.rst | 0 .../user-guide/5-advanced/4-transformers.rst | 0 .../user-guide/5-advanced/5-rendering.rst | 0 .../user-guide/5-advanced/6-transports.rst | 0 .../user-guide/5-advanced/index.rst | 0 docs/{source => }/user-guide/index.rst | 0 docs/{source => }/user-guide/quick-start.rst | 0 examples/spa/components/app.py | 17 +-- examples/spa/components/usage.py | 2 +- examples/spa/pages/counter.py | 7 +- node/packages/core/package.json | 2 +- node/packages/core/src/renderer.ts | 104 ++++++++++++++++++ seamless/context/context.py | 11 +- seamless/core/javascript.py | 20 +++- seamless/extra/transformers/__init__.py | 0 seamless/extra/transports/dispatcher.py | 18 +-- .../extra/transports/socketio/__init__.py | 4 + .../extra/transports/socketio/middleware.py | 7 +- seamless/extra/transports/subscriptable.py | 14 +-- seamless/internal/constants.py | 3 +- seamless/internal/injector.py | 4 +- seamless/styling/style.py | 12 +- seamless/types/events.py | 6 +- seamless/types/html/html_event_props.py | 2 +- 66 files changed, 220 insertions(+), 271 deletions(-) rename docs/{source => }/_static/css/custom.css (100%) rename docs/{source => }/_static/images/favicon.ico (100%) rename docs/{source => }/_static/images/favicon.png (100%) rename docs/{source => }/_static/images/favicon.svg (100%) rename docs/{source => }/_static/images/quick-start.jpeg (100%) rename docs/{source => }/_static/images/seamless.svg (100%) create mode 100644 docs/api-reference/index.rst rename docs/{source => }/conf.py (75%) rename docs/{source => }/index.rst (100%) delete mode 100644 docs/source/api-reference/1-components/1-base.rst delete mode 100644 docs/source/api-reference/1-components/index.rst delete mode 100644 docs/source/api-reference/1-components/page.rst delete mode 100644 docs/source/api-reference/1-components/router/index.rst delete mode 100644 docs/source/api-reference/1-components/router/route.rst delete mode 100644 docs/source/api-reference/1-components/router/router-link.rst delete mode 100644 docs/source/api-reference/1-components/router/router.rst delete mode 100644 docs/source/api-reference/2-state/index.rst delete mode 100644 docs/source/api-reference/3-core/index.rst delete mode 100644 docs/source/api-reference/3-core/rendering.rst delete mode 100644 docs/source/api-reference/99-misc/index.rst delete mode 100644 docs/source/api-reference/index.rst rename docs/{source => }/user-guide/1-basics/1-syntax.rst (100%) rename docs/{source => }/user-guide/1-basics/2-rendering-components.rst (100%) rename docs/{source => }/user-guide/1-basics/3-dynamic-pages.rst (100%) rename docs/{source => }/user-guide/1-basics/4-state.rst (100%) rename docs/{source => }/user-guide/1-basics/index.rst (100%) rename docs/{source => }/user-guide/2-components/1-base-component.rst (100%) rename docs/{source => }/user-guide/2-components/2-page.rst (100%) rename docs/{source => }/user-guide/2-components/3-fragment.rst (100%) rename docs/{source => }/user-guide/2-components/4-router.rst (100%) rename docs/{source => }/user-guide/2-components/index.rst (100%) rename docs/{source => }/user-guide/3-events/1-transports.rst (100%) rename docs/{source => }/user-guide/3-events/2-data-validation.rst (100%) rename docs/{source => }/user-guide/3-events/index.rst (100%) rename docs/{source => }/user-guide/4-styling/1-style-object.rst (100%) rename docs/{source => }/user-guide/4-styling/2-css-modules.rst (100%) rename docs/{source => }/user-guide/4-styling/index.rst (100%) rename docs/{source => }/user-guide/5-advanced/1-javascript.rst (100%) rename docs/{source => }/user-guide/5-advanced/2-empty.rst (100%) rename docs/{source => }/user-guide/5-advanced/3-context.rst (100%) rename docs/{source => }/user-guide/5-advanced/4-transformers.rst (100%) rename docs/{source => }/user-guide/5-advanced/5-rendering.rst (100%) rename docs/{source => }/user-guide/5-advanced/6-transports.rst (100%) rename docs/{source => }/user-guide/5-advanced/index.rst (100%) rename docs/{source => }/user-guide/index.rst (100%) rename docs/{source => }/user-guide/quick-start.rst (100%) create mode 100644 node/packages/core/src/renderer.ts create mode 100644 seamless/extra/transformers/__init__.py create mode 100644 seamless/extra/transports/socketio/__init__.py diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 74daac1..72e839d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,7 +6,7 @@ build: python: "3.12" sphinx: - configuration: docs/source/conf.py + configuration: docs/conf.py python: install: diff --git a/.vscode/settings.json b/.vscode/settings.json index 5a20232..e82f7cc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { - "cSpell.words": [ - "Renderable" - ] -} \ No newline at end of file + "cSpell.words": ["pydom", "Renderable"], + "python.analysis.diagnosticSeverityOverrides": { + "reportWildcardImportFromLibrary": "none" + }, + "python.analysis.extraPaths": ["../pydom"] +} diff --git a/docs/source/_static/css/custom.css b/docs/_static/css/custom.css similarity index 100% rename from docs/source/_static/css/custom.css rename to docs/_static/css/custom.css diff --git a/docs/source/_static/images/favicon.ico b/docs/_static/images/favicon.ico similarity index 100% rename from docs/source/_static/images/favicon.ico rename to docs/_static/images/favicon.ico diff --git a/docs/source/_static/images/favicon.png b/docs/_static/images/favicon.png similarity index 100% rename from docs/source/_static/images/favicon.png rename to docs/_static/images/favicon.png diff --git a/docs/source/_static/images/favicon.svg b/docs/_static/images/favicon.svg similarity index 100% rename from docs/source/_static/images/favicon.svg rename to docs/_static/images/favicon.svg diff --git a/docs/source/_static/images/quick-start.jpeg b/docs/_static/images/quick-start.jpeg similarity index 100% rename from docs/source/_static/images/quick-start.jpeg rename to docs/_static/images/quick-start.jpeg diff --git a/docs/source/_static/images/seamless.svg b/docs/_static/images/seamless.svg similarity index 100% rename from docs/source/_static/images/seamless.svg rename to docs/_static/images/seamless.svg diff --git a/docs/api-reference/index.rst b/docs/api-reference/index.rst new file mode 100644 index 0000000..c1abcb2 --- /dev/null +++ b/docs/api-reference/index.rst @@ -0,0 +1,11 @@ +API Reference +============= + +This page contains auto-generated API reference documentation [#f1]_. + +.. toctree:: + :titlesonly: + + /api-reference/seamless/index + +.. [#f1] Created with `sphinx-autoapi `_ \ No newline at end of file diff --git a/docs/source/conf.py b/docs/conf.py similarity index 75% rename from docs/source/conf.py rename to docs/conf.py index d067050..d8cbf3f 100644 --- a/docs/source/conf.py +++ b/docs/conf.py @@ -1,6 +1,6 @@ import sys -sys.path.insert(0, "../..") +sys.path.insert(0, "..") from seamless import __version__ # Configuration file for the Sphinx documentation builder. @@ -22,11 +22,26 @@ extensions = [ "pydata_sphinx_theme", "sphinx_substitution_extensions", - "sphinx.ext.autodoc", + "autoapi.extension", ] templates_path = ["_templates"] -exclude_patterns = [] +exclude_patterns = ["_build", "_templates", "Thumbs.db", ".DS_Store"] + +autoapi_dirs = ["../seamless"] +autoapi_ignore = ["*/internal/*"] +autoapi_options = [ + "members", + "undoc-members", + "show-inheritance", + "show-module-summary", + "special-members", + "imported-members", +] +autoapi_root = "api-reference" +autoapi_keep_files = True +autoapi_generate_api_docs = True +autoapi_add_toctree_entry = True # -- Options for HTML output ------------------------------------------------- diff --git a/docs/source/index.rst b/docs/index.rst similarity index 100% rename from docs/source/index.rst rename to docs/index.rst diff --git a/docs/package.json b/docs/package.json index 1298b1c..aa66f8c 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "scripts": { - "dev": "sphinx-autobuild -b html source build --host 0.0.0.0 --port 8080", - "dev:clean": "sphinx-autobuild -a -b html source build --host 0.0.0.0 --port 8080" + "dev": "sphinx-autobuild -b html . _build --host 0.0.0.0 --port 8080", + "dev:clean": "sphinx-autobuild -a -b html . _build --host 0.0.0.0 --port 8080" } } \ No newline at end of file diff --git a/docs/source/api-reference/1-components/1-base.rst b/docs/source/api-reference/1-components/1-base.rst deleted file mode 100644 index 4a1209d..0000000 --- a/docs/source/api-reference/1-components/1-base.rst +++ /dev/null @@ -1,26 +0,0 @@ -.. _base-component-api-reference: - -######### -Component -######### - -.. py:class:: Component - :module: seamless - :canonical: seamless.core.component.Component - - The base class for all components. - - .. py:method:: __init__(*children: ChildType, **props: Any) - - :param children: The children of the component - :type children: :py:type:`~seamless.types.ChildrenType` - :param props: The props of the component - :type props: :py:type:`~seamless.types.PropsType` - - .. py:method:: render(self) -> RenderResult - :abstractmethod: - - Renders the component. |br| - This method is an abstract method that must be implemented by the subclass. - - :rtype: :py:type:`~seamless.types.RenderResult` \ No newline at end of file diff --git a/docs/source/api-reference/1-components/index.rst b/docs/source/api-reference/1-components/index.rst deleted file mode 100644 index 05441fb..0000000 --- a/docs/source/api-reference/1-components/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -########## -Components -########## - -.. toctree:: - :glob: - :maxdepth: 1 - - * - */index \ No newline at end of file diff --git a/docs/source/api-reference/1-components/page.rst b/docs/source/api-reference/1-components/page.rst deleted file mode 100644 index eb72aeb..0000000 --- a/docs/source/api-reference/1-components/page.rst +++ /dev/null @@ -1,34 +0,0 @@ -.. _page-api-reference: - -#### -Page -#### - -.. py:currentmodule:: seamless.components - -.. py:class:: Page - :canonical: seamless.components.page.Page - - Inherits from :py:class:`~seamless.core.component.Component`. - - Methods - ======= - - .. py:method:: __init__(self, title=None, html_props=None, head_props=None, body_props=None) - - :param string title: The page's title - :param dict html_props: The props to insert inside the ``html`` tag |br| **default**: ``{ "lang": "en" }`` - :param dict head_props: The props to insert inside the ``head`` tag |br| **default**: ``{}`` - :param dict body_props: The props to insert inside the ``body`` tag |br| **default**: ``{ "dir": "ltr" }`` - - .. py:method:: head(self) -> Iterable[ChildType] - - The children that will be inside the ``head`` tag. - - :rtype: :py:type:`~seamless.types.ChildrenType` - - .. py:method:: body(self) -> Iterable[ChildType] - - The children that will be inside the ``body`` tag. - - :rtype: :py:type:`~seamless.types.ChildrenType` \ No newline at end of file diff --git a/docs/source/api-reference/1-components/router/index.rst b/docs/source/api-reference/1-components/router/index.rst deleted file mode 100644 index 241f592..0000000 --- a/docs/source/api-reference/1-components/router/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -###### -Router -###### - -.. py:module:: seamless.components.router - -.. toctree:: - :glob: - :maxdepth: 1 - - * \ No newline at end of file diff --git a/docs/source/api-reference/1-components/router/route.rst b/docs/source/api-reference/1-components/router/route.rst deleted file mode 100644 index bf5a214..0000000 --- a/docs/source/api-reference/1-components/router/route.rst +++ /dev/null @@ -1,20 +0,0 @@ -.. _route-api-reference: - -##### -Route -##### - -.. py:currentmodule:: seamless.components.router - -.. py:class:: Route - :canonical: seamless.components.router.route.Route - - Inherits from :py:class:`~seamless.core.component.Component`. - - Methods - ======= - - .. py:method:: __init__(self, path: str, component: type[Component]) - - :param string path: The path of the page - :param ``Component`` component: The component to render when the path is matched diff --git a/docs/source/api-reference/1-components/router/router-link.rst b/docs/source/api-reference/1-components/router/router-link.rst deleted file mode 100644 index db09700..0000000 --- a/docs/source/api-reference/1-components/router/router-link.rst +++ /dev/null @@ -1,20 +0,0 @@ -.. _router-link-api-reference: - -########### -Router Link -########### - -.. py:currentmodule:: seamless.components.router - -.. py:class:: RouterLink - :canonical: seamless.components.router.router_link.RouterLink - - Inherits from :py:class:`~seamless.core.component.Component`. - - Methods - ======= - - .. py:method:: __init__(self, *, to: str, **anchor_props) - - :param string to: The path to navigate to - :param dict anchor_props: The properties to pass to the anchor element diff --git a/docs/source/api-reference/1-components/router/router.rst b/docs/source/api-reference/1-components/router/router.rst deleted file mode 100644 index dada75e..0000000 --- a/docs/source/api-reference/1-components/router/router.rst +++ /dev/null @@ -1,22 +0,0 @@ -.. _router-api-reference: - -###### -Router -###### - -.. py:currentmodule:: seamless.components.router - -.. py:class:: Router - :canonical: seamless.components.router.router.Router - - Inherits from :py:class:`~seamless.core.component.Component`. - - Methods - ======= - - .. py:method:: __init__(self, *, loading_component: type[Component] | None = None) - .. py:method:: __init__(self, *routes: Route, loading_component: type[Component] | None = None) - :no-index: - - :param ``Component`` loading_component: The component to show between components loading - :param ``Route`` routes: The routes to include in the application - can also passed as children of the Router component diff --git a/docs/source/api-reference/2-state/index.rst b/docs/source/api-reference/2-state/index.rst deleted file mode 100644 index 2bdcbaa..0000000 --- a/docs/source/api-reference/2-state/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -.. _state-api-reference: - -##### -State -##### - -.. toctree:: - :glob: - - * - */index \ No newline at end of file diff --git a/docs/source/api-reference/3-core/index.rst b/docs/source/api-reference/3-core/index.rst deleted file mode 100644 index 4b4ebeb..0000000 --- a/docs/source/api-reference/3-core/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -#### -Core -#### - -.. py:module:: seamless.core - -.. toctree:: - :glob: - :maxdepth: 1 - - * - */index \ No newline at end of file diff --git a/docs/source/api-reference/3-core/rendering.rst b/docs/source/api-reference/3-core/rendering.rst deleted file mode 100644 index d7bbfc2..0000000 --- a/docs/source/api-reference/3-core/rendering.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _rendering-api-reference: - -######### -Rendering -######### - diff --git a/docs/source/api-reference/99-misc/index.rst b/docs/source/api-reference/99-misc/index.rst deleted file mode 100644 index 2478815..0000000 --- a/docs/source/api-reference/99-misc/index.rst +++ /dev/null @@ -1,25 +0,0 @@ -.. _misc: - -############# -Miscellaneous -############# - -.. py:type:: Primitive - :module: seamless.types - :canonical: Union[str, int, float, bool, None] - -.. py:type:: Renderable - :module: seamless.types - :canonical: Union[Component, Element] - -.. py:type:: ChildType - :module: seamless.types - :canonical: Union[Renderable, Primitive] - -.. py:type:: ChildrenType - :module: seamless.types - :canonical: Collection[ChildType] - -.. py:type:: RenderResult - :module: seamless.types - :canonical: Renderable | Primitive diff --git a/docs/source/api-reference/index.rst b/docs/source/api-reference/index.rst deleted file mode 100644 index 6211d48..0000000 --- a/docs/source/api-reference/index.rst +++ /dev/null @@ -1,13 +0,0 @@ -.. _api-reference: - -############# -API Reference -############# - -This section contains the API reference for Seamless. - -.. autosummary:: - :toctree: _autosummary - :recursive: - - seamless \ No newline at end of file diff --git a/docs/source/user-guide/1-basics/1-syntax.rst b/docs/user-guide/1-basics/1-syntax.rst similarity index 100% rename from docs/source/user-guide/1-basics/1-syntax.rst rename to docs/user-guide/1-basics/1-syntax.rst diff --git a/docs/source/user-guide/1-basics/2-rendering-components.rst b/docs/user-guide/1-basics/2-rendering-components.rst similarity index 100% rename from docs/source/user-guide/1-basics/2-rendering-components.rst rename to docs/user-guide/1-basics/2-rendering-components.rst diff --git a/docs/source/user-guide/1-basics/3-dynamic-pages.rst b/docs/user-guide/1-basics/3-dynamic-pages.rst similarity index 100% rename from docs/source/user-guide/1-basics/3-dynamic-pages.rst rename to docs/user-guide/1-basics/3-dynamic-pages.rst diff --git a/docs/source/user-guide/1-basics/4-state.rst b/docs/user-guide/1-basics/4-state.rst similarity index 100% rename from docs/source/user-guide/1-basics/4-state.rst rename to docs/user-guide/1-basics/4-state.rst diff --git a/docs/source/user-guide/1-basics/index.rst b/docs/user-guide/1-basics/index.rst similarity index 100% rename from docs/source/user-guide/1-basics/index.rst rename to docs/user-guide/1-basics/index.rst diff --git a/docs/source/user-guide/2-components/1-base-component.rst b/docs/user-guide/2-components/1-base-component.rst similarity index 100% rename from docs/source/user-guide/2-components/1-base-component.rst rename to docs/user-guide/2-components/1-base-component.rst diff --git a/docs/source/user-guide/2-components/2-page.rst b/docs/user-guide/2-components/2-page.rst similarity index 100% rename from docs/source/user-guide/2-components/2-page.rst rename to docs/user-guide/2-components/2-page.rst diff --git a/docs/source/user-guide/2-components/3-fragment.rst b/docs/user-guide/2-components/3-fragment.rst similarity index 100% rename from docs/source/user-guide/2-components/3-fragment.rst rename to docs/user-guide/2-components/3-fragment.rst diff --git a/docs/source/user-guide/2-components/4-router.rst b/docs/user-guide/2-components/4-router.rst similarity index 100% rename from docs/source/user-guide/2-components/4-router.rst rename to docs/user-guide/2-components/4-router.rst diff --git a/docs/source/user-guide/2-components/index.rst b/docs/user-guide/2-components/index.rst similarity index 100% rename from docs/source/user-guide/2-components/index.rst rename to docs/user-guide/2-components/index.rst diff --git a/docs/source/user-guide/3-events/1-transports.rst b/docs/user-guide/3-events/1-transports.rst similarity index 100% rename from docs/source/user-guide/3-events/1-transports.rst rename to docs/user-guide/3-events/1-transports.rst diff --git a/docs/source/user-guide/3-events/2-data-validation.rst b/docs/user-guide/3-events/2-data-validation.rst similarity index 100% rename from docs/source/user-guide/3-events/2-data-validation.rst rename to docs/user-guide/3-events/2-data-validation.rst diff --git a/docs/source/user-guide/3-events/index.rst b/docs/user-guide/3-events/index.rst similarity index 100% rename from docs/source/user-guide/3-events/index.rst rename to docs/user-guide/3-events/index.rst diff --git a/docs/source/user-guide/4-styling/1-style-object.rst b/docs/user-guide/4-styling/1-style-object.rst similarity index 100% rename from docs/source/user-guide/4-styling/1-style-object.rst rename to docs/user-guide/4-styling/1-style-object.rst diff --git a/docs/source/user-guide/4-styling/2-css-modules.rst b/docs/user-guide/4-styling/2-css-modules.rst similarity index 100% rename from docs/source/user-guide/4-styling/2-css-modules.rst rename to docs/user-guide/4-styling/2-css-modules.rst diff --git a/docs/source/user-guide/4-styling/index.rst b/docs/user-guide/4-styling/index.rst similarity index 100% rename from docs/source/user-guide/4-styling/index.rst rename to docs/user-guide/4-styling/index.rst diff --git a/docs/source/user-guide/5-advanced/1-javascript.rst b/docs/user-guide/5-advanced/1-javascript.rst similarity index 100% rename from docs/source/user-guide/5-advanced/1-javascript.rst rename to docs/user-guide/5-advanced/1-javascript.rst diff --git a/docs/source/user-guide/5-advanced/2-empty.rst b/docs/user-guide/5-advanced/2-empty.rst similarity index 100% rename from docs/source/user-guide/5-advanced/2-empty.rst rename to docs/user-guide/5-advanced/2-empty.rst diff --git a/docs/source/user-guide/5-advanced/3-context.rst b/docs/user-guide/5-advanced/3-context.rst similarity index 100% rename from docs/source/user-guide/5-advanced/3-context.rst rename to docs/user-guide/5-advanced/3-context.rst diff --git a/docs/source/user-guide/5-advanced/4-transformers.rst b/docs/user-guide/5-advanced/4-transformers.rst similarity index 100% rename from docs/source/user-guide/5-advanced/4-transformers.rst rename to docs/user-guide/5-advanced/4-transformers.rst diff --git a/docs/source/user-guide/5-advanced/5-rendering.rst b/docs/user-guide/5-advanced/5-rendering.rst similarity index 100% rename from docs/source/user-guide/5-advanced/5-rendering.rst rename to docs/user-guide/5-advanced/5-rendering.rst diff --git a/docs/source/user-guide/5-advanced/6-transports.rst b/docs/user-guide/5-advanced/6-transports.rst similarity index 100% rename from docs/source/user-guide/5-advanced/6-transports.rst rename to docs/user-guide/5-advanced/6-transports.rst diff --git a/docs/source/user-guide/5-advanced/index.rst b/docs/user-guide/5-advanced/index.rst similarity index 100% rename from docs/source/user-guide/5-advanced/index.rst rename to docs/user-guide/5-advanced/index.rst diff --git a/docs/source/user-guide/index.rst b/docs/user-guide/index.rst similarity index 100% rename from docs/source/user-guide/index.rst rename to docs/user-guide/index.rst diff --git a/docs/source/user-guide/quick-start.rst b/docs/user-guide/quick-start.rst similarity index 100% rename from docs/source/user-guide/quick-start.rst rename to docs/user-guide/quick-start.rst diff --git a/examples/spa/components/app.py b/examples/spa/components/app.py index e590022..2a77bb0 100644 --- a/examples/spa/components/app.py +++ b/examples/spa/components/app.py @@ -1,5 +1,4 @@ -from seamless import Component, Div, Nav, Button -from seamless.context.context import Context +from seamless import Component, Div, Nav, Button, Context from seamless.styling import StyleObject from seamless.extensions import State, SocketIOTransport from seamless.components.router import Router, Route, RouterLink @@ -21,10 +20,13 @@ def render(self): RouterLink(to="/usage", class_name="navbar-brand")("Usage"), ), Div( - Button(on_click=foo, style=StyleObject(border_radius="5px", background_color="red"))( - "Click me!" - ) - ) + Button( + on_click=foo, + style=StyleObject( + border_radius="5px", background_color="red" + ), + )("Click me!") + ), ), Div(class_name="content flex-grow-1")( Router(loading_component=Loading)( @@ -38,5 +40,6 @@ def render(self): title="Seamless", ) + def foo(event, context: Context): - print("foo") \ No newline at end of file + print("foo") diff --git a/examples/spa/components/usage.py b/examples/spa/components/usage.py index 8599455..577885f 100644 --- a/examples/spa/components/usage.py +++ b/examples/spa/components/usage.py @@ -36,7 +36,7 @@ def render(self, context: Context): events = context.get_feature(EventsFeature) total_scoped = sum(len(scope) for scope in events.DB.scoped_events.values()) - return Div( + return Div(class_name="p-4")( Details( Summary( Span(class_name="h2")( diff --git a/examples/spa/pages/counter.py b/examples/spa/pages/counter.py index a5d31b6..01eb811 100644 --- a/examples/spa/pages/counter.py +++ b/examples/spa/pages/counter.py @@ -1,3 +1,4 @@ +from typing import TypedDict from pydantic import BaseModel from seamless import Div, Form, Input, Button, Component from seamless.extra.transports.client_id import ClientID @@ -5,7 +6,7 @@ from seamless.types.events import SubmitEvent -class Counter(BaseModel): +class Counter(TypedDict): counter_value: int @@ -50,10 +51,10 @@ def render(self): ), ) - def submit(self, event: SubmitEvent[Counter], sid: ClientID): + def submit(self, event: SubmitEvent, sid: ClientID): print("Form submitted!") print("socket id:", sid) - print(event) + print(event.data["counter_value"]) def global_submit(event, sid: ClientID): diff --git a/node/packages/core/package.json b/node/packages/core/package.json index 8937db9..a6dbce9 100644 --- a/node/packages/core/package.json +++ b/node/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "python-seamless", - "version": "0.1.2", + "version": "0.9.2", "description": "JavaScript integration for Seamless", "main": "dist/index.js", "private": false, diff --git a/node/packages/core/src/renderer.ts b/node/packages/core/src/renderer.ts new file mode 100644 index 0000000..688ef0d --- /dev/null +++ b/node/packages/core/src/renderer.ts @@ -0,0 +1,104 @@ +import { SeamlessOptions, Primitive, SeamlessElement } from "./types"; +import { SEAMLESS_ELEMENT, SEAMLESS_INIT, SEAMLESS_EMPTY } from "./constants"; + +export class Renderer { + private readonly eventObjectTransformer: ( + originalEvent: Event, + outEvent: any + ) => any; + private readonly context: Record = {}; + + constructor(config?: SeamlessOptions) { + this.eventObjectTransformer = + config?.eventObjectTransformer || ((_, outEvent) => outEvent); + + this.context.instance = this; + this.init(); + } + + init() { + const allSeamlessElements = document.querySelectorAll( + "[seamless\\:element]" + ); + + this.processElements(Array.from(allSeamlessElements)); + } + + processElements(elements: HTMLElement[]) { + elements.forEach((element) => { + if (element.hasAttribute(SEAMLESS_INIT)) { + this.attachInit(element); + } + }); + elements.forEach((element) => { + if (element.tagName.toLowerCase() === SEAMLESS_EMPTY) { + this.initEmpty(element); + } + }); + elements.forEach((element) => { + element.removeAttribute(SEAMLESS_ELEMENT); + }); + } + + render(component: SeamlessElement, parentElement: any): void; + render(component: SeamlessElement, parentElement: HTMLElement): void { + this.toDOMElement(component, parentElement); + } + + private toDOMElement( + element: SeamlessElement | Primitive, + parentElement?: HTMLElement + ): HTMLElement | Text { + if (this.isPrimitive(element)) { + const primitiveNode = document.createTextNode(element?.toString() || ""); + if (parentElement) { + parentElement.appendChild(primitiveNode); + } + return primitiveNode; + } + + const domElement = document.createElement(element.type); + Object.entries(element.props).forEach(([key, value]) => { + domElement.setAttribute(key, value); + }); + + if (parentElement) { + parentElement.appendChild(domElement); + } + + if (Array.isArray(element.children)) { + element.children.map((child) => this.toDOMElement(child, domElement)); + } + + if (domElement.hasAttribute(SEAMLESS_ELEMENT)) { + this.processElements([domElement]); + } + + return domElement; + } + + protected attachInit(element: HTMLElement) { + const initCode = element.getAttribute(SEAMLESS_INIT); + if (initCode) { + new Function("seamless", initCode).apply(element, [this.context]); + element.removeAttribute(SEAMLESS_INIT); + } + } + + protected initEmpty(element: HTMLElement) { + while (element.firstChild) { + element.parentElement?.insertBefore(element.firstChild, element); + } + element.parentElement?.removeChild(element); + } + + protected isPrimitive(value: any): value is Primitive { + return ( + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" || + value === null || + value === undefined + ); + } +} diff --git a/seamless/context/context.py b/seamless/context/context.py index 034ceea..a31f180 100644 --- a/seamless/context/context.py +++ b/seamless/context/context.py @@ -21,13 +21,12 @@ from ..internal.constants import DISABLE_GLOBAL_CONTEXT_ENV from ..internal.injector import Injector -T = TypeVar("T", bound=_Context) -P = ParamSpec("P") +_P = ParamSpec("_P") -Feature = Callable[Concatenate["Context", P], Any] -PropertyMatcher = Union[Callable[Concatenate[str, Any, P], bool], str] -PropertyTransformer = Callable[Concatenate[str, Any, "ContextNode", P], None] -PostRenderTransformer = Callable[Concatenate["ContextNode", P], None] +Feature = Callable[Concatenate["Context", _P], Any] +PropertyMatcher = Union[Callable[Concatenate[str, Any, _P], bool], str] +PropertyTransformer = Callable[Concatenate[str, Any, "ContextNode", _P], None] +PostRenderTransformer = Callable[Concatenate["ContextNode", _P], None] class Context(_Context): diff --git a/seamless/core/javascript.py b/seamless/core/javascript.py index 6830366..3908f56 100644 --- a/seamless/core/javascript.py +++ b/seamless/core/javascript.py @@ -3,10 +3,26 @@ class JavaScript: + """ + A class representing a block of JavaScript code. + """ + @overload - def __init__(self, code: str) -> None: ... + def __init__(self, code: str) -> None: + """ + Create a new JavaScript object from a string of code. + + Args: + code: The JavaScript code. + """ @overload - def __init__(self, *, file: Union[str, PathLike]) -> None: ... + def __init__(self, *, file: Union[str, PathLike]) -> None: + """ + Create a new JavaScript object from a file. + + Args: + file: The path to the file containing the JavaScript code. + """ def __init__(self, code=None, *, file=None) -> None: if file: diff --git a/seamless/extra/transformers/__init__.py b/seamless/extra/transformers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/seamless/extra/transports/dispatcher.py b/seamless/extra/transports/dispatcher.py index dc6a80f..1b12fb0 100644 --- a/seamless/extra/transports/dispatcher.py +++ b/seamless/extra/transports/dispatcher.py @@ -7,35 +7,35 @@ if TYPE_CHECKING: from .transport import TransportFeature -P = ParamSpec("P") -T = TypeVar("T") +_P = ParamSpec("_P") +_T = TypeVar("_T") -class Dispatcher(Generic[P, T]): +class Dispatcher(Generic[_P, _T]): def __init__(self, context: Context) -> None: self._context = context self._handlers: dict[Any, Callable] = {} - def on(self, name: Any, handler: Callable[P, Awaitable[T]]): + def on(self, name: Any, handler: Callable[_P, Awaitable[_T]]): if name in self._handlers: raise ValueError(f"Handler for {name} already exists") self._handlers[name] = self._context.inject(handler) - async def __call__(self, name: Any, *args: P.args, **kwds: P.kwargs) -> Awaitable[T]: + async def __call__(self, name: Any, *args: _P.args, **kwds: _P.kwargs) -> Awaitable[_T]: return await self._handlers[name](*args, **kwds) -class DispatcherDescriptor(Generic[P, T]): - def __get__(self, instance: "TransportFeature", owner) -> Dispatcher[P, T]: +class DispatcherDescriptor(Generic[_P, _T]): + def __get__(self, instance: "TransportFeature", owner) -> Dispatcher[_P, _T]: return instance.__dict__.setdefault(self.name, Dispatcher(instance.context)) - def __set__(self, instance: "TransportFeature", value: Dispatcher[P, T]) -> None: + def __set__(self, instance: "TransportFeature", value: Dispatcher[_P, _T]) -> None: pass def __set_name__(self, owner, name): self.name = name -def dispatcher(_: Callable[P, T]) -> DispatcherDescriptor[P, T]: +def dispatcher(_: Callable[_P, _T]) -> DispatcherDescriptor[_P, _T]: return DispatcherDescriptor() diff --git a/seamless/extra/transports/socketio/__init__.py b/seamless/extra/transports/socketio/__init__.py new file mode 100644 index 0000000..d61cdf3 --- /dev/null +++ b/seamless/extra/transports/socketio/__init__.py @@ -0,0 +1,4 @@ +from .middleware import SocketIOMiddleware +from .transport import SocketIOTransport + +__all__ = ["SocketIOMiddleware", "SocketIOTransport"] diff --git a/seamless/extra/transports/socketio/middleware.py b/seamless/extra/transports/socketio/middleware.py index e396d86..dd6187c 100644 --- a/seamless/extra/transports/socketio/middleware.py +++ b/seamless/extra/transports/socketio/middleware.py @@ -23,4 +23,9 @@ def __init__( self.app = ASGIApp(transport.server, app, socketio_path=self.socket_path) async def __call__(self, scope, receive, send): - await self.app(scope, receive, send) + ... + + def __new__(cls, *args, **kwargs): + instance = super().__new__(cls) + instance.__init__(*args, **kwargs) + return instance.app diff --git a/seamless/extra/transports/subscriptable.py b/seamless/extra/transports/subscriptable.py index 211d084..007e7b0 100644 --- a/seamless/extra/transports/subscriptable.py +++ b/seamless/extra/transports/subscriptable.py @@ -7,15 +7,15 @@ from ...errors import ClientError -P = ParamSpec("P") +_P = ParamSpec("_P") -class Subscriptable(Generic[P]): +class Subscriptable(Generic[_P]): def __init__(self, context: Context) -> None: self._callbacks = [] self._context = context - async def __call__(self, *args: P.args, **kwds: P.kwargs) -> None: + async def __call__(self, *args: _P.args, **kwds: _P.kwargs) -> None: try: for callback in self._callbacks: await callback(*args, **kwds) @@ -30,16 +30,16 @@ def __iadd__(self, callback: Any) -> "Subscriptable": return self -class SubscriptableDescriptor(Generic[P]): - def __get__(self, instance: "Feature", owner) -> Subscriptable[P]: +class SubscriptableDescriptor(Generic[_P]): + def __get__(self, instance: "Feature", owner) -> Subscriptable[_P]: return instance.__dict__.setdefault(self.name, Subscriptable(instance.context)) - def __set__(self, instance: "Feature", value: Subscriptable[P]) -> None: + def __set__(self, instance: "Feature", value: Subscriptable[_P]) -> None: pass def __set_name__(self, owner, name): self.name = name -def event(_: Callable[P, None]) -> SubscriptableDescriptor[P]: +def event(_: Callable[_P, None]) -> SubscriptableDescriptor[_P]: return SubscriptableDescriptor() diff --git a/seamless/internal/constants.py b/seamless/internal/constants.py index d363e45..3b01bee 100644 --- a/seamless/internal/constants.py +++ b/seamless/internal/constants.py @@ -1,4 +1,5 @@ SEAMLESS_ELEMENT_ATTRIBUTE = "seamless:element" SEAMLESS_INIT_ATTRIBUTE = "seamless:init" -DISABLE_GLOBAL_CONTEXT_ENV = "SEAMLESS_DISABLE_GLOBAL_CONTEXT" \ No newline at end of file +DISABLE_GLOBAL_CONTEXT_ENV = "SEAMLESS_DISABLE_GLOBAL_CONTEXT" +DISABLE_VALIDATION_ENV = "SEAMLESS_DISABLE_VALIDATION" diff --git a/seamless/internal/injector.py b/seamless/internal/injector.py index 5b428a4..687caad 100644 --- a/seamless/internal/injector.py +++ b/seamless/internal/injector.py @@ -7,9 +7,9 @@ from pydom.utils.injector import Injector as _Injector -T = TypeVar("T") +_T = TypeVar("_T") -InjectFactory: TypeAlias = Callable[[], T] +InjectFactory: TypeAlias = Callable[[], _T] class Injector(_Injector): diff --git a/seamless/styling/style.py b/seamless/styling/style.py index 7be50a4..1d32902 100644 --- a/seamless/styling/style.py +++ b/seamless/styling/style.py @@ -1,20 +1,18 @@ -from typing import Generic, TypeVar, Union, TYPE_CHECKING +from typing import Generic, TypeVar, Union from typing_extensions import Unpack +from pydom.types.styling.css_properties import CSSProperties -if TYPE_CHECKING: - from ..types.styling.css_properties import CSSProperties - -T = TypeVar("T") +_T = TypeVar("_T") class StyleObject: - class _StyleProperty(Generic[T]): + class _StyleProperty(Generic[_T]): def __init__(self, instance: "StyleObject", name: str): self.instance = instance self.name = name.replace("_", "-") - def __call__(self, value: T): + def __call__(self, value: _T): self.instance.style[self.name] = value return self.instance diff --git a/seamless/types/events.py b/seamless/types/events.py index 4357034..17d288d 100644 --- a/seamless/types/events.py +++ b/seamless/types/events.py @@ -9,7 +9,7 @@ class BaseModel: ... -T = TypeVar("T") +_T = TypeVar("_T") # region: Event Types @@ -131,8 +131,8 @@ class StorageEvent(Event): url: str -class SubmitEvent(Event, Generic[T]): - data: T +class SubmitEvent(Event, Generic[_T]): + data: _T class TimeEvent(Event): ... diff --git a/seamless/types/html/html_event_props.py b/seamless/types/html/html_event_props.py index 4c6f2ea..f2f526d 100644 --- a/seamless/types/html/html_event_props.py +++ b/seamless/types/html/html_event_props.py @@ -17,7 +17,7 @@ WheelEvent, ) -P = ParamSpec("P") +_P = ParamSpec("_P") EventProps = TypeVar("EventProps", bound=Event, contravariant=True) if TYPE_CHECKING: From 61ae67b3cac983345ee849846015dc12a218409c Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Thu, 7 Nov 2024 19:04:22 +0200 Subject: [PATCH 20/23] Make page inherit from pydom page --- seamless/components/page.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/seamless/components/page.py b/seamless/components/page.py index 7b11ecd..357ac7e 100644 --- a/seamless/components/page.py +++ b/seamless/components/page.py @@ -1,14 +1,9 @@ from typing import Optional, overload, Iterable -from pydom import Component -from pydom.utils.functions import to_iter +from pydom.page import Page as BasePage from ..html import ( - Fragment, - Html, - Head, Title, - Body, Meta, ) @@ -16,7 +11,7 @@ from ..types.html import HTMLHtmlElement, HTMLBodyElement, HTMLHeadElement -class Page(Component): +class Page(BasePage): @overload def __init__( self, @@ -66,15 +61,6 @@ def body(self) -> Iterable[ChildType]: """ return self.children - def render(self): - return Fragment( - "", - Html(**self._html_props)( - Head(**self._head_props)(*to_iter(self.head())), - Body(**self._body_props)(*to_iter(self.body())), - ), - ) - def __init_subclass__(cls, title: Optional[str] = None, **kwargs) -> None: super().__init_subclass__(**kwargs) From 4055902bd09598c422e5ff7e790723bd0697fc17 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Thu, 7 Nov 2024 19:04:41 +0200 Subject: [PATCH 21/23] Fix context import from pydom to seamless --- seamless/extra/transports/socketio/transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seamless/extra/transports/socketio/transport.py b/seamless/extra/transports/socketio/transport.py index 7e938ff..a1a9d2c 100644 --- a/seamless/extra/transports/socketio/transport.py +++ b/seamless/extra/transports/socketio/transport.py @@ -2,11 +2,11 @@ from pathlib import Path from urllib.parse import parse_qs -from pydom import Component, Context from pydom.rendering.render_state import RenderState from pydom.utils.functions import random_string from socketio import AsyncServer +from .... import Component, Context from ....core.javascript import JS from ....core.empty import Empty from ..client_id import ClientID From 67cc50f01f1b4b2757c15c2b38d87c390f7ae291 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Sun, 30 Mar 2025 02:34:22 +0300 Subject: [PATCH 22/23] Migrate to pydom v0.4 --- docs/conf.py | 4 +- docs/user-guide/1-basics/1-syntax.rst | 6 +- .../1-basics/2-rendering-components.rst | 4 +- .../2-components/1-base-component.rst | 12 +- docs/user-guide/2-components/2-page.rst | 16 +- docs/user-guide/2-components/4-router.rst | 8 +- docs/user-guide/4-styling/2-css-modules.rst | 2 +- docs/user-guide/5-advanced/3-context.rst | 4 +- docs/user-guide/5-advanced/4-transformers.rst | 12 +- docs/user-guide/quick-start.rst | 8 +- examples/simple/components/app.py | 12 +- examples/simple/components/loading.py | 6 +- examples/simple/pages/base.py | 2 +- examples/simple/pages/counter.py | 28 +-- examples/simple/pages/home.py | 12 +- examples/spa/components/app.py | 23 ++- examples/spa/components/loading.py | 6 +- examples/spa/components/usage.py | 12 +- examples/spa/main.py | 16 ++ examples/spa/pages/base.py | 2 +- examples/spa/pages/counter.py | 30 +-- examples/spa/pages/home.py | 14 +- examples/spa/pages/user.py | 18 +- pyproject.toml | 14 +- requirements.txt | 2 +- seamless/components/router/router.py | 4 + seamless/context/context.py | 39 ++-- seamless/context/feature.py | 14 ++ seamless/extra/components/__init__.py | 4 +- seamless/extra/events/__init__.py | 18 +- seamless/extra/events/database.py | 8 +- seamless/extra/feature.py | 2 +- seamless/extra/state/__init__.py | 2 +- .../extra/transports/socketio/middleware.py | 2 +- .../extra/transports/socketio/transport.py | 1 - seamless/extra/transports/subscriptable.py | 4 +- seamless/extra/transports/transport.py | 2 +- seamless/internal/constants.py | 2 + seamless/internal/injector.py | 26 --- seamless/internal/utils.py | 6 +- seamless/styling/__init__.py | 1 - seamless/styling/style.py | 42 ---- seamless/types/html/html_element.py | 24 +-- seamless/types/html/html_element_props.py | 2 +- tests/__init__.py | 4 + tests/base.py | 35 ++++ tests/components/__init__.py | 29 +-- tests/css/styles.css | 13 ++ tests/html/escaping.html | 4 + tests/html/escaping_script.html | 3 + tests/html/nested_components.html | 5 + tests/html/page_inheritance.html | 15 ++ tests/manual.py | 111 ++-------- tests/requirements.txt | Bin 180 -> 0 bytes tests/server/common.py | 10 +- tests/simple.py | 27 +++ tests/test_css_modules.py | 21 ++ tests/test_escaping.py | 39 ++++ tests/test_html_rendering.py | 118 +++++++++++ tests/test_json_rendering.py | 191 ++++++++++++++++++ tests/test_nested.py | 15 +- tests/test_rendering.py | 4 +- tests/utils.py | 26 +++ 63 files changed, 773 insertions(+), 373 deletions(-) create mode 100644 seamless/context/feature.py delete mode 100644 seamless/internal/injector.py delete mode 100644 seamless/styling/__init__.py delete mode 100644 seamless/styling/style.py create mode 100644 tests/base.py create mode 100644 tests/css/styles.css create mode 100644 tests/html/escaping.html create mode 100644 tests/html/escaping_script.html create mode 100644 tests/html/nested_components.html create mode 100644 tests/html/page_inheritance.html create mode 100644 tests/simple.py create mode 100644 tests/test_css_modules.py create mode 100644 tests/test_escaping.py create mode 100644 tests/test_html_rendering.py create mode 100644 tests/test_json_rendering.py create mode 100644 tests/utils.py diff --git a/docs/conf.py b/docs/conf.py index d8cbf3f..c140fc4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,4 @@ -import sys +import sys, datetime sys.path.insert(0, "..") from seamless import __version__ @@ -12,8 +12,8 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "Seamless" -copyright = "2024, Xpo Development" author = "Xpo Development" +copyright = f"{datetime.date.today().year}, {author}" version = __version__ # -- General configuration --------------------------------------------------- diff --git a/docs/user-guide/1-basics/1-syntax.rst b/docs/user-guide/1-basics/1-syntax.rst index 5ab83c6..ce92fbf 100644 --- a/docs/user-guide/1-basics/1-syntax.rst +++ b/docs/user-guide/1-basics/1-syntax.rst @@ -22,11 +22,11 @@ Using the card component as an example, we will show the different syntaxes. self.title = title def render(self): - return Div(class_name="card")( - self.title and Div(class_name="card-title")( + return Div(classes="card")( + self.title and Div(classes="card-title")( self.title ), - Div(class_name="card-content")( + Div(classes="card-content")( *self.children ) ) diff --git a/docs/user-guide/1-basics/2-rendering-components.rst b/docs/user-guide/1-basics/2-rendering-components.rst index 4cee85c..02d468f 100644 --- a/docs/user-guide/1-basics/2-rendering-components.rst +++ b/docs/user-guide/1-basics/2-rendering-components.rst @@ -114,11 +114,11 @@ Props Rendering ############### When rendering components, some props names are converted to another name in the HTML representation. -For example, the ``class_name`` prop is converted to the ``class`` attribute in the HTML representation. +For example, the ``classes`` prop is converted to the ``class`` attribute in the HTML representation. The full list of prop names and their corresponding HTML attributes is as follows: -- ``class_name`` -> ``class`` +- ``classes`` -> ``class`` - ``html_for`` -> ``for`` - ``accept_charset`` -> ``accept-charset`` - ``http_equiv`` -> ``http-equiv`` diff --git a/docs/user-guide/2-components/1-base-component.rst b/docs/user-guide/2-components/1-base-component.rst index 384cc31..7beec68 100644 --- a/docs/user-guide/2-components/1-base-component.rst +++ b/docs/user-guide/2-components/1-base-component.rst @@ -25,11 +25,11 @@ It provides a ``children`` property that is a tuple of the components that are c self.title = title def render(self): - return Div(class_name="card")( - self.title and Div(class_name="card-title")( + return Div(classes="card")( + self.title and Div(classes="card-title")( self.title ), - Div(class_name="card-content")( + Div(classes="card-content")( *self.children ) ) @@ -136,11 +136,11 @@ component with the children as arguments. (See :ref:`syntax`) self.title = title def render(self): - return Div(class_name="card")( - Div(class_name="card-header")( + return Div(classes="card")( + Div(classes="card-header")( self.title ), - Div(class_name="card-body")( + Div(classes="card-body")( *self.children ) ) diff --git a/docs/user-guide/2-components/2-page.rst b/docs/user-guide/2-components/2-page.rst index c720d2b..32575e0 100644 --- a/docs/user-guide/2-components/2-page.rst +++ b/docs/user-guide/2-components/2-page.rst @@ -45,12 +45,12 @@ The page component can be used to create a new page by passing the following pro def my_awesome_page(): return Page(title="My awesome page")( - Div(class_name="container mt-5")( - Div(class_name="text-center p-4 rounded")( - Div(class_name="h-1")( + Div(classes="container mt-5")( + Div(classes="text-center p-4 rounded")( + Div(classes="h-1")( "Awesome page" ), - P(class_name="lead")( + P(classes="lead")( "Welcome to seamless" ) ) @@ -90,12 +90,12 @@ You can create custom pages by extending the page component and overriding the d def my_awesome_page(): return MyPage(title="My awesome page")( - Div(class_name="container mt-5")( - Div(class_name="text-center p-4 rounded")( - Div(class_name="h-1")( + Div(classes="container mt-5")( + Div(classes="text-center p-4 rounded")( + Div(classes="h-1")( "Awesome page" ), - P(class_name="lead")( + P(classes="lead")( "Welcome to seamless" ) ) diff --git a/docs/user-guide/2-components/4-router.rst b/docs/user-guide/2-components/4-router.rst index 9e60fe1..2327867 100644 --- a/docs/user-guide/2-components/4-router.rst +++ b/docs/user-guide/2-components/4-router.rst @@ -54,7 +54,7 @@ To navigate between the pages, use the ``RouterLink`` component from ``seamless. class MyApp(Component): def render(self): - return Div(class_name="root")( + return Div(classes="root")( Nav( RouterLink(to="/")( "Home" @@ -66,7 +66,7 @@ To navigate between the pages, use the ``RouterLink`` component from ``seamless. "Contact" ) ), - Div(class_name="content")( + Div(classes="content")( Router( Route(path="/", component=Home), Route(path="/about", component=About), @@ -149,7 +149,7 @@ The parameter will be passed to the component as a prop with the same name. class MyApp(Component): def render(self): - return Div(class_name="root")( + return Div(classes="root")( Nav( RouterLink(to="/user/1")( "User 1" @@ -158,7 +158,7 @@ The parameter will be passed to the component as a prop with the same name. "User 2" ) ), - Div(class_name="content")( + Div(classes="content")( Router( Route(path="/user/{id:int}", component=User) ) diff --git a/docs/user-guide/4-styling/2-css-modules.rst b/docs/user-guide/4-styling/2-css-modules.rst index aa8e50c..fbbc8ff 100644 --- a/docs/user-guide/4-styling/2-css-modules.rst +++ b/docs/user-guide/4-styling/2-css-modules.rst @@ -34,7 +34,7 @@ Then, use the ``CSS`` to import your css files. class MyComponent(Component): def render(self, css): - return Div(class_name=styles.card)( + return Div(classes=styles.card)( "Hello, world!" ) diff --git a/docs/user-guide/5-advanced/3-context.rst b/docs/user-guide/5-advanced/3-context.rst index 1977be0..0c19f08 100644 --- a/docs/user-guide/5-advanced/3-context.rst +++ b/docs/user-guide/5-advanced/3-context.rst @@ -51,7 +51,7 @@ The default context is created using the ``Context.standard`` method and has the The standard context also comes with the following :ref:`property transformers` in order: -- **Class Transformer**: Changes the ``class_name`` property key to ``class`` and converts +- **Class Transformer**: Changes the ``classes`` property key to ``class`` and converts the value to a string if it is a list. .. code-block:: python @@ -62,7 +62,7 @@ The standard context also comes with the following :ref:`property transformers

` except for ``class_name``. +- **Simple Transformer**: Converts the properties in :ref:`this list` except for ``classes``. .. code-block:: python diff --git a/docs/user-guide/5-advanced/4-transformers.rst b/docs/user-guide/5-advanced/4-transformers.rst index 8153ca3..f9a4ca6 100644 --- a/docs/user-guide/5-advanced/4-transformers.rst +++ b/docs/user-guide/5-advanced/4-transformers.rst @@ -52,11 +52,11 @@ The following example demonstrates how to use a property transformer to give a c @property_transformer("style") def add_class_to_inline_styles(key, value, element: ElementNode, render_state: RenderState): - class_name = uuid4().hex + classes = uuid4().hex if value: - element.props["class"] = f"{class_name} {element.props.get('class', '')}" + element.props["class"] = f"{classes} {element.props.get('class', '')}" - render_state.custom_data["css_string"] = render_state.custom_data.get("css_string", "") + f".{class_name} {{{value}}}" + render_state.custom_data["css_string"] = render_state.custom_data.get("css_string", "") + f".{classes} {{{value}}}" del element.props[key] @@ -104,11 +104,11 @@ The following example demonstrates how to use a post-render transformer to add t @property_transformer("style") def add_class_to_inline_styles(key, value, element, render_state: RenderState): - class_name = uuid4().hex + classes = uuid4().hex if value: - element.props["class"] = f"{class_name} {element.props.get('class', '')}" + element.props["class"] = f"{classes} {element.props.get('class', '')}" - render_state.custom_data["css_string"] = render_state.custom_data.get("css_string", "") + f".{class_name} {{{value}}}" + render_state.custom_data["css_string"] = render_state.custom_data.get("css_string", "") + f".{classes} {{{value}}}" del element.props[key] @post_render_transformer() diff --git a/docs/user-guide/quick-start.rst b/docs/user-guide/quick-start.rst index 8667a04..0ba8f38 100644 --- a/docs/user-guide/quick-start.rst +++ b/docs/user-guide/quick-start.rst @@ -61,12 +61,12 @@ Last, we create the ``FastAPI`` app and add an endpoint that will render our pag async def read_root(): return render( AppPage( - Div(class_name="container mt-5")( - Div(class_name="text-center p-4 rounded")( - Div(class_name="display-4")( + Div(classes="container mt-5")( + Div(classes="text-center p-4 rounded")( + Div(classes="display-4")( "Hello, World!" ), - P(class_name="lead")( + P(classes="lead")( "Welcome to seamless" ) ) diff --git a/examples/simple/components/app.py b/examples/simple/components/app.py index 2d4238c..fa7083d 100644 --- a/examples/simple/components/app.py +++ b/examples/simple/components/app.py @@ -12,11 +12,11 @@ def render(self): return BasePage( State.init(), SocketIOTransport.init(), - Div(class_name="d-flex flex-column h-100")( - Div(class_name="d-flex justify-content-between")( - Nav(class_name="navbar navbar-expand-lg navbar-light bg-light")( - RouterLink(to="/", class_name="navbar-brand")("Home"), - RouterLink(to="/counter", class_name="navbar-brand")("Counter"), + Div(classes="d-flex flex-column h-100")( + Div(classes="d-flex justify-content-between")( + Nav(classes="navbar navbar-expand-lg navbar-light bg-light")( + RouterLink(to="/", classes="navbar-brand")("Home"), + RouterLink(to="/counter", classes="navbar-brand")("Counter"), ), Div( Button(on_click=self.foo)( @@ -24,7 +24,7 @@ def render(self): ) ) ), - Div(class_name="content flex-grow-1")( + Div(classes="content flex-grow-1")( Router(loading_component=Loading)( Route(path="/", component=HomePage), Route(path="/counter", component=CounterPage), diff --git a/examples/simple/components/loading.py b/examples/simple/components/loading.py index 321e0c8..f3c3013 100644 --- a/examples/simple/components/loading.py +++ b/examples/simple/components/loading.py @@ -1,13 +1,13 @@ +from pydom.styling import CSS from seamless import Component, Div -from seamless.styling import CSS styles = CSS.module("./loading.css") class Loading(Component): def render(self): - return Div(class_name="d-flex align-items-center justify-content-center h-100")( + return Div(classes="d-flex align-items-center justify-content-center h-100")( Div( - class_name=f"{styles.spinner}", + classes=f"{styles.spinner}", ), ) diff --git a/examples/simple/pages/base.py b/examples/simple/pages/base.py index ab0865d..8ebb174 100644 --- a/examples/simple/pages/base.py +++ b/examples/simple/pages/base.py @@ -1,6 +1,6 @@ +from pydom.styling import CSS from seamless import Link, Script, Style, __version__ from seamless.components import Page -from seamless.styling import CSS class BasePage(Page): def head(self): diff --git a/examples/simple/pages/counter.py b/examples/simple/pages/counter.py index 633bc13..d548de8 100644 --- a/examples/simple/pages/counter.py +++ b/examples/simple/pages/counter.py @@ -14,34 +14,34 @@ class Counter(BaseModel): class CounterPage(Component): def render(self): - return Div(class_name="container")( - Div(class_name="row")( - Div(class_name="display-1 text-center")("Counter"), - Div(class_name="display-6 text-center")("A simple counter page."), + return Div(classes="container")( + Div(classes="row")( + Div(classes="display-1 text-center")("Counter"), + Div(classes="display-6 text-center")("A simple counter page."), ), - Div(class_name="row mt-4")( - Div(class_name="lead col-12 text-center")( + Div(classes="row mt-4")( + Div(classes="lead col-12 text-center")( "Current counter: ", counter() ), ), - Div(class_name="row")( - Div(class_name="col-12 text-center")( - Div(class_name="btn-group")( - Div(class_name="btn btn-danger", on_click=counter("current - 1"))( + Div(classes="row")( + Div(classes="col-12 text-center")( + Div(classes="btn-group")( + Div(classes="btn btn-danger", on_click=counter("current - 1"))( "Decrement" ), - Div(class_name="btn btn-primary", on_click=counter("0"))( + Div(classes="btn btn-primary", on_click=counter("0"))( "Reset" ), Div( - class_name="btn btn-success", on_click=counter("current + 1") + classes="btn btn-success", on_click=counter("current + 1") )("Increment"), ), ), - Div(class_name="col-12 text-center mt-4")( + Div(classes="col-12 text-center mt-4")( Form(action="#", on_submit=self.submit)( Input(type="hidden", name="counter_value", value=counter()), - Button(class_name="btn btn-primary", type="submit")("Submit"), + Button(classes="btn btn-primary", type="submit")("Submit"), ) ), ), diff --git a/examples/simple/pages/home.py b/examples/simple/pages/home.py index 1a65e99..73ad72c 100644 --- a/examples/simple/pages/home.py +++ b/examples/simple/pages/home.py @@ -5,15 +5,15 @@ class HomePage(Component): def render(self): - return Div(class_name="container")( - Div(class_name="row")( - Div(class_name="display-1 text-center")("Welcome to Seamless!"), - Div(class_name="display-6 text-center")( + return Div(classes="container")( + Div(classes="row")( + Div(classes="display-1 text-center")("Welcome to Seamless!"), + Div(classes="display-6 text-center")( "A Python library for building web pages using Python." ), ), - Div(class_name="row mt-4")( - Div(class_name="lead col-12 text-center")("Current counter: ", State("counter").get()), + Div(classes="row mt-4")( + Div(classes="lead col-12 text-center")("Current counter: ", State("counter").get()), ), Clock(), ) diff --git a/examples/spa/components/app.py b/examples/spa/components/app.py index 2a77bb0..a1187e4 100644 --- a/examples/spa/components/app.py +++ b/examples/spa/components/app.py @@ -1,5 +1,5 @@ +from pydom.styling import StyleSheet from seamless import Component, Div, Nav, Button, Context -from seamless.styling import StyleObject from seamless.extensions import State, SocketIOTransport from seamless.components.router import Router, Route, RouterLink from pages import HomePage, CounterPage, BasePage, UserPage @@ -12,23 +12,23 @@ def render(self): return BasePage( State.init(), SocketIOTransport.init(), - Div(class_name="d-flex flex-column h-100")( - Div(class_name="d-flex justify-content-between bg-light")( - Nav(class_name="navbar navbar-expand-lg navbar-light")( - RouterLink(to="/", class_name="navbar-brand")("Home"), - RouterLink(to="/counter", class_name="navbar-brand")("Counter"), - RouterLink(to="/usage", class_name="navbar-brand")("Usage"), + Div(classes="d-flex flex-column h-100")( + Div(classes="d-flex justify-content-between bg-light")( + Nav(classes="navbar navbar-expand-lg navbar-light")( + RouterLink(to="/", classes="navbar-brand")("Home"), + RouterLink(to="/counter", classes="navbar-brand")("Counter"), + RouterLink(to="/usage", classes="navbar-brand")("Usage"), ), - Div( + Div(on_click=self.moo)( Button( on_click=foo, - style=StyleObject( + style=StyleSheet( border_radius="5px", background_color="red" ), )("Click me!") ), ), - Div(class_name="content flex-grow-1")( + Div(classes="content flex-grow-1")( Router(loading_component=Loading)( Route(path="/", component=HomePage), Route(path="/counter", component=CounterPage), @@ -40,6 +40,9 @@ def render(self): title="Seamless", ) + def moo(self, event): + print("moo") + def foo(event, context: Context): print("foo") diff --git a/examples/spa/components/loading.py b/examples/spa/components/loading.py index 321e0c8..f3c3013 100644 --- a/examples/spa/components/loading.py +++ b/examples/spa/components/loading.py @@ -1,13 +1,13 @@ +from pydom.styling import CSS from seamless import Component, Div -from seamless.styling import CSS styles = CSS.module("./loading.css") class Loading(Component): def render(self): - return Div(class_name="d-flex align-items-center justify-content-center h-100")( + return Div(classes="d-flex align-items-center justify-content-center h-100")( Div( - class_name=f"{styles.spinner}", + classes=f"{styles.spinner}", ), ) diff --git a/examples/spa/components/usage.py b/examples/spa/components/usage.py index 577885f..0ce0bd1 100644 --- a/examples/spa/components/usage.py +++ b/examples/spa/components/usage.py @@ -1,8 +1,8 @@ +from pydom.styling import CSS from seamless import Component, Div, Table, Th, Tr, Td, Span, Details, Summary from seamless.context import Context from seamless.extra.events import EventsFeature from seamless.extra.events.database import Action -from seamless.styling import CSS styles = CSS.module("./usage.css") @@ -12,7 +12,7 @@ def __init__(self, actions: dict[str, Action]): self.actions = actions def render(self): - return Table(class_name=styles.table)( + return Table(classes=styles.table)( Tr( Th("Index"), Th("Event ID"), @@ -36,20 +36,20 @@ def render(self, context: Context): events = context.get_feature(EventsFeature) total_scoped = sum(len(scope) for scope in events.DB.scoped_events.values()) - return Div(class_name="p-4")( + return Div(classes="p-4")( Details( Summary( - Span(class_name="h2")( + Span(classes="h2")( f"Global Events - Total: {len(events.DB.events)}" ) ), ActionTable(actions=events.DB.events), ), Details( - Summary(Span(class_name="h2")(f"Actions - Total: {total_scoped}")), + Summary(Span(classes="h2")(f"Actions - Total: {total_scoped}")), *( Details( - Summary(Span(class_name="h3")(f"Scope: {scope}")), + Summary(Span(classes="h3")(f"Scope: {scope}")), Div( ActionTable(actions=events.DB.scoped_events[scope]), ), diff --git a/examples/spa/main.py b/examples/spa/main.py index 5209b8c..de37856 100644 --- a/examples/spa/main.py +++ b/examples/spa/main.py @@ -7,6 +7,13 @@ from components.app import App +from seamless.context import get_context +from pydom.context.standard.transformers.class_transformer import ClassTransformer + +class_transformer = next(t for t in get_context().prop_transformers if isinstance(t, ClassTransformer)) +if class_transformer: + class_transformer.prop_name = "classes" + HERE = Path(__file__).parent app = FastAPI() @@ -18,6 +25,15 @@ def read_static(file_path: str): return FileResponse(HERE / "static" / file_path) +@app.get("/favicon.ico") +def read_favicon(): + favicon = HERE / "static" / "favicon.ico" + if favicon.exists(): + return FileResponse(favicon) + + return "" + + @app.get("/{full_path:path}", response_class=HTMLResponse) def read_root(): return render(App()) diff --git a/examples/spa/pages/base.py b/examples/spa/pages/base.py index ab0865d..8ebb174 100644 --- a/examples/spa/pages/base.py +++ b/examples/spa/pages/base.py @@ -1,6 +1,6 @@ +from pydom.styling import CSS from seamless import Link, Script, Style, __version__ from seamless.components import Page -from seamless.styling import CSS class BasePage(Page): def head(self): diff --git a/examples/spa/pages/counter.py b/examples/spa/pages/counter.py index 01eb811..97c7a88 100644 --- a/examples/spa/pages/counter.py +++ b/examples/spa/pages/counter.py @@ -15,38 +15,38 @@ class Counter(TypedDict): class CounterPage(Component): def render(self): - return Div(class_name="container")( - Div(class_name="row")( - Div(class_name="display-1 text-center")("Counter"), - Div(class_name="display-6 text-center")("A simple counter page."), + return Div(classes="container")( + Div(classes="row")( + Div(classes="display-1 text-center")("Counter"), + Div(classes="display-6 text-center")("A simple counter page."), ), - Div(class_name="row mt-4")( - Div(class_name="lead col-12 text-center")( + Div(classes="row mt-4")( + Div(classes="lead col-12 text-center")( "Current counter: ", counter() ), ), - Div(class_name="row")( - Div(class_name="col-12 text-center")( - Div(class_name="btn-group")( - Div(class_name="btn btn-danger", on_click=counter("current - 1"))( + Div(classes="row")( + Div(classes="col-12 text-center")( + Div(classes="btn-group")( + Div(classes="btn btn-danger", on_click=counter("current - 1"))( "Decrement" ), - Div(class_name="btn btn-primary", on_click=counter("0"))( + Div(classes="btn btn-primary", on_click=counter("0"))( "Reset" ), Div( - class_name="btn btn-success", on_click=counter("current + 1") + classes="btn btn-success", on_click=counter("current + 1") )("Increment"), ), ), - Div(class_name="col-12 text-center mt-4")( + Div(classes="col-12 text-center mt-4")( Form(action="#", on_submit=self.submit)( Input(type="hidden", name="counter_value", value=counter()), - Button(class_name="btn btn-primary", type="submit")("Submit"), + Button(classes="btn btn-primary", type="submit")("Submit"), ) ), Button( - class_name="btn btn-primary", type="submit", on_click=global_submit + classes="btn btn-primary", type="submit", on_click=global_submit )("Submit 2"), ), ) diff --git a/examples/spa/pages/home.py b/examples/spa/pages/home.py index a99fa9b..c81d754 100644 --- a/examples/spa/pages/home.py +++ b/examples/spa/pages/home.py @@ -5,17 +5,17 @@ class HomePage(Component): def render(self): - return Div(class_name="container")( - Div(class_name="row")( - Div(class_name="display-1 text-center")("Welcome to Seamless!"), - Div(class_name="display-6 text-center")( + return Div(classes="container")( + Div(classes="row")( + Div(classes="display-1 text-center")("Welcome to Seamless!"), + Div(classes="display-6 text-center")( "A Python library for building web pages using Python." ), ), - Div(class_name="row mt-4")( - Div(class_name="lead col-12 text-center")("Current counter: ", State("counter").get()), + Div(classes="row mt-4")( + Div(classes="lead col-12 text-center")("Current counter: ", State("counter").get()), ), - Div(class_name="row mt-4")( + Div(classes="row mt-4")( "Search User: ", Input(type="text", on_change=search_user("this.value")), Button(on_click=JS(f"seamless.navigateTo('/user/' + {search_user})"))("Search"), diff --git a/examples/spa/pages/user.py b/examples/spa/pages/user.py index 8888c0e..71acb1e 100644 --- a/examples/spa/pages/user.py +++ b/examples/spa/pages/user.py @@ -14,9 +14,9 @@ class UserPage(Component): def render(self): if self.user_id >= len(users): - return Div(class_name="container")( - Div(class_name="row")( - Div(class_name="display-1 text-center")("User not found"), + return Div(classes="container")( + Div(classes="row")( + Div(classes="display-1 text-center")("User not found"), ) ) @@ -25,16 +25,16 @@ def render(self): f"{user['name']['title']} {user['name']['first']} {user['name']['last']}" ) - return Div(class_name="container")( - Div(class_name="row")( - Div(class_name="text-center")( + return Div(classes="container")( + Div(classes="row")( + Div(classes="text-center")( Img( src=user["picture"]["large"], alt=user_name, - class_name="rounded-circle", + classes="rounded-circle", ), ), - Div(class_name="display-1 text-center")(user_name), - Div(class_name="display-6 text-center")(f"Email: {user['email']}"), + Div(classes="display-1 text-center")(user_name), + Div(classes="display-6 text-center")(f"Email: {user['email']}"), ), ) diff --git a/pyproject.toml b/pyproject.toml index 7dec424..714da20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,19 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] -dynamic = ["dependencies", "version"] +dynamic = ["version"] +dependencies = [ + "cssutils==2.9.0", + "pydom", + "python-socketio==5.11.1", + "typing-extensions", +] + +[dependency-groups] +dev = [ + "fastapi", + "uvicorn", +] [project.urls] Homepage = "https://github.com/xpodev/seamless" diff --git a/requirements.txt b/requirements.txt index 46ef01d..9c438e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ cssutils==2.9.0 -python-dom +pydom python-socketio==5.11.1 typing-extensions \ No newline at end of file diff --git a/seamless/components/router/router.py b/seamless/components/router/router.py index 6cc20fd..3c83eb9 100644 --- a/seamless/components/router/router.py +++ b/seamless/components/router/router.py @@ -40,4 +40,8 @@ def render(self): return Empty( init=JS(f"let routes = {dumps(routes)};") + ROUTER_JS, loading=self.loading_component, + umount_function=self.on_umount, ) + + def on_umount(self): + ... diff --git a/seamless/context/context.py b/seamless/context/context.py index a31f180..dd9c360 100644 --- a/seamless/context/context.py +++ b/seamless/context/context.py @@ -2,41 +2,52 @@ from typing import ( Any, Callable, + Dict, Optional, + Type, TypeVar, - Union, cast, ) from typing_extensions import Concatenate, ParamSpec - from pydom.context.context import ( Context as _Context, get_context as _get_context, set_default_context as _set_global_context, ) -from pydom.rendering.tree.nodes import ContextNode from ..errors import Error +from .feature import Feature from ..internal.constants import DISABLE_GLOBAL_CONTEXT_ENV -from ..internal.injector import Injector _P = ParamSpec("_P") +_T = TypeVar("_T", bound=Feature) -Feature = Callable[Concatenate["Context", _P], Any] -PropertyMatcher = Union[Callable[Concatenate[str, Any, _P], bool], str] -PropertyTransformer = Callable[Concatenate[str, Any, "ContextNode", _P], None] -PostRenderTransformer = Callable[Concatenate["ContextNode", _P], None] +FeatureFactory = Callable[Concatenate["Context", _P], Feature] class Context(_Context): def __init__(self) -> None: super().__init__() - self.injector = Injector() - self.injector.add(Context, self) + self._features: Dict[Type[Feature], Feature] = {} + + def add_feature(self, feature: FeatureFactory[_P], *args: _P.args, **kwargs: _P.kwargs): + result = feature(self, *args, **kwargs) + if isinstance(feature, type): + self._features[feature] = result + + def get_feature(self, feature_type: Type[_T]) -> _T: + try: + return cast(_T, self._features[feature_type]) + except KeyError: + for instance in self._features.values(): + if isinstance(instance, feature_type): + return instance + + raise @classmethod - def standard(cls) -> "Context": + def standard(cls: Type["Context"]) -> "Context": context = cls() from .default import add_standard_features @@ -56,12 +67,10 @@ def get_context(context: Optional[Context] = None): "You must provide a context explicitly. Did you forget to call set_global_context?" ) from None - raise Error( - "No global context found. Did you forget to call set_global_context?" - ) + raise Error("No global context found. Did you forget to call set_global_context?") return context -def set_global_context(context: _Context): +def set_global_context(context: Context): _set_global_context(context) diff --git a/seamless/context/feature.py b/seamless/context/feature.py new file mode 100644 index 0000000..da703dc --- /dev/null +++ b/seamless/context/feature.py @@ -0,0 +1,14 @@ +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from .context import Context + + +class Feature: + def __init__(self, context: "Context") -> None: + self.context = context + + @property + def feature_name(self) -> str: + return type(self).__name__.replace("Feature", "").lower() \ No newline at end of file diff --git a/seamless/extra/components/__init__.py b/seamless/extra/components/__init__.py index 5694de5..c13c387 100644 --- a/seamless/extra/components/__init__.py +++ b/seamless/extra/components/__init__.py @@ -2,10 +2,10 @@ from typing import TYPE_CHECKING, ClassVar, Optional from pydom import Component -from pydom.context import Context -from pydom.context.feature import Feature from pydom.rendering import render_json +from ...context import Context +from ...context.feature import Feature from ...errors import ClientError from .repository import ComponentsRepository from ..transports.transport import TransportFeature diff --git a/seamless/extra/events/__init__.py b/seamless/extra/events/__init__.py index 6d51a55..7406d19 100644 --- a/seamless/extra/events/__init__.py +++ b/seamless/extra/events/__init__.py @@ -1,18 +1,18 @@ -from typing import Callable +from typing import Callable, List -from pydom.context import Context from pydom.rendering.render_state import RenderState from pydom.rendering.tree.nodes import ContextNode +from ...context import Context from .database import EventsDatabase, Action from ..feature import Feature from ...internal.constants import ( SEAMLESS_ELEMENT_ATTRIBUTE, SEAMLESS_INIT_ATTRIBUTE, + UNSAFE_GLOBAL_EVENT_ATTRIBUTE, ) from ...internal.validation import wrap_with_validation -from ..transports.errors import TransportConnectionRefused from ..transports.transport import TransportFeature @@ -79,7 +79,7 @@ def transformer( return matcher, transformer def _post_render_transformer(self, root: ContextNode, render_state: RenderState): - actions = render_state.custom_data.get("events.actions", []) + actions: List[Action] = render_state.custom_data.get("events.actions", []) if len(actions) == 0: return @@ -94,4 +94,12 @@ def _post_render_transformer(self, root: ContextNode, render_state: RenderState) ) else: for action in actions: - self.DB.add_event(action, scope=client_id) + self.DB.add_event( + action, + scope=client_id, + ) + + +def UnsAfE_gL__o__bAL_EVEnt(func: Callable) -> Callable: + setattr(func, UNSAFE_GLOBAL_EVENT_ATTRIBUTE, True) + return func diff --git a/seamless/extra/events/database.py b/seamless/extra/events/database.py index fe2db87..7729095 100644 --- a/seamless/extra/events/database.py +++ b/seamless/extra/events/database.py @@ -38,8 +38,8 @@ def add_event(self, action: Action, *, scope: str): if is_global(action.action): self.events[action.id] = action - - self.scoped_events.setdefault(scope, {})[action.id] = action + else: + self.scoped_events.setdefault(scope, {})[action.id] = action return action @@ -60,7 +60,5 @@ def release_actions(self, client_id: str): def get_event(self, event_id: str, *, scope: str): if event_id in self.events: return self.events[event_id] - - return self.scoped_events[scope][event_id] - + return self.scoped_events[scope][event_id] diff --git a/seamless/extra/feature.py b/seamless/extra/feature.py index 0ac0d63..58afa1f 100644 --- a/seamless/extra/feature.py +++ b/seamless/extra/feature.py @@ -1 +1 @@ -from pydom.context.feature import Feature +from ..context.feature import Feature diff --git a/seamless/extra/state/__init__.py b/seamless/extra/state/__init__.py index 5d2de36..6773c03 100644 --- a/seamless/extra/state/__init__.py +++ b/seamless/extra/state/__init__.py @@ -3,9 +3,9 @@ from typing import Any, overload from pydom import Component -from pydom.context import Context from pydom.rendering.tree.nodes import ContextNode +from ...context import Context from ...core import Empty, JS from ..feature import Feature from ...internal.constants import SEAMLESS_ELEMENT_ATTRIBUTE, SEAMLESS_INIT_ATTRIBUTE diff --git a/seamless/extra/transports/socketio/middleware.py b/seamless/extra/transports/socketio/middleware.py index dd6187c..58cc533 100644 --- a/seamless/extra/transports/socketio/middleware.py +++ b/seamless/extra/transports/socketio/middleware.py @@ -1,7 +1,7 @@ from typing import Optional from socketio import ASGIApp -from pydom.context import Context, get_context +from ....context import Context, get_context from .transport import SocketIOTransport diff --git a/seamless/extra/transports/socketio/transport.py b/seamless/extra/transports/socketio/transport.py index a1a9d2c..4b40cbf 100644 --- a/seamless/extra/transports/socketio/transport.py +++ b/seamless/extra/transports/socketio/transport.py @@ -3,7 +3,6 @@ from urllib.parse import parse_qs from pydom.rendering.render_state import RenderState -from pydom.utils.functions import random_string from socketio import AsyncServer from .... import Component, Context diff --git a/seamless/extra/transports/subscriptable.py b/seamless/extra/transports/subscriptable.py index 007e7b0..dddd2f8 100644 --- a/seamless/extra/transports/subscriptable.py +++ b/seamless/extra/transports/subscriptable.py @@ -1,7 +1,7 @@ from typing import Any, Generic, Callable -from pydom import Context -from pydom.context.feature import Feature +from ...context.feature import Feature +from ...context import Context from typing_extensions import ParamSpec from ...errors import ClientError diff --git a/seamless/extra/transports/transport.py b/seamless/extra/transports/transport.py index 6bf89c0..e800366 100644 --- a/seamless/extra/transports/transport.py +++ b/seamless/extra/transports/transport.py @@ -1,8 +1,8 @@ from typing import Any, Set -from pydom.context import Context from pydom.utils.functions import random_string +from ...context import Context from ..feature import Feature from .dispatcher import dispatcher from .subscriptable import event diff --git a/seamless/internal/constants.py b/seamless/internal/constants.py index 3b01bee..df14300 100644 --- a/seamless/internal/constants.py +++ b/seamless/internal/constants.py @@ -3,3 +3,5 @@ DISABLE_GLOBAL_CONTEXT_ENV = "SEAMLESS_DISABLE_GLOBAL_CONTEXT" DISABLE_VALIDATION_ENV = "SEAMLESS_DISABLE_VALIDATION" + +UNSAFE_GLOBAL_EVENT_ATTRIBUTE = "seamless:unsafe-global-event" \ No newline at end of file diff --git a/seamless/internal/injector.py b/seamless/internal/injector.py deleted file mode 100644 index 687caad..0000000 --- a/seamless/internal/injector.py +++ /dev/null @@ -1,26 +0,0 @@ -from inspect import iscoroutinefunction -from functools import wraps -from typing import TypeVar, Callable - -from typing_extensions import TypeAlias - -from pydom.utils.injector import Injector as _Injector - - -_T = TypeVar("_T") - -InjectFactory: TypeAlias = Callable[[], _T] - - -class Injector(_Injector): - def inject(self, callback: Callable) -> Callable: - if iscoroutinefunction(callback): - - @wraps(callback) - async def wrapper(*args, **kwargs): # type: ignore - keyword_args = self.inject_params(callback) - return await callback(*args, **keyword_args, **kwargs) - - return wrapper - - return super().inject(callback) diff --git a/seamless/internal/utils.py b/seamless/internal/utils.py index c798018..1e8cd0a 100644 --- a/seamless/internal/utils.py +++ b/seamless/internal/utils.py @@ -1,6 +1,8 @@ from functools import wraps from inspect import iscoroutinefunction, isfunction +from seamless.internal.constants import UNSAFE_GLOBAL_EVENT_ATTRIBUTE + class Promise: def __init__(self, value): @@ -52,10 +54,12 @@ def __init__(self, d: dict): def is_global(func): func = original_func(func) + if getattr(func, UNSAFE_GLOBAL_EVENT_ATTRIBUTE, False): + return True return isfunction(func) and (getattr(func, "__closure__", None) is None) def original_func(func): if hasattr(func, "__wrapped__"): return original_func(func.__wrapped__) - return func \ No newline at end of file + return func diff --git a/seamless/styling/__init__.py b/seamless/styling/__init__.py deleted file mode 100644 index de7899e..0000000 --- a/seamless/styling/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from pydom.styling import * diff --git a/seamless/styling/style.py b/seamless/styling/style.py deleted file mode 100644 index 1d32902..0000000 --- a/seamless/styling/style.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import Generic, TypeVar, Union - -from typing_extensions import Unpack -from pydom.types.styling.css_properties import CSSProperties - -_T = TypeVar("_T") - - -class StyleObject: - class _StyleProperty(Generic[_T]): - def __init__(self, instance: "StyleObject", name: str): - self.instance = instance - self.name = name.replace("_", "-") - - def __call__(self, value: _T): - self.instance.style[self.name] = value - return self.instance - - def __init__( - self, *styles: Union["StyleObject", "CSSProperties"], **kwargs: Unpack["CSSProperties"] - ): - self.style: dict[str, object] = {} - for style in styles: - if isinstance(style, StyleObject): - style = style.style - self.style.update(style) - self.style.update(kwargs) - self.style = { - k.replace("_", "-"): v for k, v in self.style.items() if v is not None - } - - def copy(self): - return StyleObject(self) - - def to_css(self): - return "".join(map(lambda x: f"{x[0]}:{x[1]};", self.style.items())) - - def __str__(self): - return self.to_css() - - def __getattr__(self, name: str): - return StyleObject._StyleProperty(self, name) diff --git a/seamless/types/html/html_element.py b/seamless/types/html/html_element.py index 825dd51..9d99cf8 100644 --- a/seamless/types/html/html_element.py +++ b/seamless/types/html/html_element.py @@ -1,29 +1,11 @@ from typing import TYPE_CHECKING, Iterable, Union, Literal -from typing_extensions import TypedDict +from pydom.styling import StyleSheet +from pydom.types.html.html_element import HTMLElement if TYPE_CHECKING: from seamless.core.javascript import JS - from seamless.styling import StyleObject -class HTMLElement(TypedDict, total=False, closed=False): - access_key: str - auto_capitalize: str - class_name: Union[str, Iterable[str]] - content_editable: str - # data: dict[str, str] # add this if needed in the future - dir: Literal["ltr", "rtl", "auto"] - draggable: str - hidden: str - id: str - input_mode: str - lang: str - role: str - spell_check: str - style: Union[str, "StyleObject"] - tab_index: str - title: str - translate: str - +class HTMLElement(HTMLElement, total=False, closed=False): init: "JS" diff --git a/seamless/types/html/html_element_props.py b/seamless/types/html/html_element_props.py index f5347c4..c77a916 100644 --- a/seamless/types/html/html_element_props.py +++ b/seamless/types/html/html_element_props.py @@ -1,4 +1,4 @@ -from seamless.types.html.aria_props import AriaProps +from pydom.types.html.aria_props import AriaProps from seamless.types.html.html_element import HTMLElement from seamless.types.html.html_event_props import HTMLEventProps diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..c1bce1d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,4 @@ +import sys +import os + +sys.path.append(os.path.join(os.path.dirname(__file__), '../src')) \ No newline at end of file diff --git a/tests/base.py b/tests/base.py new file mode 100644 index 0000000..6fc58e4 --- /dev/null +++ b/tests/base.py @@ -0,0 +1,35 @@ +from typing import Union, overload +import unittest + +from pydom import render +from pydom.types.rendering import Primitive, Renderable + +from .utils import test_render_with_file + + +class TestCase(unittest.TestCase): + @overload + def assertRender( + self, + component: Union[Renderable, Primitive], + expected: Union[str, dict], + /, + **kwargs + ): ... + @overload + def assertRender( + self, + component: Union[Renderable, Primitive], + /, + *, + file: str, + **kwargs + ): ... + + def assertRender(self, component, expected=None, *, file=None, **kwargs): + if expected is not None: + self.assertEqual(render(component, **kwargs), expected) + elif file is not None: + self.assertTrue(test_render_with_file(component, file)) + else: + raise ValueError("Expected or file must be provided") diff --git a/tests/components/__init__.py b/tests/components/__init__.py index 6fb8b71..d0e88bc 100644 --- a/tests/components/__init__.py +++ b/tests/components/__init__.py @@ -1,5 +1,5 @@ -from seamless import Component, Div, H3, Hr, Link, Button, JS -from seamless.components import Page as _Page +from pydom import Component, Div, H3, Hr, Link +from pydom.page import Page as _Page class Plugin(Component): @@ -9,8 +9,9 @@ def __init__(self, name, version) -> None: def render(self): return Div( + classes="plugin", + )( f"{self.name} v{self.version}", - class_name="plugin", ) @@ -20,44 +21,36 @@ def __init__(self, plugins=None) -> None: def render(self): return Div( + classes="plugin-list", + )( *[Plugin(plugin.name, plugin.version) for plugin in self.plugins], - class_name="plugin-list", ) class Card(Component): def render(self): return Div( + classes="card", + )( *self.children, - class_name="card", ) class CardTitle(Component): def render(self): return H3( + classes="card-title", + )( *self.children, - class_name="card-title", ) class App(Component): def render(self): - return Card( - CardTitle("Card title"), - Hr(), - Div("Card content"), - ) + return Card(CardTitle("Card title"), Hr(), Div(*self.children)) class Page(_Page): def head(self): yield from super().head() yield Link(rel="stylesheet", href="/static/style.css") - - -class AlertButton(Component): - def render(self): - return Button(on_click=JS("alert('Button clicked')"))( - "Click me", - ) \ No newline at end of file diff --git a/tests/css/styles.css b/tests/css/styles.css new file mode 100644 index 0000000..452f1f0 --- /dev/null +++ b/tests/css/styles.css @@ -0,0 +1,13 @@ +.card { + background-color: #fff; + border-radius: 5px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin: 10px; + padding: 20px; +} + +.card_header { + font-size: 1.5em; + font-weight: bold; + margin-bottom: 10px; +} \ No newline at end of file diff --git a/tests/html/escaping.html b/tests/html/escaping.html new file mode 100644 index 0000000..02aebac --- /dev/null +++ b/tests/html/escaping.html @@ -0,0 +1,4 @@ +

+ <script>console.log('hello world'); if (a < 2) { + console.log('a is less than 2'); }</script> +
diff --git a/tests/html/escaping_script.html b/tests/html/escaping_script.html new file mode 100644 index 0000000..9123b65 --- /dev/null +++ b/tests/html/escaping_script.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/tests/html/nested_components.html b/tests/html/nested_components.html new file mode 100644 index 0000000..24dceba --- /dev/null +++ b/tests/html/nested_components.html @@ -0,0 +1,5 @@ +
+

Card title

+
+
+
diff --git a/tests/html/page_inheritance.html b/tests/html/page_inheritance.html new file mode 100644 index 0000000..da5370f --- /dev/null +++ b/tests/html/page_inheritance.html @@ -0,0 +1,15 @@ + + + + + + + + +
+

Card title

+
+
+
+ + diff --git a/tests/manual.py b/tests/manual.py index e374f93..f6499a3 100644 --- a/tests/manual.py +++ b/tests/manual.py @@ -1,101 +1,32 @@ -from functools import wraps -from inspect import iscoroutinefunction -from pathlib import Path -from fastapi import FastAPI, Response -from fastapi.responses import FileResponse -from pydantic import BaseModel -from pydom.element import Element -from seamless import * -from seamless.extra.transports.socketio.middleware import SocketIOMiddleware -from seamless.styling import CSS -from seamless.components import Page as BasePage -from .server.common import Card, Page, SuperCard, SampleComponent -from .components import Page as TestPage, App +from pydom import render, Div, Component +from components import App, Page +from pydom.context.context import get_context +import pydom.context.standard.transformers as t +from pydom.context.standard.transformers.class_transformer import ClassTransformer +from pydom.rendering.render_state import RenderState +from utils import test_render_with_file -app = FastAPI() -app.add_middleware(SocketIOMiddleware) +context = get_context() -def _make_response(response): - if isinstance(response, (Component, Element)): - if not isinstance(response, BasePage): - response = BasePage(response) +class User: ... - response = Response(render(response), media_type="text/html") - return response +def get_user(): + return {"name": "John Doe"} -@wraps(app.get) -def get(path: str, **kwargs): - def wrapper(_handler): - if iscoroutinefunction(_handler): +context.injector.add(User, get_user) - @wraps(_handler) - async def handler(*args, **kwargs): - response = await _handler(*args, **kwargs) - return _make_response(response) +class Foo(Component): + def render(self): + foo() + return Div("Hello, world!") - else: +@context.inject +def foo(user: RenderState): + print(user) - @wraps(_handler) - def handler(*args, **kwargs): - response = _handler(*args, **kwargs) - return _make_response(response) +render(Foo()) - app.get(path, **kwargs)(handler) - - return wrapper - - -def click_handler(*args, **kwargs): - print("Button clicked") - - -def card(super=True): - return (SuperCard(is_super=True) if super else Card())( - SampleComponent(name="world"), - Button("Click me"), - Form(on_submit=submit, action="#")( - Input(placeholder="Enter your name", name="name"), - Button("Submit"), - ), - ) - - -from typing import Generic, TypeVar - -T = TypeVar("T") - - -class SubmitEvent(BaseModel, Generic[T]): - type: str - data: T - - -class MyForm(BaseModel): - name: str - - -def submit(event: SubmitEvent[MyForm]): - print(f"Form submitted: name = {event.data.name}") - - -@get("/") -def index(super: bool = True): - return TestPage(App()) - - -@app.get("/static/main.js") -def socket_io_static(): - return FileResponse(Path(__file__).parent / "server/static/main.js") - - -@app.get("/static/main.css") -def css_file(): - return Response(CSS.to_css_string(), media_type="text/css") - - -@app.get("/static/main.min.css") -def css_file_min(): - return Response(CSS.to_css_string(minified=True), media_type="text/css") +foo() diff --git a/tests/requirements.txt b/tests/requirements.txt index a01e594aa66259cee8c63490a1aad9a075fddc2e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 180 zcmX|*(F%e<5Jk^(;HMa^5#mFy{fCILvXNq2)X%GTc1Z^AEOYPKnRU6-@*+wn2Ck|T zN1U`#uP8WhG{Z5L&o%G8KA PA9 None: def render(self): styles = CSS.module("./static/card.css") return Div( - class_name=styles.card, - style=StyleObject(border_radius="5px") if self.rounded else None, + classes=styles.card, + style=StyleSheet(border_radius="5px") if self.rounded else None, )(*self.children) @@ -70,6 +70,6 @@ def __init__(self, rounded=True, is_super=False) -> None: def render(self): styles = CSS.module("./static/card.css") return Div( - class_name=styles.card, - style=StyleObject(border_radius="5px") if self.rounded else None, + classes=styles.card, + style=StyleSheet(border_radius="5px") if self.rounded else None, )(Div("Super card!" if self.is_super else "Card!"), *self.children) diff --git a/tests/simple.py b/tests/simple.py new file mode 100644 index 0000000..c9e7b6d --- /dev/null +++ b/tests/simple.py @@ -0,0 +1,27 @@ +from http.server import BaseHTTPRequestHandler, HTTPServer + +from pydom import render, Div +from pydom.page import Page + + +def index(): + return Page(title="Hello, world!")( + Div(classes=["a", "b"])("Hello, world!") + ) + + +class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.end_headers() + self.wfile.write(render(index()).encode("utf-8")) + + +def run(server_class=HTTPServer, handler_class=SimpleHTTPRequestHandler): + server_address = ("", 8000) + httpd = server_class(server_address, handler_class) + httpd.serve_forever() + + +if __name__ == "__main__": + run() diff --git a/tests/test_css_modules.py b/tests/test_css_modules.py new file mode 100644 index 0000000..6253f1c --- /dev/null +++ b/tests/test_css_modules.py @@ -0,0 +1,21 @@ +from pydom.styling import CSS + +from .base import TestCase + + +class CSSModulesTest(TestCase): + @classmethod + def setUpClass(cls) -> None: + styles = CSS.module("tests/css/styles.css") + cls.card_class = styles.card + cls.card_header_class = styles.card_header + + def test_classes(self): + styles = CSS.module("tests/css/styles.css") + self.assertEqual(self.card_class, styles.card) + self.assertEqual(self.card_header_class, styles.card_header) + + def test_relative_css(self): + styles = CSS.module("./css/styles.css") + self.assertEqual(styles.card, self.card_class) + self.assertEqual(styles.card_header, self.card_header_class) diff --git a/tests/test_escaping.py b/tests/test_escaping.py new file mode 100644 index 0000000..07f2d4c --- /dev/null +++ b/tests/test_escaping.py @@ -0,0 +1,39 @@ +from pydom import Div, Script, Style + +from .base import TestCase + + +class EscapingComponentsTest(TestCase): + def test_escaping(self): + self.assertRender( + Div( + "" + ), + "
" + "<script>" + "console.log('hello world');" + "if (a < 2) {" + " console.log('a is less than 2');" + "}" + "</script>" + "
", + ) + + def test_escaping_style(self): + self.assertRender( + Style("body { color: red; }"), + "", + ) + + def test_escaping_script(self): + self.assertRender( + Script( + "console.log('hello world'); if (a < 2) { console.log('a is less than 2'); }" + ), + file="html/escaping_script.html", + ) diff --git a/tests/test_html_rendering.py b/tests/test_html_rendering.py new file mode 100644 index 0000000..8c89233 --- /dev/null +++ b/tests/test_html_rendering.py @@ -0,0 +1,118 @@ +from pydom import Component, Div, render +from pydom.element import Element +from pydom.errors import RenderError + +from .base import TestCase + + +class TestRender(TestCase): + def test_render(self): + self.assertRender(Div(), "
") + + def test_render_component(self): + class MyComponent(Component): + def render(self): + return Div() + + self.assertRender(MyComponent(), "
") + + def test_render_element(self): + class MyElement(Element): + tag_name = "my-element" + + self.assertRender(MyElement(), "") + + def test_render_inline_element(self): + class MyInlineElement(Element): + tag_name = "my-element" + inline = True + + self.assertRender(MyInlineElement(), "") + + def test_render_nested(self): + self.assertRender(Div(Div()), "
") + + def test_render_text(self): + self.assertRender(Div("Hello"), "
Hello
") + + def test_render_attributes(self): + self.assertRender(Div(id="my-id"), '
') + + def test_render_children(self): + self.assertRender(Div(Div(), Div()), "
") + + def test_render_component_children(self): + class MyComponent(Component): + def render(self): + return Div(Div(), Div()) + + self.assertRender(MyComponent(), "
") + + def test_render_element_children(self): + class MyElement(Element): + tag_name = "my-element" + + self.assertRender( + MyElement(Div(), Div()), + "
", + ) + + def test_render_text_children(self): + self.assertRender(Div("Hello", "World"), "
HelloWorld
") + + def test_render_nested_children(self): + self.assertRender( + Div(Div(Div()), Div(Div())), + "
", + ) + + def test_render_nested_text_children(self): + self.assertRender( + Div(Div("Hello"), Div("World")), + "
Hello
World
", + ) + + def test_render_nested_mixed_children(self): + self.assertRender( + Div(Div("Hello", id="my-div"), Div(), "World"), + '
Hello
World
', + ) + + def test_render_list_children(self): + self.assertRender(Div([Div(), Div()]), "
") + + def test_render_nested_list_comprehension_children(self): + self.assertRender( + Div([Div(i) for i in range(5)]), + "
0
1
2
3
4
", + ) + + def test_render_nested_list_children(self): + self.assertRender( + Div([Div([Div()]), Div([Div()])]), + "
", + ) + + def test_render_list_as_component_children(self): + class ItemList(Component): + def __init__(self, items) -> None: + self.items = items + + def render(self): + return Div([Item(item=item) for item in self.items]) + + class Item(Component): + def __init__(self, item) -> None: + self.item = item + + def render(self): + return Div(self.item) + + self.assertRender( + ItemList(items=[f"item{i}" for i in range(1, 5)]), + "
item1
item2
item3
item4
", + ) + + def test_render_invalid_children(self): + with self.assertRaises(RenderError): + render(Div(object())) # type: ignore - this is intentional diff --git a/tests/test_json_rendering.py b/tests/test_json_rendering.py new file mode 100644 index 0000000..72d4f92 --- /dev/null +++ b/tests/test_json_rendering.py @@ -0,0 +1,191 @@ +from typing import Union, overload +from pydom import Component, Div +from pydom.element import Element +from pydom.types.rendering import Primitive, Renderable + +from .base import TestCase + + +class TestRender(TestCase): + @overload + def assertRenderJson( + self, component: Union[Renderable, Primitive], expected: dict, /, **kwargs + ): ... + @overload + def assertRenderJson( + self, component: Union[Renderable, Primitive], *, file: str, **kwargs + ): ... + + def assertRenderJson( + self, + component: Union[Renderable, Primitive], + expected=None, + *, + file=None, + **kwargs, + ): + if expected is not None: + self.assertRender(component, expected, to="json", **kwargs) + elif file is not None: + self.assertRender(component, file=file, to="json", **kwargs) + else: + raise ValueError("Expected or file must be provided") + + def test_render_json(self): + self.assertRenderJson(Div(), {"type": "div", "children": [], "props": {}}) + + def test_render_json_component(self): + class MyComponent(Component): + def render(self): + return Div() + + self.assertRender( + MyComponent(), + {"type": "div", "children": [], "props": {}}, + to="json", + ) + + def test_render_json_element(self): + class MyElement(Element): + tag_name = "my-element" + + self.assertRenderJson( + MyElement(), + {"type": "my-element", "children": [], "props": {}}, + ) + + def test_render_json_nested(self): + self.assertRenderJson( + Div(Div()), + { + "type": "div", + "children": [{"type": "div", "children": [], "props": {}}], + "props": {}, + }, + ) + + def test_render_json_text(self): + self.assertRenderJson( + Div("Hello"), + {"type": "div", "children": ["Hello"], "props": {}}, + ) + + def test_render_json_attributes(self): + self.assertRenderJson( + Div(id="my-id"), + {"type": "div", "children": [], "props": {"id": "my-id"}}, + ) + + def test_render_json_nested_component(self): + class MyComponent(Component): + def render(self): + return Div( + "Hello", + Div(), + id="my-id", + classes="my-class", + ) + + class MyComponent2(Component): + def render(self): + return Div( + MyComponent(), + classes="my-class", + ) + + self.assertRenderJson( + MyComponent2(), + { + "type": "div", + "children": [ + { + "type": "div", + "children": [ + "Hello", + {"type": "div", "children": [], "props": {}}, + ], + "props": {"id": "my-id", "class": "my-class"}, + } + ], + "props": {"class": "my-class"}, + }, + ) + + def test_render_list_children(self): + self.assertRenderJson( + Div([Div(), Div()]), + { + "type": "div", + "children": [ + {"type": "div", "children": [], "props": {}}, + {"type": "div", "children": [], "props": {}}, + ], + "props": {}, + }, + ) + + def test_render_nested_list_comprehension_children(self): + self.assertRenderJson( + Div([Div(i) for i in range(5)]), + { + "type": "div", + "children": [ + {"type": "div", "children": [str(i)], "props": {}} for i in range(5) + ], + "props": {}, + }, + ) + + def test_render_nested_list_children(self): + self.assertRenderJson( + Div([Div([Div(), Div()]), Div([Div(), Div()])]), + { + "type": "div", + "children": [ + { + "type": "div", + "children": [ + {"type": "div", "children": [], "props": {}}, + {"type": "div", "children": [], "props": {}}, + ], + "props": {}, + }, + { + "type": "div", + "children": [ + {"type": "div", "children": [], "props": {}}, + {"type": "div", "children": [], "props": {}}, + ], + "props": {}, + }, + ], + "props": {}, + }, + ) + + def test_render_list_as_component_children(self): + class ItemList(Component): + def __init__(self, items) -> None: + self.items = items + + def render(self): + return Div([Item(item=item) for item in self.items]) + + class Item(Component): + def __init__(self, item) -> None: + self.item = item + + def render(self): + return Div(self.item) + + self.assertRenderJson( + ItemList(items=[f"item{i}" for i in range(1, 5)]), + { + "type": "div", + "children": [ + {"type": "div", "children": [f"item{i}"], "props": {}} + for i in range(1, 5) + ], + "props": {}, + }, + ) \ No newline at end of file diff --git a/tests/test_nested.py b/tests/test_nested.py index 261ef33..efb6958 100644 --- a/tests/test_nested.py +++ b/tests/test_nested.py @@ -1,18 +1,11 @@ -import unittest -from seamless import render from .components import App, Page +from .base import TestCase -class NestedComponentsTest(unittest.TestCase): +class NestedComponentsTest(TestCase): def test_nested_components(self): - self.assertEqual( - render(App()), - '

Card title


Card content
', - ) + self.assertRender(App(), file="html/nested_components.html") def test_page_inheritance(self): self.maxDiff = None - self.assertEqual( - render(Page(App())), - '

Card title


Card content
', - ) + self.assertRender(Page(App()), file="html/page_inheritance.html") diff --git a/tests/test_rendering.py b/tests/test_rendering.py index a1ea742..a679027 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -122,14 +122,14 @@ def render(self): "Hello", Div(), id="my-id", - class_name="my-class", + classes="my-class", ) class MyComponent2(Component): def render(self): return Div( MyComponent(), - class_name="my-class", + classes="my-class", ) self.assertEqual( diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..3b2d5c2 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,26 @@ +from typing import Union +from pathlib import Path + +from pydom import render +from pydom.types import Renderable, Primitive + +ROOT_DIR = Path(__file__).parent + + +def test_render_with_file( + element: Union[Renderable, Primitive], + file: str, + *, + pretty=False, + **kwargs, +) -> bool: + lines = [] + with open(ROOT_DIR / file) as f: + lines = f.readlines() + if not pretty: + lines = [line.strip() for line in lines] + + expected = "".join(lines) + actual = render(element, pretty=pretty, **kwargs) + + return expected == actual From 26fbe48cd2f0b962b3d688d884690776d451f193 Mon Sep 17 00:00:00 2001 From: Neriya Cohen Date: Sun, 30 Mar 2025 02:37:47 +0300 Subject: [PATCH 23/23] Add testing dependencies for FastAPI and Flask --- tests/requirements.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/requirements.txt b/tests/requirements.txt index e69de29..cdd302a 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.109.2 +Flask==3.0.2 +httpx==0.27.0 +python-socketio==5.11.1 +uvicorn==0.27.1 \ No newline at end of file