8000 typed Events by NiklasNeugebauer · Pull Request #264 · zauberzeug/rosys · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

typed Events #264

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 20, 2025
Merged

typed Events #264

merged 5 commits into from
Mar 20, 2025

Conversation

NiklasNeugebauer
Copy link
Contributor
@NiklasNeugebauer NiklasNeugebauer commented Feb 28, 2025

I have have had a lot of situations where I was not exactly sure which values an event would produce or these values changed but the consumer was not changed correctly with it.

This PR adds a Generic ParamSpec to the Event class which enables users to optinally initialize their events as

my_event = Event[int, str]()

or equivalently

my_event: Event[int, str] = Event

to let the type checker correctly check the types of the callback in my_event.register() and the other functions.

@NiklasNeugebauer NiklasNeugebauer added this to the 0.23.0 milestone Feb 28, 2025
Copy link
Contributor Author
@NiklasNeugebauer NiklasNeugebauer left a comment

Choose a reason for hiding this comment

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

The current issue with this is:
MyPy wants a type annotation (even my_event: Event = Event() would be enough).
This means either:

  1. We find a way to make this work with MyPy without needing the annotation
  2. All event initializations need to be updated

In my opinion all events should be typed so I would tend to option 2, but of course that would place a slight burden on users using MyPy, too.

@NiklasNeugebauer NiklasNeugebauer changed the title feat: add generic ParamSpec to events typed Events Feb 28, 2025
@falkoschindler
Copy link
Contributor

@NiklasNeugebauer I also like option 2. But we have to see how it turns out once we fixed all linter and type errors:
Missing type annotations can be easily added for event definitions.
But there are also errors that "Cannot infer type of lambda", where Cursor suggests to simply add # type: ignore.

@falkoschindler falkoschindler added the enhancement New feature or request label Mar 3, 2025
@NiklasNeugebauer
Copy link
Contributor Author
NiklasNeugebauer commented Mar 4, 2025

Hm, you're right, it seems simply typing empty events as Event is not enough.
The correct way to type empty events would be:

empty = Event[[]]()
empty.register(lambda: print('empty'))

We could add a global EmptyEvent: TypeVar = Event[[]] to improve ease of use.
Another option in the Event class:

class Event(Generic[P]):
    ...
    @staticmethod
    def Empty() -> Event[[]]:
        return Event()

Otherwise, we could maybe keep the Event class as is and add a TypedEvent Subclass?

@NiklasNeugebauer
Copy link
Contributor Author

As far as I can tell, ParamSpec defaults have been introduced in 3.13

https://docs.python.org/3/whatsnew/3.13.html
https://peps.python.org/pep-0696/
https://typing.python.org/en/latest/spec/generics.html#type-parameter-defaults
example from the last link (docs):

DefaultP = ParamSpec("DefaultP", default=[str, int])

class Foo(Generic[DefaultP]): ...

reveal_type(Foo)                  # type is type[Foo[DefaultP = [str, int]]]
reveal_type(Foo())                # type is Foo[[str, int]]
reveal_type(Foo[[<
8000
span class="pl-s1">bool, bool]]())  # type is Foo[[bool, bool]]

This would be very nice to have

@NiklasNeugebauer
Copy link
Contributor Author
NiklasNeugebauer commented Mar 6, 2025

This is the best I could come up with for a warning

class Event(Generic[P]):
    def __class_getitem__(cls, item):
        print('__class_getitem__', cls, item)
        setattr(cls, '__getitem_called', True)
        return super().__class_getitem__(item)

    def __new__(cls, *args, **kwargs):
        print('__new__', cls, args, kwargs)
        if not hasattr(cls, '__getitem_called'):
            log.warning(
                'The Event class was instantiated without a type argument. '
                'If you wanted to indicate that the event is emitted without any arguments, '
                'use Event[[]] instead.'
            )
            return Event[[]].__new__(cls)
        return super().__new__(cls)

@falkoschindler
Copy link
Contributor

I've got some mypy/pylint complaints, so I changed it to

    def __class_getitem__(cls, _item: Any) -> type[Event[P]]:
        setattr(cls, '__getitem_called', True)
        return cls

    def __new__(cls, *args, **kwargs) -> Event[P]:
        if not hasattr(cls, '__getitem_called'):
            log.warning(
                'The Event class was instantiated without a type argument. '
                'If you wanted to indicate that the event is emitted without any arguments, use Event[[]] instead.'
            )
            return Event[[]].__new__(cls)
        return super().__new__(cls, *args, **kwargs)

But I wonder if we should raise an exception with a helpful text instead of "fixing" user code. This would also allow to write a pytest more easily.

@codingpaula
Copy link
Contributor

I'd argue for raising instead of "fixing". While switching to typed events in our code, everything would otherwise assume it's an event without an argument which some new user might have wanted to achieve, but for us wouldn't be the case.

@NiklasNeugebauer
Copy link
Contributor Author
NiklasNeugebauer commented Mar 13, 2025

I've got some mypy/pylint complaints, so I changed it to

    def __class_getitem__(cls, _item: Any) -> type[Event[P]]:
        setattr(cls, '__getitem_called', True)
        return cls

    def __new__(cls, *args, **kwargs) -> Event[P]:
        if not hasattr(cls, '__getitem_called'):
            log.warning(
                'The Event class was instantiated without a type argument. '
                'If you wanted to indicate that the event is emitted without any arguments, use Event[[]] instead.'
            )
            return Event[[]].__new__(cls)
        return super().__new__(cls, *args, **kwargs)

But I wonder if we should raise an exception with a helpful text instead of "fixing" user code. This would also allow to write a pytest more easily.

I did not test this properly before. This does not really work since all Events are of the same class and thus there is only one single __getitem_called__ flag.
This means if Event was ever called with a type (as is now done by rosys itself), then it will never trigger again.

I suggest we just don't do it then.

@NiklasNeugebauer
Copy link
Contributor Author

I did not test this properly before. This does not really work since all Events are of the same class and thus there is only one single __getitem_called__ flag. This means if Event was ever called with a type (as is now done by rosys itself), then it will never trigger again.

I suggest we just don't do it then.

@falkoschindler @codingpaula I would love you to comment on that, is this PR approved then?

Copy link
Contributor
@falkoschindler falkoschindler left a comment

Choose a reason for hiding this comment

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

@NiklasNeugebauer Oh, I didn't notice that the PR is ready for a final review.
But yes, let's merge this one!

@NiklasNeugebauer NiklasNeugebauer merged commit 10c7372 into main Mar 20, 2025
5 checks passed
@NiklasNeugebauer NiklasNeugebauer deleted the typed-events branch March 20, 2025 13:24
Johannes-Thiel pushed a commit to zauberzeug/field_friend that referenced this pull request Apr 1, 2025
Update to [RoSys
0.23.0](https://github.com/zauberzeug/rosys/releases/tag/v0.23.0) which
includes features that were already needed on main.
zauberzeug/rosys#264 introduced typed events as
part of 0.23.0, some events had to be annotated because of it.

Was tested on U4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
0