mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-11-08 17:05:38 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f33ea41573 |
No files matched your search
@@ -0,0 +1,30 @@
|
|||||||
|
# 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())
|
||||||
|
```
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""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)
|
||||||
@@ -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)
|
|
||||||
@@ -1187,16 +1187,6 @@ 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)
|
||||||
|
|||||||
Reference in new issue
Block a user