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

Reusable hooking mechanism for Python code #3393

Closed
nvaccessAuto opened this Issue Aug 2, 2013 · 4 comments

Comments

Projects
None yet
4 participants
@nvaccessAuto

nvaccessAuto commented Aug 2, 2013

Reported by jteh on 2013-08-02 07:20
We need to come up with a simple, reusable hooking mechanism so code can register hooks for various tasks. For example, add-ons may wish to tweak speech output or respond to configuration profile switches.

There are existing frameworks for this like PubSub. However, from what I've been able to find, they can publish and subscribe, but they can't really synchronously hook; e.g. changing speech output. Also, they tend to use arbitrary text strings and I tend to think some sort of object to register with for each hook is nicer.

Requirements:

  1. Simple.
  2. Synchronous.
  3. A hook should be able to take arguments, return values and maybe raise exceptions.
  4. Depending on the hook point, code should perhaps be able to decide to stop executing later hooks.
  5. Hooks need to be possible in both modules and class instances.
  6. It'd be nice if code wanting to hook didn't have to explicitly register and unregister hooks. For example, if a hooking instance dies, the hook dies. This avoids boilerplate and stupid errors.

The easiest implementation is to have an object for each hook which has methods to register and unregister hooks. However, that fails requirement 6.

Another option is to have a decorator which you use to implement a hook; e.g.

@config.ProfileSwitchListener
def onConfigProfileSwitch():
 ...

Unfortunately, doing this in a way that satisfies both requirements 5 and 6 is pretty tricky. I haven't come up with a way yet.

@nvaccessAuto

This comment has been minimized.

Show comment
Hide comment
@nvaccessAuto

nvaccessAuto Aug 2, 2013

Comment 1 by ragb on 2013-08-02 09:11
I remember we discussed something like this when designing the add-ons system (#213), although the scope was less general. Anyway, as this is something that interests me much, count me to help with implementation.

Regarding requirement 6, I can imagine how one may unregister class-based hooks, with some metaclass or mix-ins stuff (Don't have a proper design though), however I don't know if it is possible to do that in module-based hooks (do we know when a module gets deleted in Python?).

nvaccessAuto commented Aug 2, 2013

Comment 1 by ragb on 2013-08-02 09:11
I remember we discussed something like this when designing the add-ons system (#213), although the scope was less general. Anyway, as this is something that interests me much, count me to help with implementation.

Regarding requirement 6, I can imagine how one may unregister class-based hooks, with some metaclass or mix-ins stuff (Don't have a proper design though), however I don't know if it is possible to do that in module-based hooks (do we know when a module gets deleted in Python?).

@nvaccessAuto

This comment has been minimized.

Show comment
Hide comment
@nvaccessAuto

nvaccessAuto Aug 2, 2013

Comment 2 by jteh (in reply to comment 1) on 2013-08-02 10:29
Replying to ragb:

Regarding requirement 6, I can imagine how one may unregister class-based hooks, with some metaclass or mix-ins stuff (Don't have a proper design though), however I don't know if it is possible to do that in module-based hooks (do we know when a module gets deleted in Python?).

I should have been clearer. Requirement 6 can't be satisfied for modules, only for object instances. I was thinking of some sort of token that the module/instance holds. When the token is dropped, the hook is unregistered. That way, modules can just drop it if they want to unregister. Instances should just drop it when they get garbage collected. The trick is doing that without creating circular references and associated gc leaks.

nvaccessAuto commented Aug 2, 2013

Comment 2 by jteh (in reply to comment 1) on 2013-08-02 10:29
Replying to ragb:

Regarding requirement 6, I can imagine how one may unregister class-based hooks, with some metaclass or mix-ins stuff (Don't have a proper design though), however I don't know if it is possible to do that in module-based hooks (do we know when a module gets deleted in Python?).

I should have been clearer. Requirement 6 can't be satisfied for modules, only for object instances. I was thinking of some sort of token that the module/instance holds. When the token is dropped, the hook is unregistered. That way, modules can just drop it if they want to unregister. Instances should just drop it when they get garbage collected. The trick is doing that without creating circular references and associated gc leaks.

@leonardder

This comment has been minimized.

Show comment
Hide comment
@leonardder

leonardder Jul 15, 2017

Collaborator

@jcsteh: How is this related to #4877, e.g. will #4877 contain code that will bring this issue forward somehow?

Collaborator

leonardder commented Jul 15, 2017

@jcsteh: How is this related to #4877, e.g. will #4877 contain code that will bring this issue forward somehow?

@jcsteh

This comment has been minimized.

Show comment
Hide comment
@jcsteh

jcsteh Jul 17, 2017

Contributor

This is a prerequisite for all work on #4877. I'm working on the design for this at present.

Contributor

jcsteh commented Jul 17, 2017

This is a prerequisite for all work on #4877. I'm working on the design for this at present.

@jcsteh jcsteh self-assigned this Jul 17, 2017

@derekriemer derekriemer added feature and removed enhancement labels Jul 17, 2017

@jcsteh jcsteh assigned jcsteh and unassigned jcsteh Sep 5, 2017

@jcsteh jcsteh closed this in #7484 Sep 8, 2017

@nvaccessAuto nvaccessAuto added this to the 2017.4 milestone Sep 8, 2017

jcsteh added a commit that referenced this issue Sep 8, 2017

Add generic framework to enable extensibility at specific points in t…
…he code by way of actions, filters and deciders. (issue #3393, PR #7484)

This allows interested parties to register to be notified when some action occurs, to modify a specific kind of data or to participate in deciding whether something should be done.

There are three types of extension points:

1. An Action might be used to notify interested parties that the configuration profile has been switched.
2. A Filter might be used to allow interested parties to modify spoken messages before they are passed to the synthesizer.
3. A Decider might be used to allow interested parties to prevent execution of input gestures under specific circumstances.

Handlers for extension points are always called in the order they were registered so that the order is determinate, which should make behaviour more predictable and easier to reproduce.

Handlers can be called with keyword arguments.
If a handler doesn't support a particular keyword argument, it will still be called with the keyword arguments that it does support.
This means that additional keyword arguments can be added to actions and filters in future without breaking existing handlers.

Add a config.configProfileSwitched action and have synthDriverHandler, braille, etc. register for it instead of explicitly calling each handler from the config module.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment