├── .gitignore ├── ChangeLog.txt ├── LICENSE.txt ├── MANIFEST.in ├── PySideKick ├── Call.py ├── Console.py ├── __init__.py └── tests │ ├── __init__.py │ └── test_misc.py ├── README.md ├── TODO.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *~ 4 | .*.sw* 5 | MANIFEST 6 | build 7 | dist 8 | -------------------------------------------------------------------------------- /ChangeLog.txt: -------------------------------------------------------------------------------- 1 | 2 | v0.1.0: 3 | 4 | * initial release; you might say *everything* has changed. 5 | 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Cloud Matrix Pty. Ltd. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Cloud Matrix Pty. Ltd. nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | 2 | include LICENSE.txt 3 | include ChangeLog.txt 4 | include README.txt 5 | 6 | -------------------------------------------------------------------------------- /PySideKick/Call.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2009-2010, Cloud Matrix Pty. Ltd. 2 | # All rights reserved; available under the terms of the BSD License. 3 | """ 4 | 5 | PySideKick.Call: helpers for managing function calls 6 | ===================================================== 7 | 8 | 9 | This module defines a collection of helpers for managing function calls in 10 | cooperation with the Qt event loop. We have: 11 | 12 | * qCallAfter: call function after current event has been processed 13 | * qCallLater: call function after sleeping for some interval 14 | * qCallInMainThread: (blockingly) call function in the main GUI thread 15 | * qCallInWorkerThread: (asynchronously) call function in worker thread 16 | 17 | 18 | There is also a decorator to apply these helpers to all calls to a function: 19 | 20 | * qCallUsing(helper): route all calls to a function through the helper 21 | 22 | """ 23 | 24 | import sys 25 | import thread 26 | import threading 27 | from functools import wraps 28 | import Queue 29 | 30 | import PySideKick 31 | from PySideKick import QtCore 32 | 33 | 34 | # I can't work out how to extract the main thread from a running PySide app. 35 | # Until then, we assue that whoever imports this module is the main thread. 36 | _MAIN_THREAD_ID = thread.get_ident() 37 | 38 | 39 | class qCallAfter(QtCore.QObject): 40 | """Call the given function on a subsequent iteration of the event loop. 41 | 42 | This helper arranges for the given function to be called on a subsequent 43 | iteration of the main event loop. It's most useful inside event handlers, 44 | where you may want to defer some work unti after the event has finished 45 | being processed. 46 | 47 | The implementation is as a singleton QObject subclass. It maintains a 48 | queue of functions to the called, and posts an event to itself whenever 49 | a new function is queued. 50 | """ 51 | 52 | def __init__(self): 53 | super(qCallAfter,self).__init__(None) 54 | self.app = None 55 | self.event_id = QtCore.QEvent.registerEventType() 56 | self.event_type = QtCore.QEvent.Type(self.event_id) 57 | self.pending_func_queue = Queue.Queue() 58 | self.func_queue = Queue.Queue() 59 | 60 | def customEvent(self,event): 61 | if event.type() == self.event_type: 62 | self._popCall() 63 | 64 | def __call__(self,func,*args,**kwds): 65 | if self.app is None: 66 | app = QtCore.QCoreApplication.instance() 67 | if app is None: 68 | self.pending_func_queue.put((func,args,kwds)) 69 | else: 70 | self.app = app 71 | try: 72 | while True: 73 | self.func_queue.put(self.pending_func_queue.get(False)) 74 | self._postEvent() 75 | except Queue.Empty: 76 | pass 77 | self.func_queue.put((func,args,kwds)) 78 | self._postEvent() 79 | else: 80 | self.func_queue.put((func,args,kwds)) 81 | self._postEvent() 82 | 83 | def _popCall(self): 84 | (func,args,kwds) = self.func_queue.get(False) 85 | func(*args,**kwds) 86 | 87 | def _postEvent(self): 88 | event = QtCore.QEvent(self.event_type) 89 | try: 90 | self.app.postEvent(self,event) 91 | except RuntimeError: 92 | # This can happen if the app has been destroyed. 93 | # Empty the queue. 94 | try: 95 | while True: 96 | self._popCall() 97 | except Queue.Empty: 98 | pass 99 | 100 | # Things could get a little tricky here. The object must belong to the 101 | # same thread as the QApplication. For now we assume that this module 102 | # has been imported from the main thread. 103 | qCallAfter = qCallAfter() 104 | 105 | 106 | 107 | class Future(object): 108 | """Primative "future" class, for executing functions in another thread.""" 109 | 110 | _READY_INSTANCES = [] 111 | 112 | def __init__(self): 113 | self.ready = threading.Event() 114 | self.result = None 115 | self.exception = None 116 | 117 | @classmethod 118 | def get_or_create(cls): 119 | try: 120 | return cls._READY_INSTANCES.pop(0) 121 | except IndexError: 122 | return cls() 123 | 124 | def recycle(self): 125 | self.result = None 126 | self.exception = None 127 | self.ready.clear() 128 | self._READY_INSTANCES.append(self) 129 | 130 | def call_function(self,func,*args,**kwds): 131 | try: 132 | self.result = func(*args,**kwds) 133 | except Exception: 134 | self.exception = sys.exc_info() 135 | finally: 136 | self.ready.set() 137 | 138 | def get_result(self): 139 | self.ready.wait() 140 | try: 141 | if self.exception is None: 142 | return self.result 143 | else: 144 | raise self.exception[0], \ 145 | self.exception[1], \ 146 | self.exception[2] 147 | finally: 148 | self.recycle() 149 | 150 | 151 | def qCallInMainThread(func,*args,**kwds): 152 | """Synchronosly call the given function in the main thread. 153 | 154 | This helper arranges for the given function to be called in the main 155 | event loop, then blocks and waits for the result. It's a simple way to 156 | call functions that manipulate the GUI from a non-GUI thread. 157 | """ 158 | if thread.get_ident() == PySideKick._MAIN_THREAD_ID: 159 | func(*args,**kwds) 160 | else: 161 | future = Future.get_or_create() 162 | qCallAfter(future.call_function,func,*args,**kwds) 163 | return future.get_result() 164 | 165 | 166 | def qCallInWorkerThread(func,*args,**kwds): 167 | """Asynchronously call the given function in a background worker thread. 168 | 169 | This helper arranges for the given function to be executed by a background 170 | worker thread. Eventually it'll get a thread pool; for now each task 171 | spawns a new background thread. 172 | 173 | If you need to know the result of the function call, this helper returns 174 | a Future object; use f.ready.isSet() to test whether it's ready and call 175 | f.get_result() to get the return value or raise the exception. 176 | """ 177 | future = Future.get_or_create() 178 | def runit(): 179 | future.call_function(func,*args,**kwds) 180 | threading.Thread(target=runit).start() 181 | return future 182 | 183 | 184 | def qCallLater(interval,func,*args,**kwds): 185 | """Asynchronously call the given function after a timeout. 186 | 187 | This helper is similar to qCallAfter, but it waits at least 'interval' 188 | seconds before executing the function. To cancel the call before the 189 | sleep interval has expired, call 'cancel' on the returned object. 190 | 191 | Currently this is a thin wrapper around threading.Timer; eventually it 192 | will be integrated with Qt's own timer mechanisms. 193 | """ 194 | def runit(): 195 | qCallAfter(func,*args,**kwds) 196 | t = threading.Timer(interval,runit) 197 | t.start() 198 | return t 199 | 200 | 201 | def qCallUsing(helper): 202 | """Function/method decorator to always apply a function call helper. 203 | 204 | This decorator can be used to ensure that a function is always called 205 | using one of the qCall helpers. For example, the following function can 206 | be safely called from any thread, as it will transparently apply the 207 | qCallInMainThread helper whenever it is called: 208 | 209 | @qCallUsing(qCallInMainThread) 210 | def prompt_for_input(msg): 211 | # ... pop up a dialog, return input 212 | 213 | """ 214 | def decorator(func): 215 | @wraps(func) 216 | def wrapper(*args,**kwds): 217 | return helper(func,*args,**kwds) 218 | return wrapper 219 | return decorator 220 | 221 | 222 | -------------------------------------------------------------------------------- /PySideKick/Console.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2009-2010, Cloud Matrix Pty. Ltd. 2 | # All rights reserved; available under the terms of the BSD License. 3 | """ 4 | PySideKick.Console: a simple embeddable python shell 5 | ===================================================== 6 | 7 | 8 | This module provides the call QPythonConsole, a python shell that can be 9 | embedded in your GUI. 10 | 11 | """ 12 | 13 | from code import InteractiveConsole as _InteractiveConsole 14 | import sys 15 | 16 | try: 17 | from PySideKick import QtCore, QtGui 18 | except ImportError: 19 | from PySide import QtCore, QtGui 20 | 21 | try: 22 | from cStringIO import StringIO 23 | except ImportError: 24 | try: 25 | from StringIO import StringIO 26 | except ImportError: 27 | from io import StringIO 28 | 29 | 30 | class _QPythonConsoleInterpreter(_InteractiveConsole): 31 | """InteractiveConsole subclass that sends all output to the GUI.""" 32 | 33 | def __init__(self,ui,locals=None): 34 | _InteractiveConsole.__init__(self,locals) 35 | self.ui = ui 36 | 37 | def write(self,data): 38 | if data: 39 | if data[-1] == "\n": 40 | data = data[:-1] 41 | self.ui.output.appendPlainText(data) 42 | 43 | def runsource(self,source,filename="",symbol="single"): 44 | old_stdout = sys.stdout 45 | old_stderr = sys.stderr 46 | sys.stdout = sys.stderr = collector = StringIO() 47 | try: 48 | more = _InteractiveConsole.runsource(self,source,filename,symbol) 49 | finally: 50 | if sys.stdout is collector: 51 | sys.stdout = old_stdout 52 | if sys.stderr is collector: 53 | sys.stderr = old_stderr 54 | self.write(collector.getvalue()) 55 | return more 56 | 57 | 58 | class _QPythonConsoleUI(object): 59 | """UI layout container for QPythonConsole.""" 60 | def __init__(self,parent): 61 | if parent.layout() is None: 62 | parent.setLayout(QtGui.QHBoxLayout()) 63 | layout = QtGui.QVBoxLayout() 64 | layout.setSpacing(0) 65 | # Output console: a fixed-pitch-font text area. 66 | self.output = QtGui.QPlainTextEdit(parent) 67 | self.output.setReadOnly(True) 68 | self.output.setUndoRedoEnabled(False) 69 | self.output.setMaximumBlockCount(5000) 70 | fmt = QtGui.QTextCharFormat() 71 | fmt.setFontFixedPitch(True) 72 | self.output.setCurrentCharFormat(fmt) 73 | layout.addWidget(self.output) 74 | parent.layout().addLayout(layout) 75 | # Input console, a prompt displated next to a lineedit 76 | layout2 = QtGui.QHBoxLayout() 77 | self.prompt = QtGui.QLabel(parent) 78 | self.prompt.setText(">>> ") 79 | layout2.addWidget(self.prompt) 80 | self.input = QtGui.QLineEdit(parent) 81 | layout2.addWidget(self.input) 82 | layout.addLayout(layout2) 83 | 84 | 85 | class QPythonConsole(QtGui.QWidget): 86 | """A simple python console to embed in your GUI. 87 | 88 | This widget provides a simple interactive python console that you can 89 | embed in your GUI (e.g. for debugging purposes). Use it like so: 90 | 91 | self.debug_window.layout().addWidget(QPythonConsole()) 92 | 93 | You can customize the variables that are available in the shell by 94 | passing a dict as the "locals" argument. 95 | """ 96 | 97 | def __init__(self,parent=None,locals=None): 98 | super(QPythonConsole,self).__init__(parent) 99 | self.ui = _QPythonConsoleUI(self) 100 | self.interpreter = _QPythonConsoleInterpreter(self.ui,locals) 101 | self.ui.input.returnPressed.connect(self._on_enter_line) 102 | self.ui.input.installEventFilter(self) 103 | self.history = [] 104 | self.history_pos = 0 105 | 106 | def _on_enter_line(self): 107 | line = self.ui.input.text() 108 | self.ui.input.setText("") 109 | self.interpreter.write(self.ui.prompt.text() + line) 110 | more = self.interpreter.push(line) 111 | if line: 112 | self.history.append(line) 113 | self.history_pos = len(self.history) 114 | while len(self.history) > 100: 115 | self.history = self.history[1:] 116 | self.history_pos -= 1 117 | if more: 118 | self.ui.prompt.setText("... ") 119 | else: 120 | self.ui.prompt.setText(">>> ") 121 | 122 | def eventFilter(self,obj,event): 123 | if event.type() == QtCore.QEvent.KeyPress: 124 | if event.key() == QtCore.Qt.Key_Up: 125 | self.go_history(-1) 126 | elif event.key() == QtCore.Qt.Key_Down: 127 | self.go_history(1) 128 | return False 129 | 130 | def go_history(self,offset): 131 | if offset < 0: 132 | self.history_pos = max(0,self.history_pos + offset) 133 | elif offset > 0: 134 | self.history_pos = min(len(self.history),self.history_pos + offset) 135 | try: 136 | line = self.history[self.history_pos] 137 | except IndexError: 138 | line = "" 139 | self.ui.input.setText(line) 140 | 141 | 142 | if __name__ == "__main__": 143 | app = QtGui.QApplication(sys.argv) 144 | win = QtGui.QMainWindow() 145 | win.setCentralWidget(QPythonConsole()) 146 | win.show() 147 | app.exec_() 148 | -------------------------------------------------------------------------------- /PySideKick/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2009-2010, Cloud Matrix Pty. Ltd. 2 | # All rights reserved; available under the terms of the BSD License. 3 | """ 4 | 5 | PySideKick: helpful utilities for working with PySide 6 | ====================================================== 7 | 8 | 9 | This package is a rather ad-hoc collection of helpers, utilities and custom 10 | widgets for building applications with PySide. So far we have: 11 | 12 | * PySideKick.Call: helpers for calling functions in a variety of ways, 13 | e.g. qCallAfter, qCallInMainThread 14 | 15 | * PySideKick.Console: a simple interactive console to embed in your 16 | application 17 | 18 | """ 19 | 20 | __ver_major__ = 0 21 | __ver_minor__ = 1 22 | __ver_patch__ = 0 23 | __ver_sub__ = "" 24 | __ver_tuple__ = (__ver_major__,__ver_minor__,__ver_patch__,__ver_sub__) 25 | __version__ = "%d.%d.%d%s" % __ver_tuple__ 26 | 27 | 28 | import thread 29 | 30 | from PySide import QtCore, QtGui 31 | from PySide.QtCore import Qt 32 | 33 | 34 | # I can't work out how to extract the main thread from a running PySide app. 35 | # Until then, we assue that whoever imports this module is the main thread. 36 | _MAIN_THREAD_ID = thread.get_ident() 37 | 38 | -------------------------------------------------------------------------------- /PySideKick/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmatrix/pysidekick/396c9e72e53434326042c9fec2f28445d7fd5402/PySideKick/tests/__init__.py -------------------------------------------------------------------------------- /PySideKick/tests/test_misc.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | import os 5 | import sys 6 | import shutil 7 | 8 | import PySideKick 9 | 10 | class TestMisc(unittest.TestCase): 11 | 12 | def test_README(self): 13 | """Ensure that the README is in sync with the docstring. 14 | 15 | This test should always pass; if the README is out of sync it just 16 | updates it with the contents of PySideKick.__doc__. 17 | """ 18 | dirname = os.path.dirname 19 | readme = os.path.join(dirname(dirname(dirname(__file__))),"README.txt") 20 | if not os.path.isfile(readme): 21 | f = open(readme,"wb") 22 | f.write(PySideKick.__doc__.encode()) 23 | f.close() 24 | else: 25 | f = open(readme,"rb") 26 | if f.read() != PySideKick.__doc__: 27 | f.close() 28 | f = open(readme,"wb") 29 | f.write(PySideKick.__doc__.encode()) 30 | f.close() 31 | 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Status: Unmaintained 4 | ==================== 5 | 6 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 7 | 8 | I am [no longer actively maintaining this project](https://rfk.id.au/blog/entry/archiving-open-source-projects/). 9 | 10 | 11 | PySideKick: helpful utilities for working with PySide 12 | ====================================================== 13 | 14 | 15 | This package is a rather ad-hoc collection of helpers, utilities and custom 16 | widgets for building applications with PySide. So far we have: 17 | 18 | * PySideKick.Call: helpers for calling functions in a variety of ways, 19 | e.g. qCallAfter, qCallInMainThread 20 | 21 | * PySideKick.Console: a simple interactive console to embed in your 22 | application 23 | 24 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | 2 | ...nothing at the moment... 3 | 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2009-2010, Cloud Matrix Pty. Ltd. 2 | # All rights reserved; available under the terms of the BSD License. 3 | 4 | import os 5 | import sys 6 | setup_kwds = {} 7 | if sys.version_info > (3,): 8 | from setuptools import setup 9 | setup_kwds["test_suite"] = "PySideKick.tests" 10 | setup_kwds["use_2to3"] = True 11 | else: 12 | from distutils.core import setup 13 | 14 | # This awfulness is all in aid of grabbing the version number out 15 | # of the source code, rather than having to repeat it here. Basically, 16 | # we parse out all lines starting with "__version__" and execute them. 17 | try: 18 | next = next 19 | except NameError: 20 | def next(i): 21 | return i.next() 22 | info = {} 23 | try: 24 | src = open("PySideKick/__init__.py") 25 | lines = [] 26 | ln = next(src) 27 | while "__version__" not in ln: 28 | lines.append(ln) 29 | ln = next(src) 30 | while "__version__" in ln: 31 | lines.append(ln) 32 | ln = next(src) 33 | exec("".join(lines),info) 34 | except Exception: 35 | pass 36 | 37 | 38 | # Screw the MANIFEST file, it just caches out of date data and messes 39 | # up my builds. 40 | mfst = os.path.join(os.path.dirname(__file__),"MANIFEST") 41 | if os.path.exists(mfst): 42 | os.unlink(mfst) 43 | 44 | 45 | NAME = "PySideKick" 46 | VERSION = info["__version__"] 47 | DESCRIPTION = "helpful utils for working with PySide" 48 | AUTHOR = "Ryan Kelly" 49 | AUTHOR_EMAIL = "rfk@cloudmatrix.com.au" 50 | URL = "http://github.com/cloudmatrix/pysidekick/" 51 | LICENSE = "BSD" 52 | KEYWORDS = "GUI Qt PySide" 53 | LONG_DESC = info["__doc__"] 54 | 55 | PACKAGES = ["PySideKick","PySideKick.tests"] 56 | EXT_MODULES = [] 57 | PKG_DATA = {} 58 | 59 | setup(name=NAME, 60 | version=VERSION, 61 | author=AUTHOR, 62 | author_email=AUTHOR_EMAIL, 63 | url=URL, 64 | description=DESCRIPTION, 65 | long_description=LONG_DESC, 66 | keywords=KEYWORDS, 67 | packages=PACKAGES, 68 | ext_modules=EXT_MODULES, 69 | package_data=PKG_DATA, 70 | license=LICENSE, 71 | **setup_kwds 72 | ) 73 | 74 | --------------------------------------------------------------------------------