Compare commits

..
Author SHA1 Message Date
deepend 9f92e9e4f1 add explicit IRC-aware comparison keys
(cherry picked from commit 5029a34fcb)
2026-11-08 06:06:31 -06:00
5 changed files with 105 additions and 130 deletions

No files matched your search

-30
View File
@@ -1,30 +0,0 @@
# Optional Python asyncio integration
`run_async(coroutine, context=None, interval=10)` returns an AsyncTask with
`done()`, `cancel()` and `result()`. Each invocation owns a separate event loop
pumped by an existing main-loop timer. A ready stop callback ensures selector
polling never blocks the UI. The captured context is restored after every pump;
a closed context cancels the work. No process-wide event-loop policy is changed.
Existing synchronous scripts are unaffected. Use `asyncio.get_running_loop()`
inside the coroutine. Child tasks use the same loop/context and are cancelled
when the main coroutine finishes or the script unloads. Cancellation cleanup is
bounded; tasks must not suppress cancellation indefinitely. Cleanup uses the
captured context while it remains open; after it closes, cancellation cleanup uses
the current valid context. Use explicit Context methods for context-specific
output in finally blocks, and do not issue context-dependent commands there. This provides normal
asyncio sockets/timers, not Windows subprocess support or a new HTTP library.
Asyncio hostname resolution can use its executor; use a resolver appropriate to
your library if you require no worker threads. Blocking work inside a coroutine
still blocks the client and must be avoided.
```python
import asyncio
import zoitechat
async def later():
await asyncio.sleep(1)
zoitechat.prnt('Still in the context where this task was started')
task = zoitechat.run_async(later())
```
+21
View File
@@ -0,0 +1,21 @@
# 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
```
-100
View File
@@ -1,100 +0,0 @@
"""Opt-in asyncio tasks pumped without blocking the client main loop."""
import asyncio
import operator
import traceback
import _zoitechat as api
__all__ = ['AsyncTask', 'run_async']
class AsyncTask:
"""A script-owned task and isolated event loop; cancel() is cooperative."""
def __init__(self, coroutine, plugin, context, interval):
self._plugin = plugin
self._context = context
self._loop = asyncio.new_event_loop()
self._task = self._loop.create_task(coroutine)
self._closed = False
self._unload_hook = api.hook_unload(lambda data: self._close())
self._timer_hook = api.hook_timer(interval, self._tick)
def done(self):
return self._task.done()
def cancel(self):
if not self._closed:
return self._task.cancel()
return False
def result(self):
return self._task.result()
def _pump(self):
# A ready stop callback forces the selector's timeout to zero.
self._loop.call_soon(self._loop.stop)
self._loop.run_forever()
def _close(self):
if self._closed:
return
self._closed = True
previous = api.get_context()
self._context.set()
try:
pending = asyncio.all_tasks(self._loop)
for task in pending:
task.cancel()
# Bound cleanup work; tasks must cooperate with cancellation.
for unused in range(3):
if not any(not task.done() for task in pending):
break
self._pump()
for task in pending:
if not task.done():
self._loop.call_exception_handler({
'message': 'Script task ignored cancellation during unload',
'task': task,
})
finally:
self._loop.close()
previous.set()
def _tick(self, userdata):
if self._closed:
return False
previous = api.get_context()
if not self._context.set():
self._close()
self._plugin.remove_hook(self._unload_hook)
return False
try:
self._pump()
if not self._task.done():
return True
try:
self._task.result()
except asyncio.CancelledError:
pass
except Exception:
traceback.print_exc()
self._close()
self._plugin.remove_hook(self._unload_hook)
return False
except Exception:
traceback.print_exc()
self._close()
self._plugin.remove_hook(self._unload_hook)
return False
finally:
previous.set()
def run_async(coroutine, context=None, interval=10):
"""Schedule a coroutine in a captured context; return an AsyncTask handle."""
interval = operator.index(interval)
if interval < 1 or interval > 2147483647:
raise ValueError('interval must be a positive C int in milliseconds')
if not asyncio.iscoroutine(coroutine):
raise TypeError('run_async expects a coroutine object')
plugin = api.__get_current_plugin()
return AsyncTask(coroutine, plugin, context or api.get_context(), interval)
+74
View File
@@ -0,0 +1,74 @@
"""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)
+10
View File
@@ -1187,6 +1187,16 @@ zoitechat_get_info (zoitechat_plugin *ph, const char *id)
return text_find_format_string (e); 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); hash = str_hash (id);
/* do the session independant ones first */ /* do the session independant ones first */
switch (hash) switch (hash)