4
class Reader:
    def __init__(self):
        pass

    def fetch_page(self):
        with open('/dev/blockingdevice/mypage.txt') as f:
            return f.read()

    def fetch_another_page(self):
        with open('/dev/blockingdevice/another_mypage.txt') as f:
            return f.read()   

class Wrapper(Reader):
    def __init__(self):
        super().__init__()

    def sanity_check(func):
        def wrapper():
            txt = func()
            if 'banned_word' in txt:
                raise Exception('Device has banned word on it!')
        return wrapper

    @sanity_check
    <how to automatically put this decorator on each function of base class? >

w = Wrapper()
w.fetch_page()
w.fetch_another_page()

How can I make sure that sanity_check's wrapper was run automatically when calling fetch_page and fetch_another_page on an instance of the Wrapper class?

Blckknght
  • 100,903
  • 11
  • 120
  • 169
hasanatkazmi
  • 8,041
  • 2
  • 22
  • 18
  • I just edited the question to use more meaningful terminology for what you're asking about. Context managers had nothing to do with your question, except in a minor way (you're using files as context managers in `Reader`). I think you wanted the term `decorator` instead. If I've changed too much and the question doesn't match what you want to know, please [edit] the question yourself to correct my misunderstandings. – Blckknght Mar 06 '19 at 05:51

3 Answers3

4

If using python3.6 or above, you can accomplish this using __init_subclass__

Simple implementation: (for the real thing you probably want a registry and functools.wraps, etc):

class Reader:
    def __init_subclass__(cls):
        cls.fetch_page = cls.sanity_check(cls.fetch_page)
        cls.fetch_another_page = cls.sanity_check(cls.fetch_another_page)

    def fetch_page(self):
        return 'banned_word'

    def fetch_another_page(self):
        return 'not a banned word'

class Wrapper(Reader):
    def sanity_check(func):
        def wrapper(*args, **kw):
            txt = func(*args, **kw)
            if 'banned_word' in txt:
                raise Exception('Device has banned word on it!')
            return txt
        return wrapper


Demo:

In [55]: w = Wrapper()

In [56]: w.fetch_another_page()
Out[56]: 'not a banned word'

In [57]: w.fetch_page()
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-57-4bb80bcb068e> in <module>()
----> 1 w.fetch_page()
...

Exception: Device has banned word on it!

Edit:In case you can't change the baseclass, you can subclass and create an Adapter class:

class Reader:

    def fetch_page(self):
        return 'banned_word'

    def fetch_another_page(self):
        return 'not a banned word'

class ReadAdapter(Reader):
    def __init_subclass__(cls):
        cls.fetch_page = cls.sanity_check(cls.fetch_page)
        cls.fetch_another_page = cls.sanity_check(cls.fetch_another_page)

class Wrapper(ReadAdapter):
    def sanity_check(func):
        def wrapper(*args, **kw):
            txt = func(*args, **kw)
            if 'banned_word' in txt:
                raise Exception('Device has banned word on it!')
            return txt
        return wrapper

Should provide the same result.

salparadise
  • 5,699
  • 1
  • 26
  • 32
  • Looks like a great solution. The only problem is, it requires that we can edit the base class. It will be hard if base class is in some external library. – hasanatkazmi Mar 07 '19 at 02:10
  • @hasanatkazmi you can subclass into an adapter class. Added another example. – salparadise Mar 07 '19 at 05:48
1

There's no easy way to do what you want from within the Wrapper subclass. You either need to name each method of the base class that you want to wrap with a decorator, modify the Wrapper class after you create it (perhaps with a class decorator), or you need to redesign the base class to help you out.

One relatively simple redesign would be for the base class methods to be decorated with a decorator that makes them always call a "validator" method. In the base class the validator can be a no-op, but a child class could override it to do whatever you want:

class Base:
    def sanity_check(func):
        def wrapper(self, *args, **kwargs):
            return self.validator(func(self, *args, **kwargs))
        return wrapper

    def validator(self, results):   # this validator accepts everything
        return results

    @sanity_check
    def foo(self):
        return "foo"

    @sanity_check
    def bar(self):
        return "bar"

class Derived(Base):
    def validator(self, results):   # this one doesn't like "bar"
        if results == "bar":
            raise Exception("I don't like bar")
        return results

obj = Derived()
obj.foo() # works
obj.bar() # fails to validate
Blckknght
  • 100,903
  • 11
  • 120
  • 169
0

Here is my solution for this:

class SubClass(Base):
    def __init__(self, *args, **argv):
        super().__init__(*args, **argv)

        for attr_name in Base.__dict__:
            attr = getattr(self, attr_name)
            if callable(attr):
                setattr(self, attr_name, functools.partial(__class__.sanity_check, attr))

    @classmethod
    def sanity_check(func):
        txt = func()
        if 'banned_word' in txt:
            raise Exception('Device has banned word on it!')
        return txt

This will only work if you want to process each function in your Base with sanity_check.

hasanatkazmi
  • 8,041
  • 2
  • 22
  • 18