1
0
mirror of https://github.com/aewens/ircevents synced 2024-06-15 05:46:36 +00:00

Refactored process function to split concerns, made namespaces optional if only one is given, event loop will iterate over recv if iterable

This commit is contained in:
aewens 2020-03-14 11:44:23 -05:00
parent ab10722f88
commit 2fa9de3c6d

View File

@ -1,6 +1,7 @@
from .helpers import infinitedict, trap
from collections import namedtuple
from collections.abc import Iterable
from functools import wraps
from threading import Thread, Event
from queue import Queue, Empty
@ -36,8 +37,8 @@ class StateManager:
def __init__(self):
pass
def get(self, key):
getattr(self, key, None)
def get(self, key, default=None):
getattr(self, key, default)
def set(self, key, value):
return setattr(self, key, value)
@ -64,6 +65,7 @@ class Engine:
self._recv_kwargs = dict()
self._states = defaultdict(lambda: StateManager())
self._mutations = dict()
self._namespaces = set()
self._whens = set()
@ -93,10 +95,114 @@ class Engine:
# Returns both the key and value to return like a dict
yield (attr_name, attribute)
def ns_get(self, namespace, key):
self._states[namespace].get(key)
def _apply_mutations(self):
"""
Applies mutations and passes them on as generator
"""
for using in self._using:
mutation = using.callback(raw_line)
self._mutations[using.namespace] = mutation
yield (using, mutation)
def _process_mutations(self, requires, skip_whens):
"""
Run all mutation variables against callbacks to see if applicable
"""
for (using, mutation) in self._apply_mutations():
for (key, value} in self._get_variables(mutation):
for using_whens in self._whens_map[key]:
for using_when in using_whens:
# Already been triggers, skip
if using_when in skip_whens:
continue
self._process_when(using_when, requires)
def _process_when(self, when, requires):
"""
Determines if required keys are present to check callback
If so, will run the callback with the mutation and namespace state
"""
# Check if all required fields are found
whens_requires = requires.get(when)
if whens_requires is None:
requires[whens] = set(self._whens._fields)
whens_requires = requires.get(whens)
whens_requires.remove(key)
if len(whens_requires) > 0:
return None
triggered = self._check_when(when)
if not triggered:
return None
state = self._states[namespace]
func = self._whens_funcs.get(when)
if func is None:
return None
# Run callback using mutation data and state manager
func(data, state)
def _check_when(self, when):
"""
Checks if mutation state will trigger callback
"""
# If all requirements are found, stop checking this
skip_whens.add(when)
namespace = self._whens_namespaces.get(when)
data = mutations.get(namespace)
if None in [namespace, data]:
continue
trigger_when = True
# Use magic _always pair to always trigger callback
if when.get("_always") is True:
return trigger_when
# Check if conditions match mutation
for when_key, when_value in when.items():
when_path = when_key.split("__")
pointer = data
for wpath in when_path:
if not isinstance(pointer, dict):
eprint(f"Invalid path: {when_key}")
break
pointer = pointer.get(wpath)
when_status = False
# Value can be a function complex checks
if callable(when_value):
when_status = when_value(pointer)
else:
when_status = pointer == when_value
if not when_status:
trigger_when = False
break
return trigger_when
def ns_get(self, namespace, key, default=None):
"""
Shortcut to get namespace value outside of callback
"""
self._states[namespace].get(key, default)
def ns_set(self, namespace, key, value):
"""
Shortcut to set namespace value outside of callback
"""
self._states[namespace].set(key, value)
def use(self, namespace, callback):
@ -107,13 +213,17 @@ class Engine:
Mutation = namedtuple("Mutation", ["name", "callback"])
self._using.add(Mutation(namespace, callback))
def when(self, namespace, **when_kwargs):
def when(self, namespace=None, **when_kwargs):
"""
Decorator used to flag callback functions that the engine will use
The namespace decides what scope of object to pass to callback
The when keyword arguments determine what will trigger the callback
"""
# Namespaces are optional if only one is given
if namespace is None and len(self._namespaces) == 1:
namespace = self._namespaces[0]
assert namespace in self._namespaces, f"Invalid namespace: {namespace}"
# Make hashable for set
@ -145,80 +255,18 @@ class Engine:
Applies mutations to the raw IRC text and checks it against callbacks
"""
mutations = dict()
# Clear out previous mutations
self._mutations = dict()
requires = dict()
triggered_whens = set()
skip_whens = set()
# Apply mutations
for using in self._using:
mutation = using.callback(raw_line)
mutations[using.namespace] = mutation
for (key, value} in self._get_variables(mutation):
for using_whens in self._whens_map[key]:
for using_when in using_whens:
# Already been triggers, skip
if using_when in skip_whens:
continue
# Check if all required fields are found
whens_requires = requires.get(using_when)
if whens_requires is None:
requires[using_whens] = set(self._whens._fields)
whens_requires = requires.get(using_whens)
whens_requires.remove(key)
if len(whens_requires) == 0:
# If all requirements are found, stop checking this
skip_whens.add(using_when)
namespace = self._whens_namespaces.get(using_when)
data = mutations.get(namespace)
if None in [namespace, data]:
continue
# Check if conditions match mutation
trigger_when = True
for when_key, when_value in using_when.items():
when_path = when_key.split("__")
pointer = data
for wpath in when_path:
if not isinstance(pointer, dict):
eprint(f"Invalid path: {when_key}")
break
pointer = pointer.get(wpath)
when_status = False
# Value can be a function complex checks
if callable(when_value):
when_status = when_value(pointer)
else:
when_status = pointer == when_value
if not when_status:
trigger_when = False
break
if not trigger_when:
continue
triggered_whens.add(using_when)
state = self._states[namespace]
func = self._whens_funcs.get(using_when)
if func is None:
continue
# Run callback using mutation data and state manager
func(data, state)
self._process_mutations(requires, skip_whens)
def pre_process(self, callback, *args, **kwargs):
"""
Anything that needs to be run before each new line is processed
"""
assert callable(callback), f"Expected function but got: {callback}"
self._pre_callback = callback
self._pre_args = args
@ -228,6 +276,7 @@ class Engine:
"""
Anything that needs to be run after each new line is processed
"""
assert callable(callback), f"Expected function but got: {callback}"
self._post_callback = callback
self._post_args = args
@ -237,6 +286,7 @@ class Engine:
"""
What to run against the source to receive data
"""
assert callable(callback), f"Expected function but got: {callback}"
self._recv_callback = callback
self._recv_args = args
@ -246,6 +296,7 @@ class Engine:
"""
Passes stop signal to event loop in run function
"""
self._running.set()
def run(self):
@ -253,14 +304,16 @@ class Engine:
The event loop that drives the engine
Will loop indefinitely until the stopped or gets an exception
"""
# Run until stopped
while not self._running.is_set():
# Run pre callback before processing
self._pre_callback(*self._pre_args, **self._pre_kwargs)
self._pre_callback(self._source, *self._pre_args,
**self._pre_kwargs)
try:
# Extract raw text from source using recv callback
raw_text = self._recv_callback(self._source, *self._recv_args,
# Extract receive data from source using recv callback
recv_data = self._recv_callback(self._source, *self._recv_args,
**self._recv_kwargs)
except Exception as e:
@ -268,8 +321,15 @@ class Engine:
eprint(format_exc())
break
# Process raw text
self.process(raw_text)
# Process received data
if isinstance(recv_data, Iterable):
# If iterable, iterate over lines
for recv in recv_data:
self.process(recv)
else:
self.process(recv_data)
# Run post callback before processing
self._post_callback(*self._post_args, **self._post_kwargs)
self._post_callback(self._source, *self._post_args,
**self._post_kwargs)