GoodFrench: add plugin (v0.1)
parent
bbf9f37356
commit
d4d57b99a7
|
@ -0,0 +1 @@
|
|||
Insert a description of your plugin here, with any notes, etc. about using it.
|
|
@ -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:
|
|
@ -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:
|
|
@ -0,0 +1 @@
|
|||
# Stub so local is a module, used for third-party modules
|
|
@ -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.
|
|
@ -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:
|
|
@ -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:
|
Loading…
Reference in New Issue