In python PyQt signal's callback functions also have "hidden" parameter "state". This parameter is making harder function decoration by user-defined decorators.
My example of code and a decorator. In my solution i have to admit TypeErrors, caused by incorrect parameters count, for every callback without lambda, otherwice i can't decorate. Is there a better way, in which i can exclude admitting TypeError ? Can i ignore "state" parametr somehow else in my decorator?
Code example : decorator and program example.
from PyQt4 import QtCore, QtGui
def decor(func_fuc):
def new_func(*args, **kwargs):
#do something
try:
func_fuc(*args, **kwargs)
except TypeError as TypE:
if all(i in TypE.args[0] for i in ['takes', 'positional', 'argument' ,'but', 'given']):
args = tuple([args[ar] for ar in range(len(args)-1)])
func_fuc(*args, **kwargs)
else:
raise TypE
#etc
return new_func
class MainWind(QtGui.QMainWindow):
#decor
def __init__(self,parent=None,geometry = QtCore.QRect(100,100,800,800)):
super(MainWind, self).__init__(parent,geometry=geometry)
self.pushik = QtGui.QPushButton(
self, clicked = self.vasa
,geometry = QtCore.QRect(100,100,50,50), text = 'vasa'
)
self.petjaBtn = QtGui.QPushButton(
self, clicked = lambda state : self.petja()
,geometry = QtCore.QRect(100,200,50,50), text = 'petja'
)
self.someBtn = QtGui.QPushButton(
self, clicked = self.someBtnClick
,geometry = QtCore.QRect(100,300,50,50), text = 'someBtn'
)
self.show()
##decor
def vasa(*args):
print('vasa call')
print(args) #(<__main__.MainWind object at 0x018FF940>, False)
##decor
def petja(*args):
print('petja call')
print(args) #(<__main__.MainWind object at 0x018FF940>,)
def someBtnClick(self): # usual usage
print('someBtnClick call')
# signature : accept only "self", but works fine on button click(with 2 params, self and state)
# if i write empty decorator, that just returns original call, i will get typeError (reason - extra argument)
#how to decorate without admitting "argument" TypeError?
app=QtGui.QApplication([])
wind = MainWind()
app.exec_()
If you examine inspect.getfullargspec() of decorated functions, you'll see that their signatures are "wong".
See Fix Your Decorators , wrapt library.
But unfortunately writing the decorator using #wrapt.decorator() still don't solve the problem, though getfullargspec() looks correct. It seems that the PyQt do yet another code inspection to count the function attributes.
Related
Background
I'm working on a PyQt application and have added the following debug decorator:
def debug_decorator(func):
#functools.wraps(func)
def wrapper_func(*args, **kwargs):
logging.debug(f"Calling {func.__name__}")
ret_val = func(*args, **kwargs)
logging.debug(f"{func.__name__} returned {ret_val!r}")
return ret_val
return wrapper_func
Among other things, i use this for click handler functions, for example:
self.share_qpb = QtWidgets.QPushButton("Share")
self.share_qpb.clicked.connect(self.on_share_clicked)
[...]
#debug_decorator
def on_share_clicked(self):
If i run this code i get the following error when the share button is clicked:
TypeError: on_share_clicked() takes 1 positional argument but 2 were given
The reason for this is that the clicked signal in Qt/PyQt sends the checked state of the button (documentation)
This is only an issue when i decorate functions with my debug decorator, not otherwise (i guess the extra argument is discarded somewhere)
Question
How can i make my debug decorator work without having to add a parameter for every clicked signal handler?
(I'd like to find a solution which works both for cases when the arguments match and when there is one extra argument)
I'm running Python 3.8 and PyQt 5.15
I managed to solve this by checking to see if the number of arguments provided len(args) is more than the number of arguments that the decorated function accepts func.__code__.co_argcount
I also check if the name of the decorated function contains the string "clicked" since i have made a habit of having that as part of the name of my click handler functions. This has the advantage that if another decorated function is called with too many arguments we will (as usual) get an error
def debug_decorator(func):
#functools.wraps(func)
def wrapper_func(*args, **kwargs):
logging.debug(f"=== {func.__name__} was called")
func_arg_count_int = func.__code__.co_argcount
func_name_str = func.__name__
if len(args) > func_arg_count_int and "clicked" in func_name_str: # <----
args = args[:-1]
ret_val = func(*args, **kwargs)
logging.debug(f"=== {func.__name__} returned {ret_val!r}")
return ret_val
return wrapper_func
Thank you to #ekhumoro for helping me out with this!
I have a function in python that's used in many views. Specifically its in a django app running under uwsgi. The function just fires tracking data into our database. I wanted to create a decorator that would disable that function for a specific call to the view that contains the function. Essentially:
#disable tracking
def view(request):
track(request) //disabled by decorator
The decorator works by replacing the global definition of track with a void function that does nothing. Since we're running this under uwsgi which is multithreaded if I replace a global definition it will replace the function for all threads running under the process, so I defined the decorator to only activate if the tid and pid are equivalent. Here:
def disable_tracking(func):
#decorator
def inner(*args, **kwargs):
original_tracker = pascalservice.track.track
anon = lambda *args, **kwargs: None
tid = lambda : str(current_thread().ident)
pid = lambda : str(getpid())
uid = lambda : tid() + pid()
current_uid = uid()
cache.set(current_uid, True)
switcher = lambda *args, **kwargs: anon(*args, **kwargs) if cache.get(uid()) else original_tracker(*args, **kwargs)
pascalservice.track.track = switcher
result = func(*args, **kwargs)
cache.delete(current_uid)
pascalservice.track.track = original_tracker
return result
return inner
The wierd thing about this decorated function is that I'm getting occasional crashes and I want to verify if this style of coding is correct as it's a little unconventional.
What you are doing is called monkey patching. While not a totally bad practice it often leads to difficult to pinpoint bugs so use it with caution.
If the decorator is mandatory for some reason I would suggest adding some flag to the request object in your decorator and add a check for that flag in your track function.
The decorator :
def disable_tracking(func):
def wrapper(*args, **kwargs):
kwargs["request"].pascalservice_do_not_track = true
return func(*args, **kwargs)
return wrapper
Beginning of track function :
if hasattr(request, "pascalservice_do_not_track"):
return
# do the tracking ...
You may also just comment the line calling track in your view.
I struggled to think of a good title so I'll just explain it here. I'm using Python in Maya, which has some event callback options, so you can do something like on save: run function. I have a user interface class, which I'd like it to update when certain events are triggered, which I can do, but I'm looking for a cleaner way of doing it.
Here is a basic example similar to what I have:
class test(object):
def __init__(self, x=0):
self.x = x
def run_this(self):
print self.x
def display(self):
print 'load user interface'
#Here's the main stuff that used to be just 'test().display()'
try:
callbacks = [callback1, callback2, ...]
except NameError:
pass
else:
for i in callbacks:
try:
OpenMaya.MEventMessage.removeCallback(i)
except RuntimeError:
pass
ui = test(5)
callback1 = OpenMaya.MEventMessage.addEventCallback('SomeEvent', ui.run_this)
callback2 = OpenMaya.MEventMessage.addEventCallback('SomeOtherEvent', ui.run_this)
callback3 = ......
ui.display()
The callback persists until Maya is restarted, but you can remove it using removeCallback if you pass it the value that is returned from addEventCallback. The way I have currently is just check if the variable is set before you set it, which is a lot more messy than the previous one line of test().display()
Would there be a way that I can neatly do it in the function? Something where it'd delete the old one if I ran the test class again or something similar?
There are two ways you might want to try this.
You can an have a persistent object which represents your callback manager, and allow it to hook and unhook itself.
import maya.api.OpenMaya as om
import maya.cmds as cmds
om.MEventMessage.getEventNames()
class CallbackHandler(object):
def __init__(self, cb, fn):
self.callback = cb
self.function = fn
self.id = None
def install(self):
if self.id:
print "callback is currently installed"
return False
self.id = om.MEventMessage.addEventCallback(self.callback, self.function)
return True
def uninstall(self):
if self.id:
om.MEventMessage.removeCallback(self.id)
self.id = None
return True
else:
print "callback not currently installed"
return False
def __del__(self):
self.uninstall()
def test_fn(arg):
print "callback fired 2", arg
cb = CallbackHandler('NameChanged', test_fn)
cb.install()
# callback is active
cb.uninstall()
# callback not active
cb.install()
# callback on again
del(cb) # or cb = None
# callback gone again
In this version you'd store the CallbackHandlers you create for as long as you want the callback to persist and then manually uninstall them or let them fall out of scope when you don't need them any more.
Another option would be to create your own object to represent the callbacks and then add or remove any functions you want it to trigger in your own code. This keeps the management entirely on your side instead of relying on the api, which could be good or bad depending on your needs. You'd have an Event() class which was callable (using __call__() and it would have a list of functions to fire then its' __call__() was invoked by Maya. There's an example of the kind of event handler object you'd want here
Let's consider this piece of code where I would like to create bar dynamically with a decorator
def foo():
def bar():
print "I am bar from foo"
print bar()
def baz():
def bar():
print "I am bar from baz"
print bar()
I thought I could create bar from the outside with a decorator:
def bar2():
print "I am super bar from foo"
setattr(foo, 'bar', bar2)
But the result is not what I was expecting (I would like to get I am super bar from foo:
>>> foo()
I am bar from foo
Is it possible to override a sub-function on an existing function with a decorator?
The actual use case
I am writing a wrapper for a library and to avoid boilerplate code I would like to simplify my work.
Each library function has a prefix lib_ and returns an error code. I would like to add the prefix to the current function and treat the error code. This could be as simple as this:
def call():
fname = __libprefix__ + inspect.stack()[1][3]
return_code = getattr(__lib__, fname)(*args)
if return_code < 0: raise LibError(fname, return_code)
def foo():
call()
The problem is that call might act differently in certain cases. Some library functions do not return an error_code so it would be easier to write it like
this:
def foo():
call(check_status=True)
Or much better in my opinion (this is the point where I started thinking about decorators):
#LibFunc(check_status=True)
def foo():
call()
In this last example I should declare call inside foo as a sub-function created dynamically by the decorator itself.
The idea was to use something like this:
class LibFunc(object):
def __init__(self,**kwargs):
self.kwargs = kwargs
def __call__(self, original_func):
decorator_self = self
def wrappee( *args, **kwargs):
def call(*args):
fname = __libprefix__ + original_func.__name__
return_code = getattr(__lib__, fname)(*args)
if return_code < 0: raise LibError(fname, return_code)
print original_func
print call
# <<<< The part that does not work
setattr(original_func, 'call', call)
# <<<<
original_func(*args,**kwargs)
return wrappee
Initially I was tempted to call the call inside the decorator itself to minimize the writing:
#LibFunc():
foo(): pass
Unfortunately, this is not an option since other things should sometime be done before and after the call:
#LibFunc():
foo(a,b):
value = c_float()
call(a, pointer(value), b)
return value.value
Another option that I thought about was to use SWIG, but again this is not an option because I will need to rebuild the existing library with the SWIG wrapping functions.
And last but not least, I may get inspiration from SWIG typemaps and declare my wrapper as this:
#LibFunc(check_exit = true, map = ('<a', '>c_float', '<c_int(b)')):
foo(a,b): pass
This looks like the best solution to me, but this is another topic and another question...
Are you married to the idea of a decorator? Because if your goal is bunch of module-level functions each of which wraps somelib.lib_somefunctionname, I don't see why you need one.
Those module-level names don't have to be functions, they just have to be callable. They could be a bunch of class instances, as long as they have a __call__ method.
I used two different subclasses to determine how to treat the return value:
#!/usr/bin/env python3
import libtowrap # Replace with the real library name.
class Wrapper(object):
'''
Parent class for all wrapped functions in libtowrap.
'''
def __init__(self, name):
self.__name__ = str(name)
self.wrapped_name = 'lib_' + self.__name__
self.wrapped_func = getattr(libtowrap, self.wrapped_name)
self.__doc__ = self.wrapped_func.__doc__
return
class CheckedWrapper(Wrapper):
'''
Wraps functions in libtowrap that return an error code that must
be checked. Negative return values indicate an error, and will
raise a LibError. Successful calls return None.
'''
def __call__(self, *args, **kwargs):
error_code = self.wrapped_func(*args, **kwargs)
if error_code < 0:
raise LibError(self.__name__, error_code)
return
class UncheckedWrapper(Wrapper):
'''
Wraps functions in libtowrap that return a useful value, as
opposed to an error code.
'''
def __call__(self, *args, **kwargs):
return self.wrapped_func(*args, **kwargs)
strict = CheckedWrapper('strict')
negative_means_failure = CheckedWrapper('negative_means_failure')
whatever = UncheckedWrapper('whatever')
negative_is_ok = UncheckedWrapper('negative_is_ok')
Note that the wrapper "functions" are assigned while the module is being imported. They are in the top-level module namespace, and not hidden by any if __name__ == '__main__' test.
They will behave like functions for most purposes, but there will be minor differences. For example, I gave each instance a __name__ that matches the name they're assigned to, not the lib_-prefixed name used in libtowrap... but I copied the original __doc__, which might refer to a prefixed name like lib_some_other_function. Also, testing them with isinstance will probably surprise people.
For more about decorators, and for many more annoying little discrepancies like the ones I mentioned above, see Graham Dumpleton's half-hour lecture "Advanced Methods for Creating Decorators" (PyCon US 2014; slides). He is the author of the wrapt module (Python Package Index; Git Hub; Read the Docs), which corrects all(?) of the usual decorator inconsistencies. It might solve your problem entirely (except for the old lib_-style names showing up in __doc__).
I create this object when I want to create a QAction. I then add this QAction to a menu:
class ActionObject(object):
def __init__(self, owner, command):
action = QtGui.QAction(command.name, owner)
self.action = action
self.command = command
action.setShortcut(command.shortcut)
action.setStatusTip(command.name)
QtCore.QObject.connect(action, QtCore.SIGNAL('triggered()'), self.triggered)
def triggered(self):
print("got triggered " + self.command.id + " " + repr(checked))
Unfortunately, when the menu item is selected, the 'triggered' function is not called.
QtCore.QObject.connect() returns True.
Nothing is printed on the console to indicate that anything is wrong, and no exception is thrown.
How can I debug this? (or, what am I doing wrong?)
Maybe a bit late but I had the same problem and I solved it by changing:
class ActionObject(object)
to
class ActionObject()
I don't see you adding action to any menu in this code (indeed I see no addAction call anywhere), and there's a peculiar use of a variable checked that is never defined anywhere in your triggered method (is it a global defined elsewhere?). Both of these issues suggest you have other code that you're not showing (the code that adds this action to some menu or toolbar and defines the global variable checked -- or did you omit a parameter in the def triggered statement, perhaps...?), and that would be where the bugs lie (this code is a subset of a potentially correct program... but clearly there are missing parts, and how do we know they are correct?-).
Looks like your debugging has to occur in one of the two classes you don't provide; where you're attaching attributes to them then passing them to ActionObject as parameters.
I've created an example without this, as I have no idea what your other two classes look like. The third, parent class, I don't need, because of course it can be any generic Class that has inherited QWidget/QMainWindow/QDialog, etc
class ActionObject(object):
def __init__(self, owner=None, command=None):
self.menuBar = QtGui.QMenuBar(self)
self.setMenuBar(self.menuBar)
# Make sure the menu item's parent is the menu
self.menuGeneric = QtGui.QMenu(self.menuBar)
self.menuGeneric.setTitle('&Generic')
# Create and add the action in one line
self.menuBar.addAction(self.menuGeneric.menuAction())
QtCore.QObject.connect(self.menuGeneric, qc.SIGNAL('triggered()'), self.triggered)
def triggered(self):
print "got triggered"
For your two questions:
1) How do I debug this?
1) First thing I'd try would be to see if you've got the argument declaration for your function wrong (you have). To do that, I'd add *args and **kwargs to your function and then run the code to see if it works:
def triggered(self, *args, **kwargs):
print("got triggered " + self.command.id + " " + repr(checked) + " [extra args: {}, kwargs: {}".format(args, kwargs))
I bet you'll find you were getting a boolean as the first argument to the function, but your function was declared as not taking any. The exception was possibly getting logged to stderr or being swallowed.
2) I created a simple decorator to log these types of things for convenience:
import functools, sys
def logged_action(func):
#functools.wraps(func)
def wrapped(*args, **kwargs):
sys.stderr.write("{}: {}, {}\n".format(func, args, kwargs))
return func(*args, **kwargs)
return wrapped
#logged_action
def triggered(self, *args, **kwargs):
print("got triggered " + self.command.id + " " + repr(checked))
2) (or, what am I doing wrong)
Your connected method doesn't have the right signature, based on my experience with an example I had to hand:
Traceback (most recent call last):
File "[redacted]", line 12, in wrapped
return func(*args, **kwargs)
TypeError: triggered() takes exactly 1 argument (2 given)
triggered is being called with self and another argument (hence "2 given"), but you're only declaring you take one.