Skip to content

[Feature request and solution] Add optional priority to event listeners #8

@malversan

Description

@malversan

First of all, thanks for your quality library. I will try to contribute a bit with a slight modification to add a nice and compatible feature (in my case necessary).

Feature request:
Add optional priority to event listeners.

Scenario:
You have an event emitter and you attach several listeners to a specific event, but you want to assure that the listeners execute in a specific order. Currently this is impossible if you attach the listeners in different application levels.

Real-world example:

def logEvent():
     log("Event just happened.")

def handleEvent():
     ...
     log("Event was handled.")

emitter.on("event", logEvent)     # This statement runs in one application place...
emitter.on("event", handleEvent)  # ... and this statement runs in another application place.

If you do not control in which order the listeners are assigned, you could end with something like this, which may not be the desired:

LOG: Event was handled.
LOG: Event just happened.

But if you could optionally assign priorities to listeners, you would always end with the desired execution order.

emitter.on("event", logEvent, EventPriority.FIRST)
emitter.on("event", handleEvent)

LOG: Event just happened.
LOG: Event was handled.

Solution:
events.zip

With the currently implemented listener-wrapper solution, it is very easy to add the ability to assign priorities to listeners and keep things 100% backwards compatible.

__all__ = ['EventPriority', 'EventEmitter', 'on', 'once']


class EventPriority():
	# This enumeration is just syntactic sugar for specifying events priority. Any number will do, positive or negative, real or float.
	# Higher numbers denote higher priority.

	CRITICAL	= 10000

	FIRST		= 1000

	VERY_HIGH	= 200
	HIGH		= 100

	OVER_NORMAL	= 10
	NORMAL		= 0
	BELOW_NORMAL	= -10

	LOW		= -100
	VERY_LOW	= -200

	LAST		= -1000


class ListenerWrapper(object):

	def __init__(self, listener, is_once=False, priority=None):
		self.listener = listener
		self.is_once = is_once
		self.priority = priority if isinstance(priority, (int, float)) else EventPriority.NORMAL

	def __call__(self, *args, **kwargs):
		self.listener(*args, **kwargs)

	def __eq__(self, other):
		if isinstance(other, ListenerWrapper):
			return other.listener == self.listener
		if callable(other):
			return other == self.listener
		return False


class EventEmitter(object):

	def __init__(self):
		self._events = {}

	def on(self, event, listener, priority=None):
		self._on(event, ListenerWrapper(listener, priority=priority))

	def once(self, event, listener, priority=None):
		self._on(event, ListenerWrapper(listener, is_once=True, priority=priority))

	def _on(self, event, listener_wrapper):
		if not event in self._events:
			self._events[event] = []
		self._events[event].append(listener_wrapper)

	def emit(self, event, *args, **kwargs):
		if event in self._events:
			# Use a copy of listeners sorted by priority.
			listeners = sorted(self._events[event], key=lambda listener: listener.priority, reverse=True)
			for listener in listeners:
				if listener.is_once:
					self.remove(event, listener)
				listener(*args, **kwargs)

	def remove(self, event, listener):
		if event in self._events:
			events = self._events[event]
			if listener in events:
				events.remove(listener)

	def remove_all(self, event):
		if event in self._events:
			self._events[event] = []

	def count(self, event):
		return len(self._events[event]) if event in self._events else 0


def on(emitter, event, priority=None):
	def decorator(func):
		emitter.on(event, func, priority=priority)
		return func
	return decorator


def once(emitter, event, priority=None):
	def decorator(func):
		emitter.once(event, func, priority=priority)
		return func
	return decorator

Notes:
I hope you find this useful. This is the implementation I am currently using and it works flawlessly.

I also corrected one little unrelated issue. I modified the listener wrapper comparison to accomodate to the fact that a listener can be any callable, not only a function but also an instance method, static method, etc.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions