This is for some quantum mechanics software so there will be references to molecules/atoms etc but the core issue is a python logging one, unrelated to all that.
I have a couple of logging decorators, one for logging what the program is doing, another to log exceptions (along with their full stack trace and some class object details to help with debugging).
To save me decorating every single method, I also have a class decorator which just applies a decorator to all methods of a class. This is:
def for_all_methods(decorator):
"""
Applies a decorator to all methods of a class (includes sub-classes and init; it is literally all callables).
This class decorator is applied using '@for_all_methods(timer_func)' for example.
"""
@wraps(decorator)
def decorate(cls):
# Examine all class attributes.
for attr in cls.__dict__:
# Check if each class attribute is a callable method.
if callable(getattr(cls, attr)):
# Set the callables to be decorated.
setattr(cls, attr, decorator(getattr(cls, attr)))
return cls
return decorate
The logging decorators are similar, the following is used to log method timings, while also giving the docstring, qualname and some other info:
def log_dec_factory(file_name):
"""
Logs the various timings of a function in a dated and numbered file.
Writes the start time, function / method qualname and docstring when function / method starts.
Then outputs the runtime and time when function / method finishes.
"""
def timer_logger(orig_func):
@wraps(orig_func)
def wrapper(*args, **kwargs):
start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
t1 = time()
with open(file_name, 'a+') as log_file:
log_file.write(f'{orig_func.__qualname__} began at {start_time}.\n\n')
log_file.write(f'Docstring for {orig_func.__qualname__}:\n {orig_func.__doc__}\n\n')
time_taken = time() - t1
mins, secs = divmod(time_taken, 60)
hours, mins = divmod(mins, 60)
secs, remain = str(float(secs)).split('.')
time_taken = f'{int(hours):02d}h:{int(mins):02d}m:{int(secs):02d}s.{remain[:5]}'
end_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
log_file.write(f'{orig_func.__qualname__} finished in {time_taken} at {end_time}.\n\n')
# Add some separation space between function / method logs.
log_file.write(f'{"-" * 50}\n\n')
return orig_func(*args, **kwargs)
return wrapper
return timer_logger
So, when I want to get some log info for a particular method of a class, I decorate the class with for_all_methods()
which takes the function / method decorator as an argument. The function / method decorator then takes the argument of the log file name, like so:
@for_all_methods(log_dec_factory('log_file.txt'))
class Engine:
def __init__(self, molecule):
self.molecule = molecule
self.name = molecule.name
self.log_file = molecule.log_file
def print_name_to_terminal(self):
print(self.name)
...
So now, whenever a method from the class Engine
is invoked, the details of the invocation will be written to log_file.txt
. Great!
To be clear, the molecule argument is a class which is getting passed around and stores all important information (atoms, coordinates, etc).
This is completely fine, except rather than providing a string for the log file name, I just want to get that from the molecule argument. In other words, I want to pass self.log_file
into the decorator.
Now, I can do this with a quick and dirty global in the undecorated base class:
class Engine:
def __init__(self, molecule):
self.molecule = molecule
self.name = molecule.name
self.log_file = molecule.log_file
log_file = self.log_file
global log_file
...
@for_all_methods(log_dec_factory(log_file))
class PSI4(Engine):
def __init__(self, molecule):
super().__init__(molecule)
...
Now, this works but, it feels like a hacky workaround. (I've literally never needed to use a global in Python, there's always been a better / proper way.) This also only works if I have a base class which I don't want to log the methods for. Otherwise I have to go back to manually putting in the log file name.
So finally, my question is, is there a proper way of doing something like this? I use the logging module for my exception logger decorator but couldn't find what I wanted for this particular use case.
Many thanks in advance and apologies for the rather long post.