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!
Related
I'm quite new to decorators and I'm trying to build a decorator with an argument that should work both as a decorator and a stand-alone function. The basic idea is to raise an error if some condition is not satisfied. Example:
ensure_condition("fail") # exception should be raised
ensure_condition("pass") # nothing should happen
#ensure_condition("fail") # check condition before every `func` call
def f1():
return 1
I thought about doing this:
def ensure_condition(arg: str):
if not _validate(arg):
raise Exception("failed")
def ensure_condition_decorator(f = lambda *_: None):
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper
return ensure_condition_decorator
But the above results in the _validate function being called also when the f1 function is declared (not only executed).
Any other ideas?
Thanks!
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.
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.
So, this is a 2 part question -
Is there an idiomatic way in python to inject a parameter into the function signature when using a decorator?
For example:
def _mydecorator(func):
def wrapped(someval, *args, **kwargs):
do_something(someval)
return func(*args, **kwargs)
return wrapped
#_mydecorator
def foo(thisval, thatval=None):
do_stuff()
The reason around this is when using SaltStack's runner modules You define funcs within the module, and you can call those functions via the 'salt-run' command. If the above example was a Salt runner module call 'bar', I could then run:
salt-run bar.foo aval bval
The salt-run imports the module and calls the function with the arguments you've given on the command line. Any function in the module that begins with a _ or that is in a class is ignored and cannot be run via salt-run.
So, I wanted to define something like a timeout decorator to control how long the function can run for.
I realize I could do something like:
#timeout(30)
def foo():
...
But I wanted to make that value configurable, so that I could run something like:
salt-run bar.foo 30 aval bval
salt-run bar.foo 60 aval bval
The above decorator works, but it feels like a hack, since it's changing the signature of the function and the user has no idea, unless they look at the decorator.
I have another case where I want to make a decorator for taking care of 'prechecks' before the functions in the Salt runner execute. However, the precheck needs a piece of information from the function it's decorating. Here's an example:
def _precheck(func):
def wrapper(*args, **kwargs):
ok = False
if len(args) > 0:
ok = run_prechecks(args[0])
else:
ok = run_prechecks(kwargs['record_id'])
if ok:
func(*args, **kwargs)
return wrapper
#_precheck
def foo(record_id, this_val):
do_stuff()
This also seems hackish, since it requires that the function that's being decorated, a) has a parameter called 'record_id' and that b) it's the first argument.
Now, because I'm writing all these functions, it's not a huge deal, but seems like there's probably a better way of doing this ( like not using decorators to try and solve this )
The way to dynamically define decorator arguments is not using the syntactic sugar (#). Like this:
func = dec(dec_arguments)(func_name)(func_arguments)
import json
import sys
foo = lambda record_id, thatval=None: do_stuff(record_id, thatval)
def do_stuff(*args, **kwargs):
# python3
print(*args, json.dumps(kwargs))
def _mydecorator(timeout):
print('Timeout: %s' % timeout)
def decorator(func):
def wrapped(*args, **kwargs):
return func(*args, **kwargs)
return wrapped
return decorator
if __name__ == '__main__':
_dec_default = 30
l_argv = len(sys.argv)
if l_argv == 1:
# no args sent
sys.exit('Arguments missing')
elif l_argv == 2:
# assuming record_id is a required argument
_dec_arg = _dec_default
_args = 1
else:
# three or more args: filename 1 2 [...]
# consider using optional keyword argument `timeout`
# otherwise in combination with another optional argument it's a recipe for disaster
# if only two arguments will be given - with current logic it will be tested for `timeoutedness`
try:
_dec_arg = int(sys.argv[1])
_args = 2
except (ValueError, IndexError):
_dec_arg = _dec_default
_args = 1
foo = _mydecorator(_dec_arg)(foo)(*sys.argv[_args:])
There is no idiomatic way to do this in python 3.7 as far as I know. Indeed #functools.wraps only works when you do not modify the signature (see what does functools.wraps do ?)
However there is a way to do the same as #functools.wraps (exposing the full signature, preserving the __dict__ and docstring): #makefun.wraps. With this drop-in replacement for #wraps, you can edit the exposed signature.
from makefun import wraps
def _mydecorator(func):
#wraps(func, prepend_args="someval")
def wrapped(someval, *args, **kwargs):
print("wrapper executes with %r" % someval)
return func(*args, **kwargs)
return wrapped
#_mydecorator
def foo(thisval, thatval=None):
"""A foo function"""
print("foo executes with thisval=%r thatval=%r" % (thisval, thatval))
# let's check the signature
help(foo)
# let's test it
foo(5, 1)
Yields:
Help on function foo in module __main__:
foo(someval, thisval, thatval=None)
A foo function
wrapper executes with 5
foo executes with thisval=1 thatval=None
You can see that the exposed signature contains the prepended argument.
The same mechanism applies for appending and removing arguments, as well as editing signatures more deeply. See makefun documentation for details (I'm the author by the way ;) )
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.