Creating a Logging Class Wrapper - python

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

Related

Why do I get "TypeError: wrapper() takes 0 positional arguments but 2 were give" with my decorator

I am try to use a decorator to do some debugging and learn about decorators and I do not understand why I am getting this error:
File "/Users/red/PycharmProjects/general_purpose_object_factory/music.py", line 26, in __call__
self._instance = SpotifyService(access_code)
TypeError: wrapper() takes 0 positional arguments but 2 were given
Process finished with exit code
Here is my code:
decorators.py
def debug_printer(func):
def wrapper():
print("Hello")
func()
return wrapper
music.py
import object_factory
from decorators import debug_printer
class MusicServiceProvider(object_factory.ObjectFactory):
def get(self, service_id, **kwargs):
return self.create(service_id, **kwargs)
class SpotifyService:
#debug_printer
def __init__(self, access_code):
self._access_code = access_code
def test_connection(self):
print(f'Accessing Spotify with {self._access_code}')
class SpotifyServiceBuilder:
def __init__(self):
print(f"{__class__.__name__}")
self._instance = None
def __call__(self, spotify_client_key, spotify_client_secret, **_ignored):
if not self._instance:
access_code = self.authorize(
spotify_client_key, spotify_client_secret)
self._instance = SpotifyService(access_code) # <<< LINE 26. !!!!!!
return self._instance
def authorize(self, key, secret):
return 'SPOTIFY_ACCESS_CODE'
Can someone please explain what I am doing wrong?
ok I had to change my decorator to this ...
def debug_printer(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__qualname__}({args}, {kwargs})")
return func(*args, **kwargs)
return wrapper

How to wrap a decorator around another classes method?

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

Python: Implementation of optional argument decorator as class

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)

How to reference an object instance in callable decorator object with python?

Context:
I'd like to be able to decorate functions so that I can track their stats. Using this post as a reference I went about trying to make my own callable decorator objects.
Here is what I ended up with:
def Stats(fn):
Class StatsObject(object):
def __init__(self, fn):
self.fn = fn
self.stats = {}
def __call__(self, obj, *args, **kwargs):
self.stats['times_called'] = self.stats.get('times_called', 0) + 1
return self.fn(obj, *args, **kwargs)
function = StatsObject(fn)
def wrapper(self, *args **kwargs):
return function(self, *args, **kwargs)
return wrapper
Class MockClass(object):
#Stats
def mock_fn(self, *args, **kwargs):
# do things
Problem:
This actually calls the mock_fn function correctly but I don't have a reference to the stats object outside the wrapper function. i.e. I can't do:
mc = MockClass()
mc.mock_fn()
mc.mock_fn.stats
# HasNoAttribute Exception
Then I tried changing the following code recognizing that it was a scoping issue:
From:
function = StatsObject(fn)
def wrapper(self, *args **kwargs):
return function(self, *args, **kwargs)
return wrapper
To:
function = StatsObject(fn)
return function
But of course I lost the self reference (self becomes the StatsObject instance, obj becomes the first arg, and the MockClass object self reference gets lost).
So I understand why the first issue is happening, but not the second. Is there any way that I can pass the self reference of MockClass to the StatsObject __call__ function?
Functions can actually themselves have attributes in Python.
def Stats(fn):
class StatsObject(object):
def __init__(self, fn):
self.fn = fn
self.stats = {}
def __call__(self, obj, *args, **kwargs):
self.stats['times_called'] = self.stats.get('times_called', 0) + 1
return self.fn(obj, *args, **kwargs)
function = StatsObject(fn)
def wrapper(self, *args **kwargs):
return function(self, *args, **kwargs)
# KEY LINE BELOW: make the StatsObject available outside as "stats_fn"
wrapper.stats_fn = function
return wrapper
class MockClass(object):
#Stats
def mock_fn(self, *args, **kwargs):
# do things
The key line is assigning the StatsObject instance (which you've, perhaps misleadingly, locally named function) as an attribute of the function which you return from the decorator.
Once you do this, self.mock_fn.stats_fn.stats (not self.mock_fn()! The attribute is on the function, not its return value) will work within an instance of MockClass, and MockClass.mock_fn.stats_fn.stats will be available outside. The statistics will be global across all instances of MockClass (since the decorator is called once, not once per instance), which may or may not be what you want.

Programmatically generate methods for a class

I have about 20 methods to redirect to a wrapper method that takes the original method, and the rest of the arguments:
class my_socket(parent):
def _in(self, method, *args, **kwargs):
# do funky stuff
def recv(self, *args, **kwargs):
return self._in(super().recv, *args, **kwargs)
def recv_into(self, *args, **kwargs):
return self._in(super().recv_into, *args, **kwargs)
# and so on...
How can I add more of these methods programmatically? This is about as far as I get before everything starts to look wrong:
for method in 'recv', 'recvfrom', 'recvfrom_into', 'recv_into', ...:
setattr(my_socket, method, ???)
Can I do this by assigning within the class definition, or something else that feels more natural?
class my_socket(parent):
def makes_recv_methods(name):
# wraps call to name
def recv_meh = makes_recv_methods('recv_meh')
I'd prefer to use __get__ and friends when possible over magic functions from types.
I'd do it by running some code to generate the methods from a list after the class is defined - you could put this into a decorator.
import functools
def wrap_method(cls, name):
# This unbound method will be pulled from the superclass.
wrapped = getattr(cls, name)
#functools.wraps(wrapped)
def wrapper(self, *args, **kwargs):
return self._in(wrapped.__get__(self, cls), *args, **kwargs)
return wrapper
def wrap_methods(cls):
for name in cls.WRAP_ATTRS:
setattr(cls, name, wrap_method(cls, name))
return cls
#wrap_methods
class my_socket(parent_class):
WRAP_ATTRS = ['recv', 'recvfrom'] # ... + more method names
def _in(self, method, *args, **kwargs):
# do funky stuff
wilberforce proposal works, but there is a simpler way using OOP only:
def wrap_method(wrapped):
#functools.wraps(wrapped)
def wrapper(self, *args, **kwargs):
return self._in(wrapped.__get__(self, cls), *args, **kwargs)
return wrapper
class Parent:
def _in(self, method, *args, **kwargs):
return method(*args, **kwargs)
#wrap_method
def recv(self, *args, **kwargs):
return # whatever
#wrap_method
def recv_into(self, *args, **kwargs):
return # whatever
class MySocket(Parent):
def _in(self, method, *args, **kwargs):
# do funky stuff
I'd like to expand on the accepted answer. I wanted to potentially have a very long list of decorator methods applied to a very long list of methods.
import functools
def wrap_method(cls, name, wrapper_method_name):
# This unbound method will be pulled from the superclass.
wrapped = getattr(cls, name, wrapper_method_name)
#functools.wraps(wrapped)
def wrapper(self, *args, **kwargs):
wrapper_method = getattr(self, wrapper_method_name)
return wrapper_method(wrapped.__get__(self, cls), *args, **kwargs)
return wrapper
def wrap_methods(cls):
for wrapper_method_name in cls.WRAPPER_METHOD_NAMES:
for name in cls.WRAPPED_METHODS:
setattr(cls, name, wrap_method(cls, name, wrapper_method_name))
return cls
And here is the class that wraps the original
#wrap_methods
class WrappedConnection(BaseConnection):
"""
This class adds some quality-of-life improvements to the BaseConnection class.
-WRAPPED_METHODS are wrapped by WRAPPER_METHOD_NAMES
-wrappers can be toggled on and off.
example:
connection = WrappedConnection(show_messages=True, log_errors=False, keep_authenticated=False)
default:
connection = WrappedConnection(show_messages=False, log_errors=True, keep_authenticated=True)
"""
WRAPPER_METHOD_NAMES = ['log_errors', 'keep_authenticated', 'show_messages']
WRAPPED_METHODS = ['a_method', 'b_method', 'c_method', 'd_method']
MESSAGE_OVERRIDE_MAP = {"a_method": "a_method_message_override_attribute",
"b_method": "b_method_message_override_attribute"}
def keep_authenticated(self, method, *args, **kwargs):
"""
If the session has expired, the session is re-authenticated. The incident is logged by the default logger.
This option can be turned off by setting keep_authenticated during initialization of a WrappedConnection object.
- connection = WrappedConnection(keep_authenticated=False) # why would you ever do this
:param method: (method) method to be wrapped
:param args: (args) passed args
:param kwargs: (kwargs) passed kwargs
:return: (method) method wrapped by #keep_authenticated
"""
response, expired_session = method(*args, **kwargs), None
if response["errors"] and self._keep_authenticated:
expired_session = list(filter(lambda x: 'expired session' in x, response["errors"]))
if expired_session:
self.__init__()
logging.info('Session has been re-authenticated.')
response = method(*args, **kwargs)
return response
def log_errors(self, method, *args, **kwargs):
"""
If there is an error the incident is logged. This option can be turned off by setting log_errors
during initialization of a WrappedConnection object.
- connection = WrappedConnection(log_errors=False)
:param method: (method) method to be wrapped
:param args: (args) passed args
:param kwargs: (kwargs) passed kwargs
:return: (method) method wrapped by #log_errors
"""
response = method(*args, **kwargs)
if response["errors"] and self._log_errors:
errors = response["errors"]
logging.error(errors)
return response
def show_messages(self, method, *args, **kwargs):
"""
Shows the xml that is sent during the request. This option can be turned on by setting show_messages during
initialization of a WrappedConnection object.
- connection = WrappedConnection(show_messages=True)
:param method: (method) method to be wrapped
:param args: (args) passed args
:param kwargs: (kwargs) passed kwargs
:return: (method) method wrapped by #show_messages
"""
response = method(*args, **kwargs)
if self._show_messages:
message_override_attr = WrappedConnection.MESSAGE_OVERRIDE_MAP.get(method.__name__)
if message_override_attr:
message_override = getattr(self, message_override_attr)
print(BeautifulSoup(message_override, "xml").prettify())
else:
self._show_message(method.__name__, *args, **kwargs)
return response
def __init__(self, *args, keep_authenticated=True, log_errors=True, show_messages=False, **kwargs):
super(WrappedConnection, self).__init__(*args, **kwargs)
self._keep_authenticated = keep_authenticated
self._log_errors = log_errors
self._show_messages = show_messages
You could use cog.
class MySocket(Parent):
"""[[[cog
import cog
l = ['in','out']
for item in l:
cog.outl("def _{0}(self, method, *args, **kwargs):".format(item))
]]]"""
#[[[end]]]
This has the added advantages of easily being updated, not touching your code outside of the end comment, and you can twiddle the generated code if necessary.
I've successfully used cog for generating boilerplate on another project, mixed in with the non generated code. It started out reading an input file of instructions into a dictionary. Then for each section of boilerplate it used that piece of the dictionary to know what to write.
I edit the instruction file in one spot, instead of twenty different places in the boilerplate.

Categories

Resources