GoodFrench: add plugin (v0.1)

master
Valentin Lorentz 2010-11-06 18:52:20 +01:00
parent bbf9f37356
commit d4d57b99a7
7 changed files with 442 additions and 0 deletions

1
GoodFrench/README.txt Normal file
View File

@ -0,0 +1 @@
Insert a description of your plugin here, with any notes, etc. about using it.

69
GoodFrench/__init__.py Normal file
View File

@ -0,0 +1,69 @@
# -*- coding: utf8 -*-
###
# Copyright (c) 2010, Valentin Lorentz
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
This plugin allows to punish people who make mistakes in speaking French
"""
import supybot
import supybot.world as world
# Use this for the version of this plugin. You may wish to put a CVS keyword
# in here if you're keeping the plugin in CVS or some similar system.
__version__ = "0.1"
# XXX Replace this with an appropriate author or supybot.Author instance.
if not hasattr(supybot.authors, 'progval'):
supybot.authors.progval = supybot.Author('Valentin Lorentz', 'ProgVal',
'progval@gmail.com')
__author__ = supybot.authors.progval
# This is a dictionary mapping supybot.Author instances to lists of
# contributions.
__contributors__ = {}
# This is a url where the most recent plugin package can be downloaded.
__url__ = '' # 'http://supybot.com/Members/yourname/GoodFrench/download'
import config
import plugin
reload(plugin) # In case we're being reloaded.
# Add more reloads here if you add third-party modules and want them to be
# reloaded when this plugin is reloaded. Don't forget to import them as well!
if world.testing:
import test
Class = plugin.Class
configure = config.configure
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

59
GoodFrench/config.py Normal file
View File

@ -0,0 +1,59 @@
# -*- coding: utf8 -*-
###
# Copyright (c) 2010, Valentin Lorentz
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import supybot.conf as conf
import supybot.registry as registry
def configure(advanced):
# This will be called by supybot to configure this module. advanced is
# a bool that specifies whether the user identified himself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
from supybot.questions import expect, anything, something, yn
conf.registerPlugin('GoodFrench', True)
GoodFrench = conf.registerPlugin('GoodFrench')
# This is where your configuration variables (if any) should go. For example:
# conf.registerGlobalValue(GoodFrench, 'someConfigVariableName',
# registry.Boolean(False, _("""Help for someConfigVariableName.""")))
conf.registerChannelValue(GoodFrench, 'level',
registry.Integer(0, """Le niveau de filtrage. Le niveau N filtre
ce que le niveau N-1 filtrait, avec des choses en plus.
0 : pas de filtrage ;
1 : filtre le langage SMS
2 : filtre les erreurs de pluriel ;
3 : filtre les fautes de conjugaison courantes ;
4 : filtre les fautes d'orthographe courantes ;
5 : filtre les abbréviations ("t'as" au lieu de "tu as")"""))
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

View File

@ -0,0 +1 @@
# Stub so local is a module, used for third-party modules

View File

@ -0,0 +1,6 @@
ERROR 2010-11-06T18:41:06 supybot Invalid user dictionary file, resetting to empty.
ERROR 2010-11-06T18:41:06 supybot Exact error: IOError: [Errno 2] No such file or directory: 'conf/users.conf'
ERROR 2010-11-06T18:41:06 supybot Invalid channel database, resetting to empty.
ERROR 2010-11-06T18:41:06 supybot Exact error: IOError: [Errno 2] No such file or directory: 'conf/channels.conf'
WARNING 2010-11-06T18:41:06 supybot Couldn't open ignore database: [Errno 2] No such file or directory: 'conf/ignores.conf'
INFO 2010-11-06T18:41:07 supybot Shutdown initiated.

212
GoodFrench/plugin.py Normal file
View File

@ -0,0 +1,212 @@
# -*- coding: utf8 -*-
###
# Copyright (c) 2010, Valentin Lorentz
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import re
import supybot.world as world
import supybot.ircmsgs as ircmsgs
import supybot.utils as utils
from supybot.commands import *
import supybot.plugins as plugins
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
class SpellChecker:
def __init__(self, text, level):
# 0 : pas de filtrage ;
# 1 : filtre le langage SMS
# 2 : filtre les erreurs de pluriel ;
# 3 : filtre les fautes de conjugaison courantes ;
# 4 : filtre les fautes d'orthographe courantes ;
# 5 : filtre les abbréviations ("t'as" au lieu de "tu as")
self._text = text
self._errors = []
if level >= 1:
self._checking = 'SMS'
self.checkSMS()
if level >= 2:
self._checking = 'pluriel'
self.checkPlural()
if level >= 3:
self._checking = 'conjugaison'
self.checkConjugaison()
if level >= 4:
self._checking = 'orthographe'
self.checkSpelling()
if level >= 5:
self._checking = 'abbréviation'
self.checkAbbreviation()
if level >= 6:
self._checking = 'typographie'
self.checkTypographic()
if level >= 7:
self._checking = 'lol'
self.checkLol()
def _raise(self, message):
self._errors.append('[%s] %s' % (self._checking, message))
def _detect(self, mode, correct, mask, displayedMask=None, wizard=' '):
if displayedMask is None:
displayedMask = mask
raise_ = False
text = '%s%s%s' % (wizard, self._text, wizard)
if mode == 'single' and re.match('.*\W%s\W.*' % mask, text,
re.IGNORECASE) is not None:
raise_ = True
elif mode == 'regexp' and re.match('.*%s.*' % mask, text):
raise_ = True
if raise_:
if self._checking == 'conjugaison' or \
self._checking == 'typographie':
self._raise(correct)
else:
if correct.__class__ == list:
correct = '`%s`' % '`, ou `'.join(correct)
else:
correct = '`%s`' % correct
if displayedMask.__class__ == list:
displayedMask = '`%s`' % '` ou `'.join(displayedMask)
else:
displayedMask = '`%s`' % displayedMask
self._raise('On ne dit pas %s mais %s' %
(displayedMask, correct))
def checkSMS(self):
self._detect(mode='single', correct='tout', mask='tt')
self._detect(mode='single', correct='tous', mask='ts')
self._detect(mode='single', correct='toute', mask='tte')
self._detect(mode='single', correct='toutes', mask='ttes')
self._detect(mode='single', correct="c'était", mask='ct')
self._detect(mode='single', correct="vais", mask='v')
self._detect(mode='single', correct=["aime", "aimes", "aiment"],
mask='m')
self._detect(mode='single', correct=['eu', 'eut'], mask='u')
self._detect(mode='regexp', correct="c'est",
mask="(?<!(du|Du|le|Le|en|En)) C (?<!c')",
displayedMask='C')
def checkPlural(self):
pass
def checkConjugaison(self):
self._detect(mode='regexp', correct="t'as oublié un `ne` ou un `n'`",
mask="(je|tu|on|il|elle|nous|vous|ils|elles) [^' ]+ pas")
self._detect(mode='regexp', correct="t'as oublié un `ne` ou un `n'`",
mask="j'[^' ]+ pas")
firstPerson = 'un verbe à la première personne ne finit pas par un `t`'
notAS = 'ce verbe ne devrait pas se finir par un `s` à cette personne.'
self._detect(mode='regexp', correct=firstPerson, mask="j'[^ ]*t")
self._detect(mode='regexp', correct=firstPerson,mask="je( ne)? [^ ]*t")
self._detect(mode='regexp', correct=notAS,
mask="(il|elle|on)( ne | n'| )[^ ]*s\W")
def checkSpelling(self):
self._detect(mode='regexp', correct='quelle', mask='quel [^ ]+ la',
displayedMask='quel')
self._detect(mode='regexp', correct='quel', mask='quelle [^ ]+ le',
displayedMask='quelle')
self._detect(mode='regexp', correct=['quels', 'quelles'],
mask='quel [^ ]+ les',
displayedMask='quel')
self._detect(mode='regexp', correct=['quels', 'quelles'],
mask='quelle [^ ]+ les',
displayedMask='quelle')
self._detect(mode='regexp',
correct=['quel', 'quels', 'quelle', 'quelles'],
mask='kel(le)?s?',
displayedMask=['kel', 'kels', 'kelle', 'kelles'])
def checkAbbreviation(self):
pass
def checkLol(self):
self._detect(mode='regexp', correct='mdr', mask='[Ll1][oO0 ]+[lL1]',
displayedMask='lol')
def checkTypographic(self):
self._detect(mode='regexp',
correct="Un caractère de ponctuation double est toujours "
"précédé d'un espace",
mask="[^ _][:!?;]", wizard='_')
self._detect(mode='regexp',
correct="Un caractère de ponctuation simple est toujours "
"suivi d'un espace",
mask="[:!?;][^ _]", wizard='_')
self._detect(mode='regexp',
correct="Un caractère de ponctuation simple n'est jamais "
"précédé d'un espace",
mask=" [.,]", wizard='_')
self._detect(mode='regexp',
correct="Un caractère de ponctuation simple est toujours "
"suivi d'un espace",
mask="[.,][^ _]", wizard='_')
def getErrors(self):
return self._errors
class GoodFrench(callbacks.Plugin):
def detect(self, irc, msg, args, text):
"""<texte>
Cherche des fautes dans le <texte>, en fonction de la valeur locale de
supybot.plugins.GoodFrench.level."""
checker = SpellChecker(text, self.registryValue('level', msg.channel))
errors = checker.getErrors()
if len(errors) == 0:
irc.reply('La phrase semble correcte')
elif len(errors) == 1:
irc.reply('Il semble y avoir une erreur : %s' % errors[0])
else:
irc.reply('Il semble y avoir des erreurs : %s' %
' | '.join(errors))
def doPrivmsg(self, irc, msg):
if world.testing: # FIXME
return
channel = msg.args[0]
prefix = msg.prefix
nick = prefix.split('!')[0]
text = msg.args[1]
checker = SpellChecker(text, self.registryValue('level', channel))
errors = checker.getErrors()
if len(errors) == 0:
return
elif len(errors) == 1:
reason = 'Erreur : %s' % errors[0]
else:
reason = 'Erreurs : %s' % ' | '.join(errors)
msg = ircmsgs.kick(channel, nick, reason)
irc.queueMsg(msg)
detect = wrap(detect, ['text'])
Class = GoodFrench
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:

94
GoodFrench/test.py Normal file
View File

@ -0,0 +1,94 @@
# -*- coding: utf8 -*-
###
# Copyright (c) 2010, Valentin Lorentz
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
from supybot.test import *
class GoodFrenchTestCase(ChannelPluginTestCase):
plugins = ('GoodFrench',)
config = {'plugins.GoodFrench.level': 7}
def _isKicked(self):
m = self.irc.takeMsg()
while m is not None:
if m.command == 'KICK':
return True
m = self.irc.takeMsg()
return False
_bad = "C tt"
_good = "C'est tout"
def testDetect(self):
self.assertRegexp("GoodFrench detect %s" % self._bad, 'erreurs : ')
self.assertRegexp("GoodFrench detect %s" % self._good, 'correcte')
def testKick(self):
msg = ircmsgs.privmsg(self.channel, self._bad,
prefix=self.prefix)
self.irc.feedMsg(msg)
self.failIf(self._isKicked() == False, 'Not kicked on misspell')
msg = ircmsgs.privmsg(self.channel, self._good,
prefix=self.prefix)
self.irc.feedMsg(msg)
self.failIf(self._isKicked(), 'Kicked on correct sentence')
def assertMistake(self, text):
try:
self.assertRegexp("GoodFrench detect %s" % text, 'erreurs? : ')
except AssertionError as e:
print text
raise e
def testMistakes(self):
for text in ["je suis pas là", "j'ai pas faim", "j'ait", "je ait",
"il es", "quel est la", "quelle est le",
"C'est bon; il est parti", "C'est bon , il est parti",
"C'est bon ,il est parti", "C'est bon ;il est parti",
"lol", "loooool", "l00oo ol", "LOOO00ool", "10001"]:
self.assertMistake(text)
def assertNoMistake(self, text):
try:
self.assertRegexp("GoodFrench detect %s" % text, 'correcte')
except AssertionError as e:
print text
raise e
def testNotMistakes(self):
for text in ["je ne suis pas là", "je n'ai pas faim", "j'ai",
"il est", "quelle est la", "quel est le",
"C'est bon ; il est parti", "C'est bon, il est parti"]:
self.assertNoMistake(text)
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: