Skip to content

Commit

Permalink
Merge pull request #176 from Shizmob/develop
Browse files Browse the repository at this point in the history
v1.0.0
  • Loading branch information
theunkn0wn1 authored Jul 4, 2022
2 parents 286e533 + e10369d commit b85f59b
Show file tree
Hide file tree
Showing 42 changed files with 664 additions and 679 deletions.
7 changes: 5 additions & 2 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+.
pydle is a compact, flexible and standards-abiding IRC library for Python 3.5 through 3.9.

Features
--------
Expand All @@ -26,6 +26,9 @@ Basic Usage

From there, you can `import pydle` and subclass `pydle.Client` for your own functionality.

> To enable SSL support, install the `sasl` extra.
> `pip install pydle[sasl]`
Setting a nickname and starting a connection over TLS:
```python
import pydle
Expand Down Expand Up @@ -103,7 +106,7 @@ class Client(MyBase):

**Q: How do I...?**

Stop! Read the [documentation](http://pydle.readthedocs.org) first. If you're still in need of support, join us on IRC! We hang at `#kochira` on `irc.freenode.net`. If someone is around, they'll most likely gladly help you.
Stop! Read the [documentation](http://pydle.readthedocs.org) first. If you're still in need of support, join us on IRC! We hang at `#pydle` on `irc.libera.chat`. If someone is around, they'll most likely gladly help you.

License
-------
Expand Down
4 changes: 2 additions & 2 deletions docs/features/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ In addition, it registers the `pydle.Client.on_ctcp(from, query, contents)` hook
request, and a per-type hook in the form of `pydle.Client.on_ctcp_<type>(from, contents)`, which allows you to act upon CTCP
requests of type `type`. `type` will always be lowercased. A few examples of `type` can be: `action`, `time`, `version`.

Finally, it registers the `pydle.Client.on_ctcp_reply(from, queyr, contents)` hook, which acts similar to the above hook,
Finally, it registers the `pydle.Client.on_ctcp_reply(from, query, contents)` hook, which acts similar to the above hook,
except it is triggered when the client receives a CTCP response. It also registers `pydle.Client.on_ctcp_<type>_reply`, which
works similar to the per-type hook described above.

Expand Down Expand Up @@ -252,4 +252,4 @@ Support For `RPL_WHOIS_HOST` messages, this allows pydle to expose an IRC users
real IP address and host, if the bot has access to that information.

This information will fill in the `real_ip_address` and `real_hostname` fields
of an :class:`pydle.Client.whois()` response.
of an :class:`pydle.Client.whois()` response.
4 changes: 2 additions & 2 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 and up.
pydle is an IRC library for Python 3.5 through 3.9.

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,7 +35,7 @@ All dependencies can be installed using the standard package manager for Python,

Compatibility
-------------
pydle works in any interpreter that implements Python 3.5 or higher. Although mainly tested in CPython_, the standard Python implementation,
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.

.. _CPython: https://python.org
Expand Down
4 changes: 2 additions & 2 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ Fortunately, pydle utilizes asyncio coroutines_ which allow you to handle a bloc
while still retaining the benefits of asynchronous program flow. Coroutines allow pydle to be notified when a blocking operation is done,
and then resume execution of the calling function appropriately. That way, blocking operations do not block the entire program flow.

In order for a function to be declared as a coroutine, it has to be declared as an ``async def`` function or decorated with the :meth:`asyncio.coroutine` decorator.
In order for a function to be declared as a coroutine, it has to be declared as an ``async def`` function.
It can then call functions that would normally block using Python's ``await`` operator.
Since a function that calls a blocking function is itself blocking too, it has to be declared a coroutine as well.

Expand Down Expand Up @@ -220,7 +220,7 @@ the act of WHOISing will not block the entire program flow of the client.
self.join('#kochira')
await def is_admin(self, nickname):
async def is_admin(self, nickname):
"""
Check whether or not a user has administrative rights for this bot.
This is a blocking function: use a coroutine to call it.
Expand Down
13 changes: 6 additions & 7 deletions pydle/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# noinspection PyUnresolvedReferences
from asyncio import coroutine, Future
from functools import cmp_to_key
from . import connection, protocol, client, features

from .client import Error, NotInChannel, AlreadyInChannel, BasicClient, ClientPool
from .features.ircv3.cap import NEGOTIATING as CAPABILITY_NEGOTIATING, FAILED as CAPABILITY_FAILED, \
NEGOTIATED as CAPABILITY_NEGOTIATED

# noinspection PyUnresolvedReferences
from asyncio import coroutine, Future

__name__ = 'pydle'
__version__ = '0.9.4rc1'
Expand All @@ -15,12 +15,11 @@

def featurize(*features):
""" Put features into proper MRO order. """
from functools import cmp_to_key

def compare_subclass(left, right):
if issubclass(left, right):
return -1
elif issubclass(right, left):
if issubclass(right, left):
return 1
return 0

Expand All @@ -32,9 +31,9 @@ def compare_subclass(left, right):

class Client(featurize(*features.ALL)):
""" A fully featured IRC client. """
pass
...


class MinimalClient(featurize(*features.LITE)):
""" A cut-down, less-featured IRC client. """
pass
...
59 changes: 37 additions & 22 deletions pydle/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
import asyncio
import logging
from asyncio import new_event_loop, gather, get_event_loop, sleep

from . import connection, protocol
import warnings
from . import connection, protocol
import inspect
import functools

__all__ = ['Error', 'AlreadyInChannel', 'NotInChannel', 'BasicClient', 'ClientPool']
DEFAULT_NICKNAME = '<unregistered>'


class Error(Exception):
""" Base class for all pydle errors. """
pass
...


class NotInChannel(Error):
Expand Down Expand Up @@ -56,10 +57,10 @@ def PING_TIMEOUT(self, value):
)
self.READ_TIMEOUT = value

def __init__(self, nickname, fallback_nicknames=[], username=None, realname=None,
def __init__(self, nickname, fallback_nicknames=None, username=None, realname=None,
eventloop=None, **kwargs):
""" Create a client. """
self._nicknames = [nickname] + fallback_nicknames
self._nicknames = [nickname] + (fallback_nicknames or [])
self.username = username or nickname.lower()
self.realname = realname or nickname
if eventloop:
Expand Down Expand Up @@ -149,12 +150,12 @@ async def _disconnect(self, expected):
if expected and self.own_eventloop:
self.connection.stop()

async def _connect(self, hostname, port, reconnect=False, channels=[],
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
self._autojoin_channels = channels or []
self.connection = connection.Connection(hostname, port, source_address=source_address,
eventloop=self.eventloop)
self.encoding = encoding
Expand All @@ -167,10 +168,8 @@ def _reconnect_delay(self):
if self.RECONNECT_ON_ERROR and self.RECONNECT_DELAYED:
if self._reconnect_attempts >= len(self.RECONNECT_DELAYS):
return self.RECONNECT_DELAYS[-1]
else:
return self.RECONNECT_DELAYS[self._reconnect_attempts]
else:
return 0
return self.RECONNECT_DELAYS[self._reconnect_attempts]
return 0

## Internal database management.

Expand All @@ -197,21 +196,21 @@ def _create_user(self, nickname):
'hostname': None
}

def _sync_user(self, nick, metadata):
async def _sync_user(self, nick, metadata):
# Create user in database.
if nick not in self.users:
self._create_user(nick)
await self._create_user(nick)
if nick not in self.users:
return
self.users[nick].update(metadata)

def _rename_user(self, user, new):
async def _rename_user(self, user, new):
if user in self.users:
self.users[new] = self.users[user]
self.users[new]['nickname'] = new
del self.users[user]
else:
self._create_user(new)
await self._create_user(new)
if new not in self.users:
return

Expand Down Expand Up @@ -297,8 +296,7 @@ def server_tag(self):
tag = host

return tag
else:
return None
return None

## IRC API.

Expand Down Expand Up @@ -372,7 +370,7 @@ async def handle_forever(self):
try:
await self.rawmsg("PING", self.server_tag)
data = await self.connection.recv(timeout=self.READ_TIMEOUT)
except (asyncio.TimeoutError, ConnectionResetError) as e:
except (asyncio.TimeoutError, ConnectionResetError):
data = None

if not data:
Expand Down Expand Up @@ -430,7 +428,7 @@ async def on_unknown(self, message):

async def _ignored(self, message):
""" Ignore message. """
pass
...

def __getattr__(self, attr):
""" Return on_unknown or _ignored for unknown handlers, depending on the invocation type. """
Expand All @@ -441,13 +439,30 @@ def __getattr__(self, attr):
# In that case, return the method that logs and possibly acts on unknown messages.
return self.on_unknown
# Are we in an existing handler calling super()?
else:
# Just ignore it, then.
return self._ignored
# Just ignore it, then.
return self._ignored

# This isn't a handler, just raise an error.
raise AttributeError(attr)

# Bonus features
def event(self, func):
"""
Registers the specified `func` to handle events of the same name.
The func will always be called with, at least, the bot's `self` instance.
Returns decorated func, unmodified.
"""
if not func.__name__.startswith("on_"):
raise NameError("Event handlers must start with 'on_'.")

if not inspect.iscoroutinefunction(func):
raise AssertionError("Wrapped function {!r} must be an `async def` function.".format(func))
setattr(self, func.__name__, functools.partial(func, self))

return func


class ClientPool:
""" A pool of clients that are ran and handled in parallel. """
Expand Down
6 changes: 4 additions & 2 deletions pydle/features/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from .isupport import ISUPPORTSupport
from .whox import WHOXSupport
from .ircv3 import IRCv3Support, IRCv3_1Support, IRCv3_2Support
from .rpl_whoishost import RplWhoisHostSupport

ALL = [ IRCv3Support, WHOXSupport, ISUPPORTSupport, CTCPSupport, AccountSupport, TLSSupport, RFC1459Support ]
LITE = [ WHOXSupport, ISUPPORTSupport, CTCPSupport, TLSSupport, RFC1459Support ]
ALL = [IRCv3Support, WHOXSupport, ISUPPORTSupport, CTCPSupport, AccountSupport, TLSSupport, RFC1459Support,
RplWhoisHostSupport]
LITE = [WHOXSupport, ISUPPORTSupport, CTCPSupport, TLSSupport, RFC1459Support]
21 changes: 11 additions & 10 deletions pydle/features/account.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## account.py
# Account system support.
from pydle.features import rfc1459
import asyncio


class AccountSupport(rfc1459.RFC1459Support):

Expand All @@ -15,16 +15,17 @@ def _create_user(self, nickname):
'identified': False
})

def _rename_user(self, user, new):
super()._rename_user(user, new)
async def _rename_user(self, user, new):
await super()._rename_user(user, new)
# Unset account info to be certain until we get a new response.
self._sync_user(new, {'account': None, 'identified': False})
self.whois(new)
await self._sync_user(new, {'account': None, 'identified': False})
await self.whois(new)

## IRC API.
@asyncio.coroutine
def whois(self, nickname):
info = yield from super().whois(nickname)
async def whois(self, nickname):
info = await super().whois(nickname)
if info is None:
return info
info.setdefault('account', None)
info.setdefault('identified', False)
return info
Expand All @@ -39,7 +40,7 @@ async def on_raw_307(self, message):
}

if nickname in self.users:
self._sync_user(nickname, info)
await self._sync_user(nickname, info)
if nickname in self._pending['whois']:
self._whois_info[nickname].update(info)

Expand All @@ -52,6 +53,6 @@ async def on_raw_330(self, message):
}

if nickname in self.users:
self._sync_user(nickname, info)
await self._sync_user(nickname, info)
if nickname in self._pending['whois']:
self._whois_info[nickname].update(info)
Loading

0 comments on commit b85f59b

Please sign in to comment.