0

I have a simple class in which I want to generate methods based on inherited class fields:

class Parent:
    def __init__(self, *args, **kwargs):
        self.fields = getattr(self, 'TOGGLEABLE')
        self.generate_methods()

    def _toggle(self, instance):
        print(self, instance)  # Prints correctly

        # Here I need to have the caller method, which is:
        # toggle_following()

    def generate_methods(self):
        for field_name in self.fields:
             setattr(self, f'toggle_{field_name}', self._toggle)


class Child(Parent):
    following = ['a', 'b', 'c']

    TOGGLEABLE = ('following',)

At this moment, there is a toggle_following function successfully generated in Child.

Then I invoke it with a single parameter:

>>> child = Child()
>>> child.toggle_following('b')
<__main__.Child object at 0x104d21b70> b

And it prints out the expected result in the print statement.

But I need to receive the caller name toggle_following in my generic _toggle function.

I've tried using the inspect module, but it seems that it has a different purpose regarding function inspection.

wencakisa
  • 5,850
  • 2
  • 15
  • 36
  • post your `toggle_following` declaration and describe its purpose – RomanPerekhrest Jun 27 '19 at 10:42
  • 1
    @RomanPerekhrest I think this is a great example of a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). the actual toggle functionality is not relevant to the question, and would just complicate things. – Adam.Er8 Jun 27 '19 at 10:48

2 Answers2

2

maybe this is too hackish, and maybe there's a more elegant (i.e. dedicated) way to achieve this, but:

you can create a wrapper function that passes the func_name to the inner _toggle func:

class Parent:
    def __init__(self, *args, **kwargs):
        self.fields = getattr(self, 'TOGGLEABLE')
        self.generate_methods()

    def _toggle(self, instance, func_name):
        print(self, instance)  # Prints correctly
        print(func_name)

    def generate_methods(self):
        for field_name in self.fields:
            func_name = 'toggle_{}'.format(field_name)  # I don't have python 3.7 on this computer :P
            setattr(self, func_name, lambda instance: self._toggle(instance, func_name))


class Child(Parent):
    following = ['a', 'b', 'c']

    TOGGLEABLE = ('following',)


child = Child()
child.toggle_following('b')

Output:

<__main__.Child object at 0x7fbe566c0748> b
toggle_following
Adam.Er8
  • 12,675
  • 3
  • 26
  • 38
1

Another approach to solve the same problem would be to use __getattr__ in the following way:

class Parent:
    def _toggle(self, instance, func_name):
        print(self, instance)  # Prints correctly
        print(func_name)

    def __getattr__(self, attr):

        if not attr.startswith("toggle_"):
            raise AttributeError("Attribute {} not found".format(attr))

        tmp = attr.replace("toggle_", "")
        if tmp not in self.TOGGLEABLE:
            raise AttributeError(
                "You cannot toggle the untoggleable {}".format(attr)
            )

        return lambda x: self._toggle(x, attr)


class Child(Parent):
    following = ['a', 'b', 'c']
    TOGGLEABLE = ('following',)


child = Child()
# This can toggle
child.toggle_following('b')

# This cannot toggle
child.toggle_something('b')

which yields:

(<__main__.Child instance at 0x107a0e290>, 'b')
toggle_following

Traceback (most recent call last):
  File "/Users/urban/tmp/test.py", line 26, in <module>
    child.toggle_something('b')
  File "/Users/urban/tmp/test.py", line 13, in __getattr__
    raise AttributeError("You cannot toggle the untoggleable {}".format(attr))
AttributeError: You cannot toggle the untoggleable toggle_something
urban
  • 5,392
  • 3
  • 19
  • 45