From fbb16b95491b084ab5f77aa0b07d2c9a274bb595 Mon Sep 17 00:00:00 2001 From: Kiooeht Date: Tue, 7 Jun 2011 08:48:35 -0700 Subject: [PATCH] Customizable Python quirks --- .gitignore | 2 ++ CHANGELOG.mkdn | 1 + PYQUIRKS.mkdn | 87 ++++++++++++++++++++++++++++++++++++++++++++++ TODO.mkdn | 1 - menus.py | 47 +++++++++++++++++++++++++ parsetools.py | 20 +++++------ pesterchum.py | 4 +++ pyquirks.py | 58 +++++++++++++++++++++++++++++++ quirks/defaults.py | 17 +++++++++ 9 files changed, 225 insertions(+), 12 deletions(-) create mode 100644 PYQUIRKS.mkdn create mode 100644 pyquirks.py create mode 100644 quirks/defaults.py diff --git a/.gitignore b/.gitignore index 20bcf40..b9f3db2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ tutorial/* irctest.log *.pyc pesterchum.js +quirks/* +!quirks/defaults.py diff --git a/CHANGELOG.mkdn b/CHANGELOG.mkdn index b153e1e..17b69c2 100644 --- a/CHANGELOG.mkdn +++ b/CHANGELOG.mkdn @@ -38,6 +38,7 @@ CHANGELOG * Memo OP options: Secret, Invite-only, Mute - Kiooeht [evacipatedBox] * Notify user if channel blocks message - Kiooeht [evacipatedBox] * Bug reporter - Kiooeht [evacipatedBox] +* Python quirks (users can create own quirk functions) - Kiooeht [evacipatedBox] * Bug fixes * Logviewer updates - Kiooeht [evacipatedBox] * Memo scrollbar thing - Kiooeht [evacipatedBox] diff --git a/PYQUIRKS.mkdn b/PYQUIRKS.mkdn new file mode 100644 index 0000000..343539b --- /dev/null +++ b/PYQUIRKS.mkdn @@ -0,0 +1,87 @@ +Python Quirk Functions +=============== + +0. Table of Contents +----------------- +1. Introduction +2. Create A Module +3. Functions In A Module +4. Command Requirements +5. Completed Quirk Function + +1. Introduction +--------------- +Over the course of this short tutorial you will learn: + +* How to create your own Quirk Functions +* VERY basic Python syntax + +You will not learn: + +* How to write Python +* How to bake a cake + +Throughout this tutorial there will be +
+Instructions in special boxes.
+If you follow the instructions in these boxes, by the end of this tutorial
+you will have recreated the default reverse() Quirk Function.
+
+ +2. Create A Module +------------------- +All Quirk Function Modules should be created in the 'quirks/' directory. File names must end in '.py'. +You can have multiple Quirk Functions per Module. + +Each Module can also have a 'setup' function which will be called once, the moment the Module is loaded. + +
+Create 'reverse.py' in the 'quirks/' directory.
+
+ +3. Functions In A Module +-------------------------- +If you've ever done programming before, you should know what a function is. If not, I suggest picking up a good programming book (or e-book). + +In Python, function syntax looks like this: + +def function_name(myvar1, myvar2): + +'def' is used to declare that this is a function, and 'function_name' is obviously the name of your function. +'myvar1' and 'myvar2' are variables coming into your function. For most of your functions, the only variable being passed will be 'text'. + +In Python, curly braces ({}) are not used to declare the beginning and end of a function. Instead, a colon (:) is used to declare the beginning of a function. After that, indentation is used to declare the body and end of a function. + +
+def reverserep(text):
+    return text[::-1]
+
+ +4. Command Requirements +------------------------ +For a function to be registered as a Quirk Function, it must conform to three simple rules: + +1. It must have a command name. +2. It must take exactly one arguement. +3. It must return a string. + +What is meant by having a command name, is that a name for the Quirk Function has to be defined. This is done by defining the 'command' variable for a function. + +function_name.command = "name" + +
+reverserep.command = "reverse"
+
+ +5. Completed Quirk Function +--------------------------- +Below is the completed, fully working, reverse Quirk Function. After it I will break down the pieces of each line. +
+def reverserep(text):
+    return text[::-1]
+reverserep.command = "reverse"
+
+ +As stated before, to start a function, you need to use the keyword 'def'. All Quirk Functions must take exactly one argument (in this case 'text'). +In this example, the text is reversed and returned all in one line. 'text[::-1]' is the Pythonic way of reversing a list or string. +The last line is the most important part. This tells Pesterchum to call this function whenever 'reverse()' is used in a quirk. diff --git a/TODO.mkdn b/TODO.mkdn index e7203de..60d0fd9 100644 --- a/TODO.mkdn +++ b/TODO.mkdn @@ -11,7 +11,6 @@ Features * Spy mode * Turn @ and # links on/off? * "someone has friended you" notifier -* Python quirks (so normal users can write their own quirk functions in python) Bugs ---- diff --git a/menus.py b/menus.py index 48223e3..f077ffb 100644 --- a/menus.py +++ b/menus.py @@ -326,6 +326,29 @@ class RandomQuirkDialog(MultiTextDialog): self.replacelist.takeItem(self.replacelist.currentRow()) self.replaceinput.setFocus() +class QuirkFuncWindow(QtGui.QDialog): + def __init__(self, parent): + QtGui.QDialog.__init__(self, parent) + self.mainwindow = parent + self.setStyleSheet(self.mainwindow.theme["main/defaultwindow/style"]) + self.setWindowTitle("Quirk Functions") + + self.funclist = QtGui.QListWidget(self) + self.funclist.setStyleSheet("background-color: #FFFFFF;") + + from parsetools import quirkloader + funcs = [q+")" for q in quirkloader.quirks.keys()] + funcs.sort() + self.funclist.addItems(funcs) + + layout_0 = QtGui.QVBoxLayout() + layout_0.addWidget(QtGui.QLabel("Avaliable Quirk Functions")) + layout_0.addWidget(self.funclist) + self.setLayout(layout_0) + + def closeEvent(self, event): + self.mainwindow.quirkmenu.funclistwindow = None + class PesterChooseQuirks(QtGui.QDialog): def __init__(self, config, theme, parent): QtGui.QDialog.__init__(self, parent) @@ -338,6 +361,12 @@ class PesterChooseQuirks(QtGui.QDialog): self.quirkList = PesterQuirkList(self.mainwindow, self) + self.viewQuirkFuncButton = QtGui.QPushButton("VIEW FUNCTIONS", self) + self.connect(self.viewQuirkFuncButton, QtCore.SIGNAL('clicked()'), + self, QtCore.SLOT('viewQuirkFuncSlot()')) + self.reloadQuirkFuncButton = QtGui.QPushButton("RELOAD FUNCTIONS", self) + self.connect(self.reloadQuirkFuncButton, QtCore.SIGNAL('clicked()'), + self, QtCore.SLOT('reloadQuirkFuncSlot()')) self.addPrefixButton = QtGui.QPushButton("ADD PREFIX", self) self.connect(self.addPrefixButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('addPrefixDialog()')) @@ -379,6 +408,9 @@ class PesterChooseQuirks(QtGui.QDialog): layout_quirklist.addWidget(self.quirkList) layout_quirklist.addLayout(layout_shiftbuttons) + layout_4 = QtGui.QHBoxLayout() + layout_4.addWidget(self.viewQuirkFuncButton) + layout_4.addWidget(self.reloadQuirkFuncButton) layout_1 = QtGui.QHBoxLayout() layout_1.addWidget(self.addPrefixButton) layout_1.addWidget(self.addSuffixButton) @@ -411,6 +443,7 @@ class PesterChooseQuirks(QtGui.QDialog): layout_0 = QtGui.QVBoxLayout() layout_0.addLayout(layout_quirklist) + layout_0.addLayout(layout_4) layout_0.addLayout(layout_1) layout_0.addLayout(layout_2) layout_0.addLayout(layout_3) @@ -425,6 +458,20 @@ class PesterChooseQuirks(QtGui.QDialog): return u #return [self.quirkList.item(i).quirk for i in range(self.quirkList.count())] + @QtCore.pyqtSlot() + def viewQuirkFuncSlot(self): + if not hasattr(self, 'funclistwindow'): + self.funclistwindow = None + if self.funclistwindow: + return + self.funclistwindow = QuirkFuncWindow(self.mainwindow) + self.funclistwindow.show() + + @QtCore.pyqtSlot() + def reloadQuirkFuncSlot(self): + from parsetools import reloadQuirkFunctions + reloadQuirkFunctions() + @QtCore.pyqtSlot() def editSelected(self): q = self.quirkList.currentQuirk() diff --git a/parsetools.py b/parsetools.py index 711508b..58578e1 100644 --- a/parsetools.py +++ b/parsetools.py @@ -5,6 +5,7 @@ from datetime import timedelta from PyQt4 import QtGui from generic import mysteryTime +from pyquirks import PythonQuirks _ctag_begin = re.compile(r'(?i)') _gtag_begin = re.compile(r'(?i)') @@ -16,9 +17,15 @@ _handlere = re.compile(r"(\s|^)(@[A-Za-z0-9_]+)") _imgre = re.compile(r"""(?i)""") _mecmdre = re.compile(r"^(/me|PESTERCHUM:ME)(\S*)") -_functionre = re.compile(r"(upper\(|lower\(|scramble\(|reverse\(|\)|\\[0-9]+)") +quirkloader = PythonQuirks() +_functionre = re.compile(r"%s" % quirkloader.funcre()) _groupre = re.compile(r"\\([0-9]+)") +def reloadQuirkFunctions(): + quirkloader.load() + global _functionre + _functionre = re.compile(r"%s" % quirkloader.funcre()) + def lexer(string, objlist): """objlist is a list: [(objecttype, re),...] list is in order of preference""" stringlist = [string] @@ -300,14 +307,6 @@ def timeDifference(td): timetext = "%d HOURS %s" % (hours, when) return timetext -def upperrep(text): - return text.upper() -def lowerrep(text): - return text.lower() -def scramblerep(text): - return "".join(random.sample(text, len(text))) -def reverserep(text): - return text[::-1] def nonerep(text): return text @@ -339,8 +338,7 @@ def parseRegexpFunctions(to): parsed = parseLeaf(nonerep, None) current = parsed curi = 0 - functiondict = {"upper(": upperrep, "lower(": lowerrep, - "scramble(": scramblerep, "reverse(": reverserep} + functiondict = quirkloader.quirks while curi < len(to): tmp = to[curi:] mo = _functionre.search(tmp) diff --git a/pesterchum.py b/pesterchum.py index 28f6a4d..bfd1b47 100644 --- a/pesterchum.py +++ b/pesterchum.py @@ -2355,9 +2355,13 @@ class PesterWindow(MovingWindow): item.quirk.quirk["group"] = item.quirk.group = curgroup quirks = pesterQuirks(self.quirkmenu.quirks()) self.userprofile.setQuirks(quirks) + if hasattr(self.quirkmenu, 'funclistwindow') and self.quirkmenu.funclistwindow: + self.quirkmenu.funclistwindow.close() self.quirkmenu = None @QtCore.pyqtSlot() def closeQuirks(self): + if hasattr(self.quirkmenu, 'funclistwindow') and self.quirkmenu.funclistwindow: + self.quirkmenu.funclistwindow.close() self.quirkmenu = None @QtCore.pyqtSlot() def openLogv(self): diff --git a/pyquirks.py b/pyquirks.py new file mode 100644 index 0000000..48288b0 --- /dev/null +++ b/pyquirks.py @@ -0,0 +1,58 @@ +import os, sys, imp, re + +class PythonQuirks(object): + def __init__(self): + self.home = os.getcwd() + self.quirks = {} + self.last = {} + self.load() + + def load(self): + self.last = self.quirks.copy() + self.quirks.clear() + filenames = [] + if not os.path.exists(os.path.join(self.home, 'quirks')): + os.mkdir(os.path.join(self.home, 'quirks')) + for fn in os.listdir(os.path.join(self.home, 'quirks')): + if fn.endswith('.py') and not fn.startswith('_'): + filenames.append(os.path.join(self.home, 'quirks', fn)) + + modules = [] + for filename in filenames: + name = os.path.basename(filename)[:-3] + try: module = imp.load_source(name, filename) + except Exception, e: + print "Error loading %s: %s (in pyquirks.py)" % (name, e) + else: + if hasattr(module, 'setup'): + module.setup() + self.register(vars(module)) + modules.append(name) + for k in self.last: + if k in self.quirks: + if self.last[k] == self.quirks[k]: + del self.quirks[k] + + if self.quirks: + print 'Registered quirks:', '), '.join(self.quirks) + ")" + else:print "Warning: Couldn't find any python quirks" + + def register(self, variables): + for name, obj in variables.iteritems(): + if hasattr(obj, 'command'): + try: + if not isinstance(obj("test"), basestring): + raise Exception + except: + print "Quirk malformed: %s" % (obj.command) + else: + self.quirks[obj.command+"("] = obj + + def funcre(self): + if not self.quirks: + return r"" + f = r"(" + for q in self.quirks: + f = f + q[:-1]+r"\(|" + f = f + r"\)|\\[0-9]+)" + return f diff --git a/quirks/defaults.py b/quirks/defaults.py new file mode 100644 index 0000000..7508adc --- /dev/null +++ b/quirks/defaults.py @@ -0,0 +1,17 @@ +import random + +def upperrep(text): + return text.upper() +upperrep.command = "upper" + +def lowerrep(text): + return text.lower() +lowerrep.command = "lower" + +def scramblerep(text): + return "".join(random.sample(text, len(text))) +scramblerep.command = "scramble" + +def reverserep(text): + return text[::-1] +reverserep.command = "reverse"