Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Make event data a Python object that allows for deeply nested access via dot notation #1288

shawncrawley started this conversation in Ideas
Discussion options

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?

You must be logged in to vote

Replies: 3 comments 6 replies

Comment options

@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.

You must be logged in to vote
2 replies
Comment options

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.

Comment options

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.

Comment options

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).

You must be logged in to vote
4 replies
Comment options

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)
Comment options

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).

Comment options

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
Comment options

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]
Comment options

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.

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Ideas
Labels
None yet

AltStyle によって変換されたページ (->オリジナル) /