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.
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 would like to create two objects such that methods for some of this objects will be repeated for other. Then I try to do this:
def backup(method):
def wrapper(self, *args, **kwargs):
method(self, *args, **kwargs)
getattr(self.another_tester, method.__name__)(*args, **kwargs)
return wrapper
class Tester():
def __init__(self, name):
self.name = name
def select_backup(self, tester):
self.another_tester = tester
#backup
def foo1(self, stop=False):
print("I am tester {}, do method foo1".format(self.name))
#backup
def foo2(self, stop=False):
print("I am tester {}, do method foo2".format(self.name))
#backup
def foo3(self, stop=False):
print("I am tester {}, do method foo3".format(self.name))
tester1 = Tester("A")
tester2 = Tester("B")
tester1.select_backup(tester2)
tester2.select_backup(tester1)
tester1.foo1()
tester2.foo2()
tester1.foo3()
I get RuntimeError: maximum recursion depth exceeded while calling a Python object for such code. class Tester have a lot of different methods (foo1, foo2, foo3, ...), and I would like to backup (repeat) each method. So I use decorator.
I can change decorator:
def backup(method):
def wrapper(self, *args, **kwargs):
method(self, *args, **kwargs)
try:
kwargs["stop"]
except KeyError:
getattr(self.another_tester, method.__name__)(stop=True, *args, **kwargs)
return wrapper
It's work, but I think there is more pythonic way to do it. Can anyone offer such way?
If your pair of objects are always instances of the same class (or at least, never have different implementations of the decorated method), you can change the decorator so that it directly calls the original method on the other instance, rather than getting the decorated version via getattr:
def backup(method):
def wrapper(self, *args, **kwargs):
method(self, *args, **kwargs)
method(self.another_tester, *args, **kwargs)
return wrapper
If you want to be able to support pairs of objects of different classes (and different method implementations), things need to be a bit more complicated. You can save a reference to the original undecorated method as an attribute on the wrapper function, then look it up when you need it:
def backup(method):
def wrapper(self, *args, **kwargs):
method(self, *args, **kwargs)
other_method = getattr(self.another_tester, method.__name__).orig_method
other_method(self.another_tester, *args, **kwargs)
wrapper.orig_method = method
return wrapper
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.
I want to cache certain methods of a class - but only if a read_only flag is set on the instance of the class.
So, in the object below, I want the get() method to be cacheable, but only if self.readonly is True.
I can't conditionally use the decorator, because it's set when the class is defined, not when it's instantiated.
from beaker.cache import cache_regions, cache_region
cache_regions.update({
'long_term':{
'expire':86400,
'type':'dbm',
'data_dir':'/tmp',
'key_length': 32,
}
})
class Foo(object):
def __init__(self, read_only=True):
self.read_only = read_only
#cache_region('long_term')
def get(self, arg):
return arg + 1
You can use a decorator to call the right (cached or not) function checking the desired attribute:
def conditional(decorator):
def conditional_decorator(fn):
dec = decorator(fn)
def wrapper(self, *args, **kw):
if self.read_only:
return dec(self, *args, **kw)
return fn(self, *args, **kw)
return wrapper
return conditional_decorator
Use like this:
#conditional(cache_region('long_term'))
def get(self, arg):
return arg + 1
I have a decorator declared as a class:
class predicated(object):
def __init__(self, fn):
self.fn = fn
self.fpred = lambda *args, **kwargs: True
def predicate(self, predicate):
self.fpred = predicate
return self
def validate(self, *args, **kwargs):
return self.fpred(*args, **kwargs)
def __call__(self, *args, **kwargs):
if not self.validate(*args, **kwargs):
raise PredicateNotMatchedError("predicate was not matched")
return self.fn(*args, **kwargs)
... and when I use it to wrap a method in a class, calling that method does not seem to set the instance of the object as the first argument. While this behavior is not exactly unexpected, how would I go about getting self to be frozen when the method becomes an instance method?
Simplified example:
class test_decorator(object):
def __init__(self, fn):
self.fn = fn
def __call__(self, *args, **kwargs):
return self.fn(*args, **kwargs)
class Foo(object):
#test_decorator
def some_method(self):
print(self)
Foo().some_method()
Expected instance of foo, instead get an error saying 0 arguments were passed.
Figured it out - needed to define a __get__ method in order to create a MethodType binding like so:
def __get__(self, obj, objtype=None):
return MethodType(self, obj, objtype)
which creates a MethodType object when invoking the method on an object that freezes the self argument.