4

I have had a situation arise where most methods of a class need to raise an exception if called except for one, if a certain condition is False. It would be possible to go into most methods and write if not condition such that each method will raise the exception if the condition is not true, but I think it is probably possible to do this somehow with a single decorator on the top of the class.

This question is similar but it involves decorating every method separately, and if I were going to do that, I might as well just put the if statement into each method.

Here is some code and comments to help communicate it:

CONDITION = True  # change to False to test

def CheckMethods():
    if CONDITION:
        # run all the methods as usual if they are called
        pass 
    else:
        # raise an exception for any method which is called except 'cow'
        # if 'cow' method is called, run it as usual
        pass


@CheckMethods
class AnimalCalls:
    def dog(self):
        print("woof")
    def cat(self):
        print("miaow")
    def cow(self):
        print("moo")
    def sheep(self)
        print("baa") 

a = AnimalCalls()
a.dog()
a.cat()
a.cow()
a.sheep()

Does anyone know how to do this? Have never decorated a class before or tried to check its methods like this.

cardamom
  • 6,873
  • 11
  • 48
  • 102
  • 1
    Are you sure that you want a decorator, not a proxy? – Kijewski Jul 12 '19 at 14:52
  • Am not sure what that is. As long as it checks a condition then makes most of the methods raise an exception if it is not true.. – cardamom Jul 12 '19 at 14:58
  • @cardamom: The condition is supposed to be checked separately at every call, right? Your pseudocode doesn’t do that. – Davis Herring Jul 12 '19 at 15:15
  • The intent @DavisHerring was more at every _instantiation_, that at lease fits with the way the rest of the code works which inspired it – cardamom Jul 12 '19 at 15:17
  • @cardamom, are you intend to switch "condition" dynamically? is there a need to recover original methods that were being closed previously? – RomanPerekhrest Jul 12 '19 at 15:21
  • @cardamom: So the condition can’t change after construction of any given object? (That’s somewhat unusual in Python.) – Davis Herring Jul 12 '19 at 15:24
  • @RomanPerekhrest the original thought was not for any dynamic checking, may need that one day, but at the moment not. It is enough to let the class check once when it is being initialised. The program containing it may reinitialise it as needed. – cardamom Jul 12 '19 at 15:24
  • It almost sounds like you want two different classes, one with just the one method (`cow`). – Davis Herring Jul 12 '19 at 15:31
  • Have found in the mean time another [relevant question](https://stackoverflow.com/questions/14095616/python-can-i-programmatically-decorate-class-methods-from-a-class-instance/) here. Am testing the solutions posted. – cardamom Jul 12 '19 at 15:33
  • @cardamom, Note that proposed proxies will trigger on attribute access `a.dog` (as a 1st phase) not only on direct method call `a.dog()` – RomanPerekhrest Jul 12 '19 at 15:39
  • @RomanPerekhrest intereesting observation.. You're right just tested that. Not sure if it matters in my application but it would be nicer if it would wait till it were actually called. – cardamom Jul 12 '19 at 15:41

3 Answers3

5

Implementing a proxy is as simple as that

class Proxy:
    def __init__(self, inst):
        self.__inst = inst

    def __getattr__(self, name):
        return getattr(self.__inst, name)

Instead of obj = SomeClass() you'd use obj = Proxy(SomeClass()). All accesses to obj.attribute get intercepted by Proxy.__getattr__. That's the method you may add more logic to, e.g.:

class MethodChecker:
    def __init__(self, inst, check):
        self.__inst = inst
        self.__check = check

    def __getattr__(self, name):
        self.__check()
        return getattr(self.__inst, name)
Kijewski
  • 25,517
  • 12
  • 101
  • 143
2

Largely adapting the code found here:

condition = False

def CheckMethods(Cls):
    class NewCls(object):
        def __init__(self,*args,**kwargs):
            self.oInstance = Cls(*args,**kwargs)
        def __getattribute__(self,s):
            try:    
                x = super(NewCls,self).__getattribute__(s)
            except AttributeError:      
                pass
            else:
                return x
            x = self.oInstance.__getattribute__(s)
            if condition:
                return x
            else:
                if s == 'cow':
                    return x
                else:
                    raise ValueError('Condition not true')
    return NewCls

@CheckMethods
class AnimalCalls(object):
    def dog(self):
        print("woof")
    def cat(self):
        print("miaow")
    def cow(self):
        print("moo")
    def sheep(self):
        print("baa") 

oF = AnimalCalls()

Result:

contition = False; of.moo() -> 'moo'
contition = True; of.moo() -> 'moo'
condition = False; of.dog() -> 'ValueError: Condition not true'
condition = True; of.dog() -> 'woof'
TomNash
  • 3,147
  • 2
  • 21
  • 57
  • Note that the exception is raised when `dog` is looked up, not when it is called. Among other effects, this means the arguments to the call are not evaluated. – Davis Herring Jul 12 '19 at 15:23
  • I tested this and it basically works and is a decorator, answers the question.. That other behaviour, that it raises an exception when it is looked up, not when it is called, can possibly be fixed with some methods from `traceback` as per [this answer](https://stackoverflow.com/a/7900380/4288043) – cardamom Jul 12 '19 at 16:12
2

The proxy would be my pick, but here is a decorator as requested.

I added a test to exclude any methods starting with an underscore. You might want to include _internal methods, but take care not to mess with any special __dunder__ methods.

# cond = lambda attr: True  # full access
cond = lambda attr: attr == 'cow'

def methodcheck(cls):
    def cond_getattribute(self, name):
        if name.startswith('_') or cond(name):
            return saved_gettattribute(self, name)
        raise AttributeError("access forbidden")
    saved_gettattribute = cls.__getattribute__
    cls.__getattribute__ = cond_getattribute
    return cls 

@methodcheck
class AnimalCalls:
    def dog(self):
        print("woof")
    def cat(self):
        print("miaow")
    def cow(self):
        print("moo")
    def sheep(self):
        print("baa"
VPfB
  • 14,927
  • 6
  • 41
  • 75
  • Thanks @VPfB, that is an amazing and compact piece of code, with the recursion.. Still trying to fully understand it. Have noticed the inner function gets executed 6 times. I thought that was some multiple of the class methods, or allowed class methods, but it does not seem to vary from 6 even if I delete some methods. Do you notice the same thing? – cardamom Jul 16 '19 at 14:13
  • @cardamom I'd like to answer your comment, but i do not understand it fully. My code is without recursion. The decorator installs a new `__getattribute__` which is responsible for attribute lookup, but other than that does not change the original class. The new function either calls the old one, i.e. the regular `__getattribute__` or it raises an error - depending on a condition based on the attribute name. You should see one call for each attr lookup (except some very special cases). Docs: https://docs.python.org/3/reference/datamodel.html?highlight=__getattribute__#object.__getattribute__ – VPfB Jul 16 '19 at 19:49
  • I understand it now. The behaviour I mentioned above with the 6 times was some quirk of running it in Jupyter notebook, I could not replicate it in the standard python interpretor in the terminal. – cardamom Jul 25 '19 at 11:06