-
-
Notifications
You must be signed in to change notification settings - Fork 328
Make event data a Python object that allows for deeply nested access via dot notation
#1288
-
I'm never a fan of having to write my event handlers in ReactPy, since the provided data args are returned as dictionaries that must be accessed with brackets and quoted key names. Like so:
def handle_button_click(e):
value = e['target']['value']
It would be awesome to have those events converted to an object that allows for full access to all of the theoretically infinitely-nested dictionary properties, just like in JavaScript. Like so:
def handle_button_click(e):
value = e.target.value
I already implemented this locally as a quick prototype using munch by editing the to_event_handler_function of /src/reactpy/core/events.py like so:
+ from munch import munchify def to_event_handler_function( function: Callable[..., Any], positional_args: bool = True, ) -> EventHandlerFunc: """Make a :data:`~reactpy.core.proto.EventHandlerFunc` from a function or coroutine Parameters: function: A function or coroutine accepting a number of positional arguments. positional_args: Whether to pass the event parameters a positional args or as a list. """ if positional_args: if asyncio.iscoroutinefunction(function): async def wrapper(data: Sequence[Any]) -> None: + await function(*[munchify(d) for d in data]) else: async def wrapper(data: Sequence[Any]) -> None: + function(*[munchify(d) for d in data]) return wrapper elif not asyncio.iscoroutinefunction(function): async def wrapper(data: Sequence[Any]) -> None: + function(munchify(data)) return wrapper else: return function
I found out about munch from this stackoverflow thread. There are some responses that warn of the costly time overhead that munch adds - not to mention dependency overhead. It'd probably be simple enough to create a custom class that could also handle commonly expected cases, such as keys with dashes (e.g. aria-label) by converting the dashes to underscores for the dot accessor (e.g. e.aria_label). These solutions of course allow for the standard dictionary access, so have full backwards compatability.
Any thoughts or concerns?
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 3 comments 6 replies
-
@Archmonger Curious if you have any thoughts on this. I'd love to possibly make this the next PR if we can come to an agreement on if/how it should be done.
Beta Was this translation helpful? Give feedback.
All reactions
-
Actually, after researching and thinking about it more, I think the overhead probably isn't worth it. Anyone who truly wants this could just require munch or an alternative for their own project and add the extra one-liner to convert the event arguments where desired.
Although... It could be cool to have it configurable somehow... Maybe from the eventHandler object. Alongside stop_propagation and prevent_default add event_as_attrs or something as an option... I kind of like that. But yeah, it seems we'd want to develop a custom class that would convert props with dashes into underscores on the attribute side among potential other things.
Beta Was this translation helpful? Give feedback.
All reactions
-
Then again, having to add a wrapper to a function is about the same difference as adding a one liner inside the function... Although there is less dependency overhead if reactpy had something built in. I played around and this probably gets it mostly there:
class AttrDict(dict):
@staticmethod
def _is_iterable(obj):
try:
iter(obj)
return True
except TypeError:
return False
def __init__(self, d):
for k, v in d.items():
new_k = k.replace('-', '_')
new_v = [AttrDict(i) for i in v if isinstance(i, dict)] if self._is_iterable(v) else v
setattr(self, new_k, new_v)
super(AttrDict, self).__init__(d)
I'm gonna try and do a few tests on performance... Some JS event object can be quite big.
Beta Was this translation helpful? Give feedback.
All reactions
-
Related issue: #1143
I'll be getting off work late today so I might have to write up my thoughts on this tomorrow.
As a summary, we probably can't directly use munch and we'll need to create our own light wrapper (similar to reactpy.vdom.Vdom).
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
I did a bit more testing and came up with this:
class AttrDict(dict): """Converts dictionary into object with keys as attributes""" def __init__(self, d): for k, v in d.items(): new_k = k.replace("-", "_") while hasattr(self, new_k): new_k = f"{new_k}_" new_v = ( [AttrDict(i) if isinstance(i, dict) else i for i in v] if isinstance(v, list) else v ) setattr(self, new_k, new_v) super(AttrDict, self).__init__(d)
Beta Was this translation helpful? Give feedback.
All reactions
-
Still at work so responses will be brief.
Preemptively converting the elements isn't going to be performant. We will need to keep the elements stored in their original format, and we should just mutate the lookup (getattribute).
Beta Was this translation helpful? Give feedback.
All reactions
-
class Event(dict): def __getattr__(self, key: Literal["example", "foo", "bar"] | str) -> Any: try: return self[self.snake_to_camel(key)] except KeyError as e: raise AttributeError(f"'Event has no attribute '{key}'") from e @staticmethod def snake_to_camel(snake_str): words = snake_str.split('_') camel_case = words[0] + ''.join(word.capitalize() for word in words[1:]) return camel_case
Beta Was this translation helpful? Give feedback.
All reactions
-
Had more time to think about this. We might not be able to convert from snake_case to camelCase without risking compatibility issues. Here's a more fleshed out rough draft.
class DotNotationDict: """A dictionary that allows dot notation access to its keys.""" _dict: dict[str, Any] def __init__(self, initial: dict[str, Any]) -> None: # Create an internal dictionary in a way that won't cause infinite recursion # This dict is stored via reference, allowing us to proxy our "dot notation" with zero performance impact object.__setattr__(self, "_dict", initial) def __getitem__(self, key: str) -> Any: """Get an item from the internal dictionary.""" value = self._dict[key] # Convert plain dictionaries on-demand if type(value) is dict: return DotNotationDict(value) # If the value is not a dictionary, return it as is return value def __setitem__(self, key: str, value: Any) -> None: """Set an item in the internal dictionary.""" self._dict[key] = value def __getattr__(self, key: str) -> Any: """Fetch the value from the internal dictionary in a way that won't cause infinite recursion.""" return object.__getattribute__(self, "__getitem__")(key) def __setattr__(self, key: str, value: Any) -> None: """Set the value in the internal dictionary in a way that won't cause infinite recursion.""" object.__getattribute__(self, "__setitem__")(key, value) def __repr__(self) -> str: """Return a string representation of the object.""" return str(self._dict) class Event(DotNotationDict): """A class representing an event with dot notation access. This is utilized to provide type hints and callback functions. """ # TODO: Use this class to deprecate accessing attributes via square brackets (provide a warning msg) # TODO: Remove the @event decorator from ReactPy when `preventDefault` and `stopPropogation` callbacks are implemented. # TODO: Add better type hints for the attributes of each key # Event dict attributes target: dict[str, Any] shiftKey: bool screenX: int screenY: int type: str # Callback functions preventDefault: Callable[[], None] stopPropogation: Callable[[], None]
Beta Was this translation helpful? Give feedback.
All reactions
-
On a different note, I'm glad you brought this up. This is one of the three things I've been wanting merged prior to v2.
Beta Was this translation helpful? Give feedback.