Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Python 3.10 by dropping event loop handling #180

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.6 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()
Copy link
Contributor

@Rixxan Rixxan Nov 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be incorrect. Changing the entry logic for my own implementation of pydle results in a hung program, which immediately terminates uncleanly.

However, testing on Python 3.8 if I do not change my existing entry logic, the program starts properly. (Still stability-testing.)

The same behavior replicates on 3.10.

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.6 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.6-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.6 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.create_task(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())
theunkn0wn1 marked this conversation as resolved.
Show resolved Hide resolved


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