Compare commits

..

No commits in common. "9a4dca80544cd8a64f963e775e0207b4fd9eb61d" and "d435442b39f167509e64b21a9cd1cf3f71e33033" have entirely different histories.

17 changed files with 78 additions and 239 deletions

View File

@ -15,11 +15,7 @@ jobs:
strategy:
matrix:
include:
- python-version: "3.13.0-alpha.6"
with-opt-deps: false # https://github.com/pyca/cryptography/issues/10806
runs-on: ubuntu-22.04
- python-version: "3.12.0"
- python-version: "3.12.0-alpha.7"
with-opt-deps: true
runs-on: ubuntu-22.04
@ -71,7 +67,7 @@ jobs:
- name: Upgrade pip
run: |
python3 -m pip install --upgrade pip setuptools
python3 -m pip install --upgrade pip
- name: Install optional dependencies
if: ${{ matrix.with-opt-deps }}

View File

@ -15,10 +15,14 @@ Last rule: you shouldn't add a mandatory dependency. Limnoria does not
come with any (besides Python), so please try to keep all dependencies
optional.
[Style Guidelines]:https://docs.limnoria.net/develop/style.html
[Style Guidelines]:https://limnoria.readthedocs.io/en/latest/develop/style.html
## Sending patches
When you send a pull request, **send it to the testing branch**.
It will be merged to master when it's considered to be stable enough to be
supported.
Don't fear that you spam Limnoria by sending many pull requests. According
to @ProgVal, it's easier for them to accept pull requests than to
cherry-pick everything manually.
@ -28,6 +32,6 @@ is very appreciated.
See also [Contributing to Limnoria] at [Limnoria documentation].
[Contributing to Limnoria]:https://docs.limnoria.net/contribute/index.html
[Contributing to Limnoria]:https://limnoria.readthedocs.io/en/latest/contribute/index.html
[Limnoria documentation]:https://docs.limnoria.net/
[Limnoria documentation]:https://limnoria.readthedocs.io/

View File

@ -39,7 +39,7 @@ class DDGTestCase(PluginTestCase):
def testSearch(self):
self.assertRegexp(
'ddg search wikipedia', r'Wikipedia.*? - .*?https?\:\/\/')
'ddg search wikipedia', 'Wikipedia.*? - .*?https?\:\/\/')
self.assertRegexp(
'ddg search en.wikipedia.org',
'Wikipedia, the free encyclopedia\x02 - '
@ -47,6 +47,6 @@ class DDGTestCase(PluginTestCase):
with conf.supybot.plugins.DDG.region.context('fr-fr'):
self.assertRegexp(
'ddg search wikipedia',
r'Wikipédia, l\'encyclopédie libre - .*?https?\:\/\/')
'Wikipédia, l\'encyclopédie libre - .*?https?\:\/\/')
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

View File

@ -194,7 +194,7 @@ class Connection:
if code != 151 or code is None:
break
resultword, resultdb = re.search(r'^"(.+)" (\S+)', text).groups()
resultword, resultdb = re.search('^"(.+)" (\S+)', text).groups()
defstr = self.get100block()
retval.append(Definition(self, self.getdbobj(resultdb),
resultword, defstr))

View File

@ -33,11 +33,11 @@ import datetime
# Credits for the regexp and function: https://stackoverflow.com/a/2765366/539465
_XSD_DURATION_RE = re.compile(
r"(?P<sign>-?)P"
r"(?:(?P<years>\d+)Y)?"
r"(?:(?P<months>\d+)M)?"
r"(?:(?P<days>\d+)D)?"
r"(?:T(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)?"
"(?P<sign>-?)P"
"(?:(?P<years>\d+)Y)?"
"(?:(?P<months>\d+)M)?"
"(?:(?P<days>\d+)D)?"
"(?:T(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)?"
)

View File

@ -31,6 +31,7 @@
import time
import socket
import telnetlib
import supybot.conf as conf
import supybot.utils as utils
@ -157,14 +158,14 @@ class Internet(callbacks.Plugin):
if not status:
status = 'unknown'
try:
sock = socket.create_connection(('whois.iana.org', 43))
t = telnetlib.Telnet('whois.iana.org', 43)
except socket.error as e:
irc.error(str(e))
return
sock.sendall(b'registrar ')
sock.sendall(registrar.split('(')[0].strip().encode('ascii'))
sock.sendall(b'\n')
s = sock.recv(100000)
t.write(b'registrar ')
t.write(registrar.split('(')[0].strip().encode('ascii'))
t.write(b'\n')
s = t.read_all()
url = ''
for line in s.splitlines():
line = line.decode('ascii').strip()

View File

@ -40,6 +40,7 @@ class InternetTestCase(PluginTestCase):
'Host not found.')
def testWhois(self):
self.assertNotError('internet whois ohio-state.edu')
self.assertNotError('internet whois microsoft.com')
self.assertNotError('internet whois inria.fr')
self.assertNotError('internet whois slime.com.au')

View File

@ -849,7 +849,7 @@ class UnitGroup:
def updateCurrentUnit(self, text, cursorPos):
"Set current unit number"
self.currentNum = len(re.findall(r'[\*/]', text[:cursorPos]))
self.currentNum = len(re.findall('[\*/]', text[:cursorPos]))
def currentUnit(self):
"Return current unit if its a full match, o/w None"
@ -925,7 +925,7 @@ class UnitGroup:
def parseGroup(self, text):
"Return list of units from text string"
unitList = []
parts = [part.strip() for part in re.split(r'([\*/])', text)]
parts = [part.strip() for part in re.split('([\*/])', text)]
numerator = 1
while parts:
unit = self.parseUnit(parts.pop(0))
@ -1180,7 +1180,7 @@ class Unit:
self.equiv = unitList[0].strip()
if self.equiv[0] == '[': # used only for non-linear units
try:
self.equiv, self.fromEqn = re.match(r'\[(.*?)\](.*)', \
self.equiv, self.fromEqn = re.match('\[(.*?)\](.*)', \
self.equiv).groups()
if ';' in self.fromEqn:
self.fromEqn, self.toEqn = self.fromEqn.split(';', 1)
@ -1190,7 +1190,7 @@ class Unit:
raise UnitDataError('Bad equation for "%s"' % self.name)
else: # split factor and equiv unit for linear
parts = self.equiv.split(None, 1)
if len(parts) > 1 and re.search(r'[^\d\.eE\+\-\*/]', parts[0]) \
if len(parts) > 1 and re.search('[^\d\.eE\+\-\*/]', parts[0]) \
== None: # only allowed digits and operators
try:
self.factor = float(eval(parts[0]))

View File

@ -342,31 +342,21 @@ class Misc(callbacks.Plugin):
Returns the version of the current bot.
"""
try:
versions = []
# fetch from PyPI
data = json.loads(utils.web.getUrl(
'https://pypi.org/pypi/limnoria/json'
).decode('utf8'))
release_version = data['info']['version']
# zero-left-pad months and days
release_version = re.sub(
r'\.([0-9])\b', lambda m: '.0' + m.group(1), release_version
)
# fetch from Git
data = json.loads(utils.web.getUrl(
'https://api.github.com/repos/progval/Limnoria/'
'commits/master'
).decode('utf8'))
git_version = data['commit']['committer']['date']
# Strip the last 'Z':
git_version = git_version.rsplit('T', 1)[0].replace('-', '.')
newest = _(
'The newest version available online is %(release_version)s, '
'or %(git_version)s in Git'
) % {'release_version': release_version, 'git_version': git_version}
newestUrl = 'https://api.github.com/repos/progval/Limnoria/' + \
'commits/%s'
versions = {}
for branch in ('master', 'testing'):
data = json.loads(utils.web.getUrl(newestUrl % branch)
.decode('utf8'))
version = data['commit']['committer']['date']
# Strip the last 'Z':
version = version.rsplit('T', 1)[0].replace('-', '.')
if minisix.PY2 and isinstance(version, unicode):
version = version.encode('utf8')
versions[branch] = version
newest = _('The newest versions available online are %s.') % \
', '.join([_('%s (in %s)') % (y,x)
for x,y in versions.items()])
except utils.web.Error as e:
self.log.info('Couldn\'t get website version: %s', e)
newest = _('I couldn\'t fetch the newest version '

View File

@ -280,7 +280,7 @@ class RSS(callbacks.Plugin):
raise callbacks.Error(s)
if url:
feed = self.feeds.get(url)
if feed and feed.name != feed.url and feed.name in self.feed_names:
if feed and feed.name != feed.url:
s = format(_('I already have a feed with that URL named %s.'),
feed.name)
raise callbacks.Error(s)

View File

@ -84,7 +84,7 @@ def mock_urllib(f):
url = 'http://www.advogato.org/rss/articles.xml'
class RSSTestCase(ChannelPluginTestCase):
plugins = ('RSS', 'Plugin')
plugins = ('RSS','Plugin')
timeout = 1
@ -121,27 +121,6 @@ class RSSTestCase(ChannelPluginTestCase):
self.assertEqual(self.irc.getCallback('RSS').feed_names, {})
self.assertTrue(self.irc.getCallback('RSS').get_feed('http://xkcd.com/rss.xml'))
@mock_urllib
def testChangeUrl(self, mock):
try:
self.assertNotError('rss add xkcd http://xkcd.com/rss.xml')
self.assertNotError('rss remove xkcd')
self.assertNotError('rss add xkcd https://xkcd.com/rss.xml')
self.assertRegexp('help xkcd', 'https://')
finally:
self._feedMsg('rss remove xkcd')
@mock_urllib
def testChangeName(self, mock):
try:
self.assertNotError('rss add xkcd http://xkcd.com/rss.xml')
self.assertNotError('rss remove xkcd')
self.assertNotError('rss add xkcd2 http://xkcd.com/rss.xml')
self.assertRegexp('help xkcd2', 'http://xkcd.com')
finally:
self._feedMsg('rss remove xkcd')
self._feedMsg('rss remove xkcd2')
@mock_urllib
def testInitialAnnounceNewest(self, mock):
mock._data = xkcd_new

View File

@ -33,6 +33,7 @@ import os
import re
import pwd
import sys
import crypt
import errno
import random
import select
@ -40,12 +41,6 @@ import struct
import subprocess
import shlex
try:
import crypt
except ImportError:
# Python >= 3.13
crypt = None
import supybot.conf as conf
import supybot.utils as utils
from supybot.commands import *
@ -124,26 +119,25 @@ class Unix(callbacks.Plugin):
irc.reply(format('%i', os.getpid()), private=True)
pid = wrap(pid, [('checkCapability', 'owner')])
if crypt is not None: # Python < 3.13
_cryptre = re.compile(b'[./0-9A-Za-z]')
@internationalizeDocstring
def crypt(self, irc, msg, args, password, salt):
"""<password> [<salt>]
_cryptre = re.compile(b'[./0-9A-Za-z]')
@internationalizeDocstring
def crypt(self, irc, msg, args, password, salt):
"""<password> [<salt>]
Returns the resulting of doing a crypt() on <password>. If <salt> is
not given, uses a random salt. If running on a glibc2 system,
prepending '$1$' to your salt will cause crypt to return an MD5sum
based crypt rather than the standard DES based crypt.
"""
def makeSalt():
s = b'\x00'
while self._cryptre.sub(b'', s) != b'':
s = struct.pack('<h', random.randrange(-(2**15), 2**15))
return s
if not salt:
salt = makeSalt().decode()
irc.reply(crypt.crypt(password, salt))
crypt = wrap(crypt, ['something', additional('something')])
Returns the resulting of doing a crypt() on <password>. If <salt> is
not given, uses a random salt. If running on a glibc2 system,
prepending '$1$' to your salt will cause crypt to return an MD5sum
based crypt rather than the standard DES based crypt.
"""
def makeSalt():
s = b'\x00'
while self._cryptre.sub(b'', s) != b'':
s = struct.pack('<h', random.randrange(-(2**15), 2**15))
return s
if not salt:
salt = makeSalt().decode()
irc.reply(crypt.crypt(password, salt))
crypt = wrap(crypt, ['something', additional('something')])
@internationalizeDocstring
def spell(self, irc, msg, args, word):

View File

@ -31,11 +31,6 @@
import os
import socket
try:
import crypt
except ImportError:
crypt = None
from supybot.test import *
try:
@ -111,9 +106,8 @@ if os.name == 'posix':
def testProgstats(self):
self.assertNotError('progstats')
if crypt is not None: # Python < 3.13
def testCrypt(self):
self.assertNotError('crypt jemfinch')
def testCrypt(self):
self.assertNotError('crypt jemfinch')
@skipUnlessFortune
def testFortune(self):

View File

@ -49,12 +49,10 @@ except ImportError:
install. This package is pretty standard, and often installed alongside
Python, but it is missing on your system.
Try installing it with your package manager, it is usually called
'python3-setuptools'; or with '%s -m pip install setuptools'.
If that does not work, try installing python3-pip
'python3-setuptools'. If that does not work, try installing python3-pip
instead, either with your package manager or by following these
instructions: https://pip.pypa.io/en/stable/installation/ (replace
'python' with 'python3' in all the commands)"""
% sys.executable)
'python' with 'python3' in all the commands)""")
sys.stderr.write(os.linesep*2)
sys.stderr.write(textwrap.fill(s))
sys.stderr.write(os.linesep*2)

View File

@ -419,15 +419,6 @@ def registerNetwork(name, password='', ssl=True, sasl_username='',
registry.String('', _("""Determines what user modes the bot will request
from the server when it first connects. If empty, defaults to
supybot.protocols.irc.umodes""")))
registerGlobalValue(network, 'vhost',
registry.String('', _("""Determines what vhost the bot will bind to before
connecting a server (IRC, HTTP, ...) via IPv4. If empty, defaults to
supybot.protocols.irc.vhost""")))
registerGlobalValue(network, 'vhostv6',
registry.String('', _("""Determines what vhost the bot will bind to before
connecting a server (IRC, HTTP, ...) via IPv6. If empty, defaults to
supybot.protocols.irc.vhostv6""")))
sasl = registerGroup(network, 'sasl')
registerGlobalValue(sasl, 'username', registry.String(sasl_username,
_("""Determines what SASL username will be used on %s. This should

View File

@ -312,10 +312,8 @@ class SocketDriver(drivers.IrcDriver, drivers.ServersMixin):
address,
port=self.currentServer.port,
socks_proxy=socks_proxy,
vhost=self.networkGroup.get('vhost')()
or conf.supybot.protocols.irc.vhost(),
vhostv6=self.networkGroup.get('vhostv6')()
or conf.supybot.protocols.irc.vhostv6(),
vhost=conf.supybot.protocols.irc.vhost(),
vhostv6=conf.supybot.protocols.irc.vhostv6(),
)
except socket.error as e:
drivers.log.connectError(self.currentServer, e)

View File

@ -1,5 +1,5 @@
###
# Copyright (c) 2011-2024, Valentin Lorentz
# Copyright (c) 2011-2021, Valentin Lorentz
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
@ -32,8 +32,8 @@ An embedded and centralized HTTP server for Supybot's plugins.
"""
import os
import cgi
import socket
import urllib.parse
from threading import Thread
import supybot.log as log
@ -164,114 +164,6 @@ def get_template(filename):
with open(path + '.example', 'r') as fd:
return fd.read()
class HttpHeader:
__slots__ = ('name', 'value')
def __init__(self, name, value):
self.name = name
self.value = value
def __repr__(self):
"""Return printable representation."""
return "HttpHeader(%r, %r)" % (self.name, self.value)
class HttpHeaders:
"""Copy of `cgi.FieldStorage
<https://github.com/python/cpython/blob/v3.12.3/Lib/cgi.py#L512-L594>`
before it was removed from the stdlib.
"""
__slots__ = ('list',)
def __init__(self, headers):
self.list = headers
def __repr__(self):
return 'HttpHeaders(%r)' % self.list
def __iter__(self):
return iter(self.keys())
def __getattr__(self, name):
if name != 'value':
raise AttributeError(name)
if self.file:
self.file.seek(0)
value = self.file.read()
self.file.seek(0)
elif self.list is not None:
value = self.list
else:
value = None
return value
def __getitem__(self, key):
"""Dictionary style indexing."""
if self.list is None:
raise TypeError("not indexable")
found = []
for item in self.list:
if item.name == key: found.append(item)
if not found:
raise KeyError(key)
if len(found) == 1:
return found[0]
else:
return found
def getvalue(self, key, default=None):
"""Dictionary style get() method, including 'value' lookup."""
if key in self:
value = self[key]
if isinstance(value, list):
return [x.value for x in value]
else:
return value.value
else:
return default
def getfirst(self, key, default=None):
""" Return the first value received."""
if key in self:
value = self[key]
if isinstance(value, list):
return value[0].value
else:
return value.value
else:
return default
def getlist(self, key):
""" Return list of received values."""
if key in self:
value = self[key]
if isinstance(value, list):
return [x.value for x in value]
else:
return [value.value]
else:
return []
def keys(self):
"""Dictionary style keys() method."""
if self.list is None:
raise TypeError("not indexable")
return list(set(item.name for item in self.list))
def __contains__(self, key):
"""Dictionary style __contains__ method."""
if self.list is None:
raise TypeError("not indexable")
return any(item.name == key for item in self.list)
def __len__(self):
"""Dictionary style len(x) support."""
return len(self.keys())
def __bool__(self):
if self.list is None:
raise TypeError("Cannot be converted to bool.")
return bool(self.list)
class SupyHTTPRequestHandler(BaseHTTPRequestHandler):
def do_X(self, callbackMethod, *args, **kwargs):
if self.path == '/':
@ -307,11 +199,12 @@ class SupyHTTPRequestHandler(BaseHTTPRequestHandler):
if 'Content-Type' not in self.headers:
self.headers['Content-Type'] = 'application/x-www-form-urlencoded'
if self.headers['Content-Type'] == 'application/x-www-form-urlencoded':
length = min(100000, int(self.headers.get('Content-Length', '100000')))
qs = self.rfile.read(length).decode()
form = HttpHeaders([
HttpHeader(k, v) for (k, v) in urllib.parse.parse_qsl(qs)
])
form = cgi.FieldStorage(
fp=self.rfile,
headers=self.headers,
environ={'REQUEST_METHOD':'POST',
'CONTENT_TYPE':self.headers['Content-Type'],
})
else:
content_length = int(self.headers.get('Content-Length', '0'))
form = self.rfile.read(content_length)