I want to write a decorator in python: if a called function contains print's, the decorator prints her name before this function being called. I'm familiar with decorators syntax, but I have a problem with checking if a function has print within itself.
def preceeding_name(func):
#wraps(func)
def wrapper(*args, **kwargs):
if 'print' in func:
print(func.__name__)
result = func(*args, **kwargs)
return result
return wrapper
It is not necessary to check if the print's from function will actually be called.
This can be done by holding the buffer of 'print' from flushing and checking it to see if a print has been done.
class Out(object):
def write(self,s):
self.s += s
def __init__(self)
self.s = ''
Now to check
def wrapper(*args, **kwargs):
our_out = Out()
sys.stdout = our_out
result = func(*args, **kwargs)
if len(our_out.s)>0:
sys.stdout = sys.__stdout__
print func.__name__
for s in our_out.s.split('\n'):
print s
return result
I this case you can redefine print
def preceeding_name(func):
#wraps(func)
def wrapper(*args, **kwargs):
old_print = print
def print(*arg, **kwarg):
old_print(func.__name__)
old_print(*arg, **kwarg)
result = func(*args, **kwargs)
return result
return wrapper
EDIT:
i test and this work
old_print = print
def preceeding_name(func):
def print(*arg, **kwarg):
old_print(func.__name__, end='')
old_print(*arg, **kwarg)
def wrapper(*args, **kwargs):
print('')
result = func(*args, **kwargs)
return result
return wrapper
#preceeding_name
def a():
print('hi')
a()
#preceeding_name
def b():
print('hi')
b()
EDIT2:
old_print = print
def preceeding_name(func):
global print
def print(*arg, **kwarg):
old_print(func.__name__)
old_print(*arg, **kwarg)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
#preceeding_name
def a():
print('hi')
a()
#preceeding_name
def b():
# print('hi')
pass
b()
Related
I am attempting to write a quick decorator to manage logging returns of various functions. I am not super well versed in decorators so any help you can provide would be very helpful!
from functools import update_wrapper
from typing import Any, Optional
from logging import getLogger
from time import perf_counter
from datetime import datetime
class logger:
def __init__(self, func:callable, response:str = "debug"):
self.logger = getLogger()
self.func = func
self.response = response
update_wrapper(self, func)
def __call__(self, *args, **kwargs):
return getattr(self, self.response)
def debug(self, *args, **kwargs):
self.logger.debug(f"Running {__name__} with id: {id(self)} at {datetime.now()}")
start = perf_counter()
value = self.func(*args, **kwargs)
end = perf_counter()
self.logger.debug(f"""Completed {__name__} with id: {id(self)} at {datetime.now()}.
Total Time to run: {end - start:.6f}s""")
return value
def info(self, *args, **kwargs):
self.logger.info(f"Running {__name__} at {datetime.now()}.")
return self.func(*args, **kwargs)
#logger(response="debug")
def stuff(x):
return x*x
stuff(2)
The error I am receiving is:
TypeError: __init__() missing 1 required positional argument: 'func',
clearly, it doesn't like the required callable and the response requirement. However, I see in all other class-based decorator setups that func needs to be called as part of the __init__ and I have also seen you can pass decorators addition information. What am I doing wrong here?
EDIT:
The purpose of getattr(self, self.response) is so that the function returned by __call__ is either the function along with the debug or info logging. This allows me to utilize the decorator #logging for both logging and debug, yet yields two different results depending on the response value specified in the decorator (i.e #logging(response="info")).
Solution:
class logger:
def __init__(self, response:str = "debug"):
self.logger = getLogger()
self.response = response
def __call__(self, func:callable):
update_wrapper(self, func)
self.func = func
return getattr(self, self.response)
def debug(self, *args, **kwargs):
self.logger.debug(f"Running {self.func.__name__} (type:{type(self.func)}) with id: {id(self)} at {datetime.now()}")
start = perf_counter()
value = self.func(*args, **kwargs)
end = perf_counter()
self.logger.debug(f"""Completed {self.func.__name__} with id: {id(self)} at {datetime.now()}.
Total Time to run: {end - start:.6f}s""")
return value
def info(self, *args, **kwargs):
self.logger.info(f"Running {self.func.__name__} at {datetime.now()}.")
return self.func(*args, **kwargs)
I don't know what your code should do, in particular it is not clear (to me) which kind of arguments should be passed to getattr(self, self.response)(*args, **kwargs). I am saying this to understand the proper workflow of the decorator.
So your code will never work. Here some possible examples of decoration:
the __call__way: #logger(response="debug")
class logger_1:
def __init__(self, response:str = "debug"):
print(response)
def __call__(self, func):
self.func = func
return self # ? depends on what are you doing
def debug(self, *args, **kwargs):
# ...
def info(self, *args, **kwargs):
#...
#logger_1(response="debug")
def stuff(x):
return x*x
A level more of "abstraction": #logger(response="debug").('some_parameter').debug_method
class logger_2:
def __init__(self, response:str = "debug"):
print(response)
def __call__(self, *args, **kwargs):
self.updated_response = getattr(self, self.response)(*args, **kwargs) # just an example
return self
def debug_method(self, func):
self.func = func
# ...
return func
def debug(self, *args, **kwargs):
# ...
def info(self, *args, **kwargs):
#...
#logger_2(response="debug")('some_parameter').debug_method
def stuff(x):
return x*x
NB: logger_2(response="debug").('some_parameter').debug_method is not taking argument because it waits to be "feed" with the target function stuff
These are examples of syntax which constraint the workflow, so you need to be careful when design your decorator
I have created a decorator which I am using to manage logging. I want logging to occur before and after the decorated function runs. The function works fine when interacting with very basic functions, however, when interacting with methods that are a part of other classes, things break. I suspect the issue is a result of there being 2 self arguments. Do you have any idea how to resolve it?
Simplified Decorator Class
class Logger:
def __init__(self, logging_type:str = 'debug'):
self.logging_type = logging_type
def __call__(self, decorated_function:callable):
self.func = decorated_function
return getattr(self, self.logging_type)
def debug(self, *args, **kwargs):
print("starting function")
output = self.func(*args, **kwargs)
print("Completing Function")
return output
We see that the decorator works on basic functions:
#Logger(logging_type="debug")
def simple_function(x):
return x**2
In [2]: simple_function(3)
starting function
Completing Function
Out[2]: 9
However, fails when work with other classes:
class BigClass:
def __init__(self, stuff = 10):
self.stuff = stuff
#Logger(logging_type="debug")
def cool_function(self, input1: int):
return self.stuff + input1
In [16]: test = BigClass()
...: test.cool_function(3)
starting function
It then hits a type error on the output line:
TypeError: cool_function() missing 1 required positional argument: 'input1'
Ideas?
By all means read juanpa.arrivillaga's informative answer. But here is a simpler approach. In writing a class decorator of this type, __call__ should return an ordinary function instead of a member function, like this:
class Logger:
def __init__(self, logging_type:str = 'debug'):
self.logging_function = getattr(self, logging_type)
def __call__(self, decorated_function: callable):
def f(*args, **kwargs):
return self.logging_function(decorated_function, *args, **kwargs)
return f
def debug(self, decorated_function, *args, **kwargs):
print("starting function")
output = decorated_function(*args, **kwargs)
print("Completing Function")
return output
#Logger(logging_type="debug")
def simple_function(x):
return x**2
class BigClass:
def __init__(self, stuff = 10):
self.stuff = stuff
#Logger(logging_type="debug")
def cool_function(self, input1: int):
return self.stuff + input1
print(simple_function(12))
test = BigClass()
print(test.cool_function(3))
OUTPUT:
starting function
Completing Function
144
starting function
Completing Function
13
The problem is that you are decorating your function with a bound-method type, look at type(BigClass.cool_function), you'll see something like: <bound method Logger.debug of <__main__.Logger object at 0x11081f7c0>. Since bound-method objects aren't functions, they don't implement the descriptor protocol to bind the instance as the first argument, hence, the instance is never passed implicitly as the first argument.
The best solution is to avoid class-based decorators to begin with. Here's how you could implement what you are doing using function-based decorators, using the closures to maintain internal state:
from functools import wraps
def logger(*, logging_type): # I prefer keyword-only arugments for decorators, but that is your call...
def decorator(func):
#wraps(func)
def debug(*args, **kwargs):
print("starting function")
result = func(*args, **kwargs)
print("ending function")
return result
#wraps(func)
def another_option(*args, **kwargs):
print("another option")
return func(*args, **kwargs)
options = {"debug": debug, "another_option": another_option}
return options[logging_type]
return decorator
class BigClass:
def __init__(self, stuff = 10):
self.stuff = stuff
#logger(logging_type="debug")
def cool_function(self, input1: int):
return self.stuff + input1
#logger(logging_type="another_option")
def another_function(self):
return self.stuff*100
I'm trying to apply a conditional decorator as described in another stackoverflow post, but I'd like the condition to be set from inside the class its being used. Instead I get a Reference error pointing that self is not defined.
class foo:
def __init__(self):
self.debug = True
#conditional_decorator(decorator, self.debug)
def function(self):
pass
I tried defining a global variable and updating it from inside the __init__() method but it kept its original value when called as an argument of the decorator.
debug = None
class foo:
def __init__(self):
self.debug = True
global debug
debug = self.debug
#conditional_decorator(decorator, debug)
def function(self):
pass
The only way it worked was declaring a global variable and setting it outside of the class.
How can I apply the value of the class property to the decorator?
An update to the answer given by #Maurice Meyer which allows a member of the class to be nominated:
from functools import wraps
def conditional_decorator(decoration, member):
def decorator(method):
predecorated = decoration(method)
#wraps(method)
def wrapper(*args, **kwargs):
self = args[0]
condition = getattr(self, member)
if not condition:
return method(*args, **kwargs)
return predecorated(*args, **kwargs)
return wrapper
return decorator
#And used like this for example:
class foo:
def __init__(self, debug):
self.debug = debug
#conditional_decorator(decorator, "debug")
def function(self):
pass
f1 = foo(True)
f1.function()
This is how you make a decorator handle classes and arguments:
from functools import wraps
def conditional_decorator(param):
def real_decorator(fn):
#wraps(fn)
def wrapper(*args, **kw):
cls = args[0]
print(cls.debug)
print(param)
return wrapper
return real_decorator
class foo:
def __init__(self):
self.debug = True
#conditional_decorator('param1')
def function(self):
pass
f = foo()
f.function()
Output:
True
param1
The decorator should not be conditional. Rather, when the decorated function is called, it should look at self.debug to determine whether to use the original function or the wrapped part.
def conditional_decorator(dec):
def decorator(func):
def _(self, *args, **kwargs):
f = func
if self.debug:
f = dec(f)
return f(self, *args, **kwargs)
return _
return decorator
def decorator(f):
def _(*args, **kwargs):
print("Decorated")
return f(*args, **kwargs)
return _
class foo:
def __init__(self, debug):
self.debug = debug
#conditional_decorator(decorator)
def function(self):
print("foo stuff")
foo(True).function()
print("===")
foo(False).function()
outputs
Decorated
foo stuff
===
foo stuff
After reading the excellent Primer on Python Decorators I thought of implementing some of the fancy (advanced) decorators from the article as classes as an exercise.
So for example the decorator with arguments example
def repeat(num_times):
def decorator_repeat(func):
#functools.wraps(func)
def wrapper_repeat(*args, **kwargs):
for _ in range(num_times):
value = func(*args, **kwargs)
return value
return wrapper_repeat
return decorator_repeat
could be implemented as a class like this
class Repeat:
def __init__(self, times):
self.times = times
def __call__(self, fn):
def _wrapper(*args, **kwargs):
for _ in range(self.times):
result = fn(*args, **kwargs)
return result
return _wrapper
However I seem to be unable to find a class solution for the optional argument decorator example:
def repeat(_func=None, *, num_times=2):
def decorator_repeat(func):
#functools.wraps(func)
def wrapper_repeat(*args, **kwargs):
for _ in range(num_times):
value = func(*args, **kwargs)
return value
return wrapper_repeat
if _func is None:
return decorator_repeat
else:
return decorator_repeat(_func)
Is it just me, or is that one rather wicked? XD
Would love to see a solution!
You can override the __new__ method to achieve the same behavior:
def __new__(cls, _func=None, *, times=2):
obj = super().__new__(cls)
obj.__init__(times)
if _func is None:
return obj
else:
return obj(_func)
so that both:
#Repeat
def a():
print('hi')
and:
#Repeat(times=2)
def a():
print('hi')
output:
hi
hi
Just came across this old question and gave it another try.
I think this is a rather interesting (recursive) solution:
class Repeat:
def __init__(self, fn=None, *, times=2):
self._fn = fn
self._times = times
def _fn_proxy(self, fn):
self._fn = fn
return self
def __call__(self, *args, **kwargs):
if self._fn:
for _ in range(self._times):
result = self._fn(*args, **kwargs)
return result
# assertion: if not self._fn, then args[0] must be the decorated function object
return self._fn_proxy(args[0])
#Repeat
def fun(x,y):
print(f"{x} and {y} and fun!")
#Repeat(times=4)
def more_fun(x,y):
print(f"{x} and {y} and even more fun!")
fun(1,2)
print()
more_fun(3,4)
why this code does not work?
from threading import Timer
def delayed(seconds):
def decorator(f):
def wrapper(*args, **kargs):
Timer(seconds, f, args, kargs)
return wrapper
return decorator
#delayed(1)
def foo():
'''this function does not return'''
print('foo')
foo()
print('dudee')
i except this result:
dudee
foo
i have only
dudee
Because you didn't start your timer try like this:
from threading import Timer
def delayed(seconds):
def decorator(f):
def wrapper(*args, **kargs):
t = Timer(seconds, f, args, kargs)
t.start()
return wrapper
return decorator
#delayed(1)
def foo():
print('foo')
foo()
print('dudee')