Compare commits

..
Author SHA1 Message Date
deepend 66c93fe2e1 expose main-loop file descriptor hooks
(cherry picked from commit 92d39cd077)
2026-11-08 06:04:48 -06:00
7 changed files with 81 additions and 105 deletions

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
# Python descriptor hooks
`hook_fd(fd, callback, userdata=None, flags=FD_READ)` returns an ordinary hook
handle accepted by `unhook()`. Callback arguments are `(fd, ready_flags, userdata)`;
return True to retain the watch and False/None to remove it. Exceptions print a
traceback and remove the watch. Unloading the script removes its watches.
The callback runs in the context captured during registration, and that context
is restored afterwards. A closed context removes the watch.
Use nonblocking IO and bounded reads/writes. The descriptor belongs to the script;
unhook before closing it. On Windows socket descriptors use the default flags;
CRT descriptors need FD_NOTSOCKET. The existing C API uses int descriptors: values
outside its range are rejected rather than truncated. This does not add threads.
Example:
```python
watch = zoitechat.hook_fd(sock.fileno(), on_readable, flags=zoitechat.FD_READ)
# def on_readable(fd, flags, userdata): ...; return True
```
-21
View File
@@ -1,21 +0,0 @@
# Explicit IRC comparison keys
`IRCName(text, context=None, casemapping=None, connection_id=None)` is an
immutable helper, not a str subclass. Construct it explicitly for dictionary/set
keys; existing get_info/list/event values remain normal strings.
Equality and hashing include the connection ID and captured casemapping, so
identical-looking names on different connections are different keys. An IRCName
never compares equal to a plain str. `str(key)` / `key.text` recover the spelling.
`irc_casefold(text, casemapping='rfc1459')` supports ascii, strict-rfc1459 and
rfc1459. Only ASCII IRC characters fold; Unicode casefold would be incorrect.
The new `get_info('casemapping')` reports the comparator actually used by core.
Current master handles ASCII and RFC1459; this patch does not change its handling
of advertised strict-rfc1459. An explicit mapping is available for scripts that
need strict-rfc1459 independently. Keys capture their mapping so hashes stay
stable; rebuild a collection if the connection's mapping changes.
```python
seen = {zoitechat.IRCName('[SomeNick]')}
assert zoitechat.IRCName('{somenick}') in seen # RFC1459 connection
```
+39
View File
@@ -0,0 +1,39 @@
"""File descriptor readiness hooks; callbacks execute on the main thread."""
import operator
import _zoitechat as api
from _zoitechat_embedded import ffi, lib
__all__ = ['FD_READ', 'FD_WRITE', 'FD_EXCEPTION', 'FD_NOTSOCKET', 'hook_fd']
FD_READ, FD_WRITE, FD_EXCEPTION, FD_NOTSOCKET = 1, 2, 4, 8
def hook_fd(fd, callback, userdata=None, flags=FD_READ):
"""callback(fd, ready_flags, userdata): True keeps the watch, False removes it."""
fd = operator.index(fd)
flags = operator.index(flags)
if fd < 0 or fd > 2147483647:
raise ValueError('fd must fit the C API nonnegative int descriptor')
if flags & ~15 or not flags & 7:
raise ValueError('flags must select READ, WRITE or EXCEPTION')
if not callable(callback):
raise TypeError('callback must be callable')
plugin = api.__get_current_plugin()
context = api.get_context()
def dispatch(descriptor, ready, data):
previous = api.get_context()
if not context.set():
return False
try:
return callback(descriptor, ready, data)
finally:
previous.set()
hook = plugin.add_hook(dispatch, userdata)
handle = lib.zoitechat_hook_fd(lib.ph, fd, flags, lib._on_fd_hook, hook.handle)
if handle == ffi.NULL:
hook.is_unload = True
plugin.remove_hook(id(hook))
raise RuntimeError('unable to watch descriptor')
hook.zoitechat_hook = handle
return id(hook)
-74
View File
@@ -1,74 +0,0 @@
"""Explicit immutable IRC names; existing string-returning APIs are unchanged."""
import _zoitechat as api
__all__ = ['IRCName', 'irc_casefold']
def irc_casefold(text, casemapping='rfc1459'):
"""Fold ASCII IRC characters only; never apply Unicode lower()/casefold()."""
if casemapping not in ('ascii', 'strict-rfc1459', 'rfc1459'):
raise ValueError('unsupported IRC casemapping')
extra = {'[': '{', ']': '}', '\\': '|'} if casemapping != 'ascii' else {}
if casemapping == 'rfc1459':
extra['^'] = '~'
return ''.join(chr(ord(c) + 32) if 'A' <= c <= 'Z' else extra.get(c, c)
for c in text)
class IRCName:
"""Equality/hash include connection identity and the captured casemapping."""
__slots__ = ('_text', '_casemapping', '_connection_id', '_key')
def __init__(self, text, context=None, casemapping=None, connection_id=None):
context = context or api.get_context()
if casemapping is None:
casemapping = context.get_info('casemapping')
if casemapping is None:
raise RuntimeError('host does not expose its comparison casemapping')
if connection_id is None:
previous = api.get_context()
if not context.set():
raise ValueError('context has closed')
try:
connection_id = api.get_prefs('id')
finally:
previous.set()
object.__setattr__(self, '_text', text)
object.__setattr__(self, '_casemapping', casemapping)
object.__setattr__(self, '_connection_id', connection_id)
object.__setattr__(self, '_key', (connection_id, casemapping,
irc_casefold(text, casemapping)))
def __setattr__(self, name, value):
raise AttributeError('IRCName is immutable')
@property
def text(self):
return self._text
@property
def casemapping(self):
return self._casemapping
@property
def connection_id(self):
return self._connection_id
def __str__(self):
return self._text
def __repr__(self):
return 'IRCName({!r}, casemapping={!r}, connection_id={!r})'.format(
self._text, self._casemapping, self._connection_id)
def __eq__(self, other):
if not isinstance(other, IRCName):
return NotImplemented
return self._key == other._key
def __ne__(self, other):
result = self.__eq__(other)
return NotImplemented if result is NotImplemented else not result
def __hash__(self):
return hash(self._key)
+1
View File
@@ -38,6 +38,7 @@ extern "Python" int _on_print_attrs_hook(char **, zoitechat_event_attrs *, void
extern "Python" int _on_server_hook(char **, char **, void *);
extern "Python" int _on_server_attrs_hook(char **, char **, zoitechat_event_attrs *, void *);
extern "Python" int _on_timer_hook(void *);
extern "Python" int _on_fd_hook(int, int, void *);
extern "Python" int _on_plugin_init(char **, char **, char **, char *, char *);
extern "Python" int _on_plugin_deinit(void);
+22
View File
@@ -304,6 +304,28 @@ def _on_timer_hook(userdata):
return 0
@ffi.def_extern()
def _on_fd_hook(fd, flags, userdata):
hook = ffi.from_handle(userdata)
keep = False
try:
keep = bool(hook.callback(fd, flags, hook.userdata))
except Exception:
traceback.print_exc()
if not keep:
# The C watch removes itself on return; do not unhook it a second time.
try:
hook.is_unload = True
# from_handle() yields a weak proxy: id(proxy) is not id(Hook).
for existing in hook.plugin.hooks:
if existing == hook:
hook.plugin.remove_hook(id(existing))
break
except ReferenceError:
pass # the callback already unhooked itself
return int(keep)
@ffi.def_extern()
def _on_say_command(word, word_eol, userdata):
"""Handle input in the special >>python<< tab.
-10
View File
@@ -1187,16 +1187,6 @@ zoitechat_get_info (zoitechat_plugin *ph, const char *id)
return text_find_format_string (e);
}
/* Report the comparison actually used by this connection, not an
* advertised mapping that the current core does not implement. */
if (strcmp (id, "casemapping") == 0)
{
if (!is_session (ph->context))
return NULL;
return ph->context->server->p_cmp == g_ascii_strcasecmp
? "ascii" : "rfc1459";
}
hash = str_hash (id);
/* do the session independant ones first */
switch (hash)