Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 35 additions & 50 deletions src/vuer/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ class Event:
An event is a message sent from the server to the client.
"""

ts: float
"""
timestamp is a float representing the UTC datetime. Msgpack natively
supports this. `datetime`'s Datetime class is significantly more
complex as it includes timezone information.
"""
ts: float #
# Timestamp is a float representing the UTC datetime. Msgpack natively
# supports this. `datetime`'s Datetime class is significantly more complex
# as it includes timezone information.

value: Optional[dict] # value for client events

data: Optional[dict] # data for server-side events

def __init__(self, ts: Optional[float] = None, **kwargs):
self.ts = Datetime.timestamp(Datetime.now()) if ts is None else ts / 1000

for key, value in kwargs.items():
setattr(self, key, value)

def __eq__(self, etype):
"""
Expand All @@ -29,19 +37,13 @@ def __eq__(self, etype):


class ClientEvent(Event):
value = None
value: Optional[dict] # client-side event payloads.

def __repr__(self):
return f"client<{self.etype}>({self.value})"

def __init__(self, etype=None, ts=None, **kwargs):
if ts is None:
self.ts = Datetime.timestamp(Datetime.now())
else:
self.ts = Datetime.fromtimestamp(ts / 1000)

self.etype = etype
self.__dict__.update(kwargs)
def __init__(self, **kwargs):
super().__init__(**kwargs)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Missing default values for ClientEvent attributes

The refactoring removed default values for etype and value attributes in ClientEvent. The old code set self.etype = None when not provided and had a class attribute value = None. The new code only sets these via kwargs, causing AttributeError when accessed but not provided. This breaks backward compatibility since __eq__ (line 36), __repr__ (line 43), and UPLOAD handling (line 48-54) all access these attributes, expecting them to exist.

Additional Locations (1)

Fix in Cursor Fix in Web


if self == "UPLOAD":
import base64
Expand All @@ -62,26 +64,24 @@ def __init__(self, **kwargs):


class NullEvent(ClientEvent):
etype = "NULL"

def __init__(self, **kwargs):
super().__init__(etype="NULL", **kwargs)
super().__init__(ts=None, **kwargs)


NULL = NullEvent()


class ServerEvent(Event):
def __init__(self, data, etype=None, ts: Union[Datetime, Timedelta] = None, **kwargs):
if ts is None:
self.ts = Datetime.timestamp(Datetime.now())
else:
self.ts = Datetime.fromtimestamp(ts / 1000)
self.data = data
data: Optional[dict] # server-side event payloads

def __init__(self, data, etype: Optional[str] = None, ts: Optional[float] = None, **kwargs):
super().__init__(ts=ts, **kwargs)
self.data = data
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: ServerEvent etype not serialized from class attribute

The refactoring removed self.__dict__.update(etype=self.etype, **kwargs) from ServerEvent.__init__. This line copied the class attribute etype into the instance's __dict__, ensuring it was included in serialization. Without it, when etype is only a class attribute (as in Noop, Set, Update, etc.), it won't be in self.__dict__ and won't be included in the serialized output from _serialize(). The client requires etype to identify event types, so this breaks event communication.

Additional Locations (1)

Fix in Cursor Fix in Web

if etype is not None:
self.etype = etype

self.__dict__.update(etype=self.etype, **kwargs)

def _serialize(self):
"""
Serialize the event to a dictionary for sending over the websocket.
Expand Down Expand Up @@ -137,9 +137,8 @@ class Update(ServerEvent):

etype = "UPDATE"

def __init__(self, *elements: Element, strict=False):
# tuple is not serializable
super().__init__({"nodes": elements}, strict=strict)
def __init__(self, *elements: Element, strict: bool = False):
super().__init__(data={"nodes": elements}, strict=strict)

def _serialize(self):
return {
Expand Down Expand Up @@ -171,13 +170,8 @@ class Add(ServerEvent):

etype = "ADD"

def __init__(self, *elements: List[Element], to: str = None):
# tuple is not serializable
event_data = dict(
nodes=elements,
to=to,
)
super().__init__(data=event_data)
def __init__(self, *elements: Element, to: Optional[str] = None):
super().__init__(data={"nodes": elements, "to": to})

def _serialize(self):
return {
Expand Down Expand Up @@ -209,14 +203,8 @@ class Upsert(ServerEvent):

etype = "UPSERT"

def __init__(self, *elements: List[Element], to: str = None, strict=False):
# tuple is not serializable
event_data = dict(
nodes=elements,
to=to,
strict=strict,
)
super().__init__(data=event_data)
def __init__(self, *elements: Element, to: Optional[str] = None, strict: bool = False):
super().__init__(data={"nodes": elements, "to": to, "strict": strict})

def _serialize(self):
return {
Expand All @@ -230,14 +218,12 @@ def _serialize(self):

class Remove(ServerEvent):
"""
An Update ServerEvent is sent to the client when the server wants to update the client's state.
It appends the data sent in the Update ServerEvent to the client's current state.
REMOVE Operator is used to remove nodes from the scene graph by their keys.
"""

etype = "REMOVE"

def __init__(self, *keys: List[str], **kwargs):
# tuple is not serializable
def __init__(self, *keys: str, **kwargs):
super().__init__(data={"keys": keys}, **kwargs)


Expand Down Expand Up @@ -274,10 +260,9 @@ class Frame(ServerEvent):
A higher-level ServerEvent that wraps other ServerEvents
"""

ServerEvent: ServerEvent
etype = "FRAME"

def __init__(self, data: ServerEvent, frame_rate=60.0, **kwargs):
def __init__(self, data: ServerEvent, frame_rate: float = 60.0, **kwargs):
"""Frame object returns a NOOP client event, to keep the on_socket generator
running at a constant rate.

Expand Down Expand Up @@ -310,8 +295,8 @@ class ServerRPC(ServerEvent):
# we can override this in the constructor to control the behavior on the front end.
rtype = "RPC_RESPONSE@{uuid}"

def __init__(self, data, uuid=None, **kwargs):
self.uuid = uuid or str(uuid4())
def __init__(self, data, uuid: Optional[str] = None, **kwargs):
self.uuid = uuid if uuid is not None else str(uuid4())
super().__init__(data, **kwargs)


Expand Down
21 changes: 1 addition & 20 deletions src/vuer/schemas/html_components.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from fnmatch import fnmatch
from io import BytesIO
from itertools import count
from typing import Literal, Union
Expand All @@ -7,29 +6,11 @@
from PIL import Image as pil_image

from vuer.serdes import IMAGE_FORMATS
from vuer.utils import omit

element_counter = count()


def omit(d, *patterns):
"""Omit keys from dictionary that match any of the glob patterns.

:param d: Dictionary to filter
:param patterns: Glob patterns to match keys against (e.g., "_*", "tag")
:return: New dictionary with matching keys removed

Example::

omit({"foo": 1, "_bar": 2, "tag": 3}, "_*", "tag")
# Returns: {"foo": 1}
"""
result = {}
for k, v in d.items():
if not any(fnmatch(k, pattern) for pattern in patterns):
result[k] = v
return result


class Element:
"""
Base class for all elements
Expand Down
20 changes: 20 additions & 0 deletions src/vuer/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from fnmatch import fnmatch


def omit(d, *patterns):
"""Omit keys from dictionary that match any of the glob patterns.

:param d: Dictionary to filter
:param patterns: Glob patterns to match keys against (e.g., "_*", "tag")
:return: New dictionary with matching keys removed

Example::

omit({"foo": 1, "_bar": 2, "tag": 3}, "_*", "tag")
# Returns: {"foo": 1}
"""
result = {}
for k, v in d.items():
if not any(fnmatch(k, pattern) for pattern in patterns):
result[k] = v
return result
Empty file removed your_script.py
Empty file.