Skip to content

Commit

Permalink
Add support for Python 3.10 by dropping event loop handling
Browse files Browse the repository at this point in the history
Implements suggestions in
shizmob#162 (comment) to
remove Pydle's internal handling of the event loop object entirely.
  • Loading branch information
felixonmars committed Aug 13, 2022
1 parent e10369d commit ab03fee
Show file tree
Hide file tree
Showing 12 changed files with 34 additions and 60 deletions.
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pydle
Python IRC library.
-------------------

pydle is a compact, flexible and standards-abiding IRC library for Python 3.5 through 3.9.
pydle is a compact, flexible and standards-abiding IRC library for Python 3.7 through 3.10.

Features
--------
Expand Down Expand Up @@ -65,9 +65,7 @@ Furthermore, since pydle is simply `asyncio`-based, you can run the client in yo
import asyncio

client = MyOwnBot('MyBot')
loop = asyncio.get_event_loop()
asyncio.ensure_future(client.connect('irc.rizon.net', tls=True, tls_verify=False), loop=loop)
loop.run_forever()
asyncio.run(client.connect('irc.rizon.net', tls=True, tls_verify=False))
```


Expand Down
6 changes: 3 additions & 3 deletions docs/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Introduction to pydle

What is pydle?
--------------
pydle is an IRC library for Python 3.5 through 3.9.
pydle is an IRC library for Python 3.7 through 3.10.

Although old and dated on some fronts, IRC is still used by a variety of communities as the real-time communication method of choice,
and the most popular IRC networks can still count on tens of thousands of users at any point during the day.
Expand Down Expand Up @@ -35,8 +35,8 @@ All dependencies can be installed using the standard package manager for Python,

Compatibility
-------------
pydle works in any interpreter that implements Python 3.5-3.9. Although mainly tested in CPython_, the standard Python implementation,
there is no reason why pydle itself should not work in alternative implementations like PyPy_, as long as they support the Python 3.5 language requirements.
pydle works in any interpreter that implements Python 3.7-3.10. Although mainly tested in CPython_, the standard Python implementation,
there is no reason why pydle itself should not work in alternative implementations like PyPy_, as long as they support the Python 3.7 language requirements.

.. _CPython: https://python.org
.. _PyPy: http://pypy.org
42 changes: 10 additions & 32 deletions pydle/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Basic IRC client implementation.
import asyncio
import logging
from asyncio import new_event_loop, gather, get_event_loop, sleep
from asyncio import gather, sleep
import warnings
from . import connection, protocol
import inspect
Expand Down Expand Up @@ -58,17 +58,11 @@ def PING_TIMEOUT(self, value):
self.READ_TIMEOUT = value

def __init__(self, nickname, fallback_nicknames=None, username=None, realname=None,
eventloop=None, **kwargs):
**kwargs):
""" Create a client. """
self._nicknames = [nickname] + (fallback_nicknames or [])
self.username = username or nickname.lower()
self.realname = realname or nickname
if eventloop:
self.eventloop = eventloop
else:
self.eventloop = get_event_loop()

self.own_eventloop = not eventloop
self._reset_connection_attributes()
self._reset_attributes()

Expand Down Expand Up @@ -104,11 +98,7 @@ def _reset_connection_attributes(self):

def run(self, *args, **kwargs):
""" Connect and run bot in event loop. """
self.eventloop.run_until_complete(self.connect(*args, **kwargs))
try:
self.eventloop.run_forever()
finally:
self.eventloop.stop()
asyncio.run(self.connect(*args, **kwargs))

async def connect(self, hostname=None, port=None, reconnect=False, **kwargs):
""" Connect to IRC server. """
Expand All @@ -128,7 +118,7 @@ async def connect(self, hostname=None, port=None, reconnect=False, **kwargs):
if self.server_tag:
self.logger = logging.getLogger(self.__class__.__name__ + ':' + self.server_tag)

self.eventloop.create_task(self.handle_forever())
asyncio.create_task(self.handle_forever())

async def disconnect(self, expected=True):
""" Disconnect from server. """
Expand All @@ -146,18 +136,13 @@ async def _disconnect(self, expected):
# Callback.
await self.on_disconnect(expected)

# Shut down event loop.
if expected and self.own_eventloop:
self.connection.stop()

async def _connect(self, hostname, port, reconnect=False, channels=None,
encoding=protocol.DEFAULT_ENCODING, source_address=None):
""" Connect to IRC host. """
# Create connection if we can't reuse it.
if not reconnect or not self.connection:
self._autojoin_channels = channels or []
self.connection = connection.Connection(hostname, port, source_address=source_address,
eventloop=self.eventloop)
self.connection = connection.Connection(hostname, port, source_address=source_address)
self.encoding = encoding

# Connect.
Expand Down Expand Up @@ -387,7 +372,7 @@ async def on_data(self, data):

while self._has_message():
message = self._parse_message()
self.eventloop.create_task(self.on_raw(message))
asyncio.create_task(self.on_raw(message))

async def on_data_error(self, exception):
""" Handle error. """
Expand Down Expand Up @@ -467,24 +452,20 @@ def event(self, func):
class ClientPool:
""" A pool of clients that are ran and handled in parallel. """

def __init__(self, clients=None, eventloop=None):
self.eventloop = eventloop if eventloop else new_event_loop()
def __init__(self, clients=None):
self.clients = set(clients or [])
self.connect_args = {}

def connect(self, client: BasicClient, *args, **kwargs):
""" Add client to pool. """
self.clients.add(client)
self.connect_args[client] = (args, kwargs)
# hack the clients event loop to use the pools own event loop
client.eventloop = self.eventloop
# necessary to run multiple clients in the same thread via the pool

def disconnect(self, client):
""" Remove client from pool. """
self.clients.remove(client)
del self.connect_args[client]
asyncio.run_coroutine_threadsafe(client.disconnect(expected=True), self.eventloop)
asyncio.run_coroutine_threadsafe(client.disconnect(expected=True))

def __contains__(self, item):
return item in self.clients
Expand All @@ -499,10 +480,7 @@ def handle_forever(self):
args, kwargs = self.connect_args[client]
connection_list.append(client.connect(*args, **kwargs))
# single future for executing the connections
connections = gather(*connection_list, loop=self.eventloop)
connections = gather(*connection_list)

# run the connections
self.eventloop.run_until_complete(connections)

# run the clients
self.eventloop.run_forever()
asyncio.run(connections)
10 changes: 4 additions & 6 deletions pydle/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Connection:

def __init__(self, hostname, port, tls=False, tls_verify=True, tls_certificate_file=None,
tls_certificate_keyfile=None, tls_certificate_password=None, ping_timeout=240,
source_address=None, eventloop=None):
source_address=None):
self.hostname = hostname
self.port = port
self.source_address = source_address
Expand All @@ -36,7 +36,6 @@ def __init__(self, hostname, port, tls=False, tls_verify=True, tls_certificate_f

self.reader = None
self.writer = None
self.eventloop = eventloop or asyncio.new_event_loop()

async def connect(self):
""" Connect to target. """
Expand All @@ -49,8 +48,7 @@ async def connect(self):
host=self.hostname,
port=self.port,
local_addr=self.source_address,
ssl=self.tls_context,
loop=self.eventloop
ssl=self.tls_context
)

def create_tls_context(self):
Expand Down Expand Up @@ -103,8 +101,8 @@ def connected(self):
return self.reader is not None and self.writer is not None

def stop(self):
""" Stop event loop. """
self.eventloop.call_soon(self.eventloop.stop)
""" Stop connection. """
asyncio.run(self.disconnect())

async def send(self, data):
""" Add data to send queue. """
Expand Down
4 changes: 3 additions & 1 deletion pydle/features/ircv3/metadata.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import asyncio

from . import cap

VISIBLITY_ALL = '*'
Expand Down Expand Up @@ -28,7 +30,7 @@ async def get_metadata(self, target):

self._metadata_queue.append(target)
self._metadata_info[target] = {}
self._pending['metadata'][target] = self.eventloop.create_future()
self._pending['metadata'][target] = asyncio.get_event_loop().create_future()

return self._pending['metadata'][target]

Expand Down
5 changes: 3 additions & 2 deletions pydle/features/ircv3/sasl.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## sasl.py
# SASL authentication support. Currently we only support PLAIN authentication.
import asyncio
import base64
from functools import partial

Expand Down Expand Up @@ -47,7 +48,7 @@ async def _sasl_start(self, mechanism):
await self.rawmsg('AUTHENTICATE', mechanism)
# create a partial, required for our callback to get the kwarg
_sasl_partial = partial(self._sasl_abort, timeout=True)
self._sasl_timer = self.eventloop.call_later(self.SASL_TIMEOUT, _sasl_partial)
self._sasl_timer = asyncio.get_event_loop().call_later(self.SASL_TIMEOUT, _sasl_partial)

async def _sasl_abort(self, timeout=False):
""" Abort SASL authentication. """
Expand Down Expand Up @@ -166,7 +167,7 @@ async def on_raw_authenticate(self, message):
await self._sasl_respond()
else:
# Response not done yet. Restart timer.
self._sasl_timer = self.eventloop.call_later(self.SASL_TIMEOUT, self._sasl_abort(timeout=True))
self._sasl_timer = asyncio.get_event_loop().call_later(self.SASL_TIMEOUT, self._sasl_abort(timeout=True))

on_raw_900 = cap.CapabilityNegotiationSupport._ignored # You are now logged in as...

Expand Down
9 changes: 5 additions & 4 deletions pydle/features/rfc1459/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## rfc1459.py
# Basic RFC1459 stuff.
import asyncio
import copy
import datetime
import ipaddress
Expand Down Expand Up @@ -398,7 +399,7 @@ async def whois(self, nickname):
# We just check if there's a space in the nickname -- if there is,
# then we immediately set the future's result to None and don't bother checking.
if protocol.ARGUMENT_SEPARATOR.search(nickname) is not None:
result = self.eventloop.create_future()
result = asyncio.get_event_loop().create_future()
result.set_result(None)
return result

Expand All @@ -412,7 +413,7 @@ async def whois(self, nickname):
}

# Create a future for when the WHOIS requests succeeds.
self._pending['whois'][nickname] = self.eventloop.create_future()
self._pending['whois'][nickname] = asyncio.get_event_loop().create_future()

return await self._pending['whois'][nickname]

Expand All @@ -425,7 +426,7 @@ async def whowas(self, nickname):
"""
# Same treatment as nicknames in whois.
if protocol.ARGUMENT_SEPARATOR.search(nickname) is not None:
result = self.eventloop.create_future()
result = asyncio.get_event_loop().create_future()
result.set_result(None)
return result

Expand All @@ -434,7 +435,7 @@ async def whowas(self, nickname):
self._whowas_info[nickname] = {}

# Create a future for when the WHOWAS requests succeeds.
self._pending['whowas'][nickname] = self.eventloop.create_future()
self._pending['whowas'][nickname] = asyncio.get_event_loop().create_future()

return await self._pending['whowas'][nickname]

Expand Down
3 changes: 1 addition & 2 deletions pydle/features/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ async def _connect(self, hostname, port, reconnect=False, password=None, encodin
tls=tls, tls_verify=tls_verify,
tls_certificate_file=self.tls_client_cert,
tls_certificate_keyfile=self.tls_client_cert_key,
tls_certificate_password=self.tls_client_cert_password,
eventloop=self.eventloop)
tls_certificate_password=self.tls_client_cert_password)
self.encoding = encoding

# Connect.
Expand Down
2 changes: 1 addition & 1 deletion pydle/utils/irccat.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ async def _main():
def main():
# Setup logging.
logging.basicConfig(format='!! %(levelname)s: %(message)s')
asyncio.get_event_loop().run_until_complete(_main())
asyncio.run(_main())


if __name__ == '__main__':
Expand Down
4 changes: 1 addition & 3 deletions pydle/utils/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@

def main():
client, connect = _args.client_from_args('pydle', description='pydle IRC library.')
loop = asyncio.get_event_loop()
asyncio.ensure_future(connect(), loop=loop)
loop.run_forever()
asyncio.run(connect())


if __name__ == '__main__':
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ keywords = ["irc", "library","python3","compact","flexible"]
license = "BSD"

[tool.poetry.dependencies]
python = ">=3.6;<3.10"
python = ">=3.7;<3.11"

[tool.poetry.dependencies.pure-sasl]
version = "^0.6.2"
Expand Down
1 change: 0 additions & 1 deletion tests/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ async def _connect(self, hostname, port, *args, **kwargs):
port,
mock_client=self,
mock_server=self._mock_server,
eventloop=self.eventloop,
)
await self.connection.connect()
await self.on_connect()
Expand Down

0 comments on commit ab03fee

Please sign in to comment.