0

I start my question by describing the use case:

A context-menu should be populated with actions. Depending on the item for which the menu is requested, some actions should be hidden because they are not allowed for that specific item.

So my idea is to create actions like this:

edit_action = Action("Edit Item")
edit_action.set_condition(lambda item: item.editable)

Then, when the context-menu is about to be opened, I evaluate every possible action whether it is allowed for the specific item or not:

allowed_actions = list(filter(
    lambda action: action.allowed_for_item(item),
    list_of_all_actions
))

For that plan to work, I have to store the condition in the specific Action instance so that it can be evaluated later. Obviously, the condition will be different for every instance.

The basic idea is that the one who defines the actions also defines the conditions under which they are allowed or not. I want to use the same way to enable/disable toolbar buttons depending on the item selected.

So that is how I tried to implement Action (leaving out unrelated parts):

class Action:
    _condition = lambda i: True
    def set_condition(self, cond):
        self._condition = cond

    def allowed_for_item(self, item):
        return self._condition(item)

My problem is now:

TypeError('<lambda>() takes 1 positional argument but 2 were given')

Python treats self._condition(item) as call of an instance method and passes self as the first argument.

Any ideas how I can make that call work? Or is the whole construct too complicated and there is a simpler way that I just don't see? Thanks in advance!


Update: I included the initializer for _condition, which I found (thanks @slothrop) to be the problem. This was meant as default value, so allowed_for_item() also works when set_condition() has not been called before.

chrset
  • 581
  • 6
  • 11
  • 1
    I can't reproduce this. e.g. `fn = lambda x: x<5; a = Action(); a.set_condition(fn); print(a.allowed_for_item(7))` prints `False` as intended. – slothrop May 26 '23 at 14:21
  • It would be good to see a reproducible example with a full stack trace, i.e. you can leave out the unrelated parts, but show enough code that we can run it and reproduce the error. – slothrop May 26 '23 at 14:28
  • The proposed solution seems needlessly overcomplicated. All you need is a method that takes the conditions as parameters and then builds an appropriate menu based on that. The individual actions shouldn't need to know anything about the context they are being used in. – ekhumoro May 26 '23 at 16:16
  • You are right @slothrop, the example above is working because I left out an "unrelated part" (so I thought): the initializer of ```Action._condition```. I wrote: ```class Action: _condition = lambda i: True``` as a default value for all actions for which ```set_condition()``` is never called. Turns out this is the problem. If I move this assigment to the ```__init__``` constructor, everything works. I'm not sure why this makes a difference, but thanks for pointing me in the right direction! – chrset May 27 '23 at 04:48
  • 1
    @chrset Interesting, that explains it. So the additional "self" parameter is inserted into calls in the case when *"a name resolves to a class attribute and this attribute has a _ _get_ _ method"* (see https://wiki.python.org/moin/FromFunctionToMethod). That's true for a function defined at class level (regardless of whether it uses `def` or `lambda`) but not true for a function as instance attribute - which is why it works fine once you assign the default to the instance within the constructor. – slothrop May 27 '23 at 08:05
  • Related: https://stackoverflow.com/questions/27244073/how-to-use-lambda-as-method-within-a-class – slothrop May 27 '23 at 08:16
  • 1
    @slothrop, I updated my post to include the problematic initialization - do you want to write an answer so I can mark it as the solution? – chrset May 27 '23 at 08:50

1 Answers1

1

Setting the class attribute _condition to a function (whether through a lambda or a def) makes that function into a method: i.e. when accessed as an instance attribute, the instance is inserted as the first argument to the function call.

So this:

class Action:
    _condition = lambda i: True

does the same as this:

class Action:
    def _condition(i):
        return True

While these two are equivalent, the def version is more familiar, so the problem with it (lack of self in the signature) is more obvious.

The underlying mechanism for this is summarised on the Python wiki:

The descriptor protocol specifies that during an attribute lookup, if a name resolves to a class attribute and this attribute has a __get__ method, then this __get__ method is called. The argument list to this call includes either:

the instance and the class itself, or

None and the class itself

Possible solutions are:

  1. Set the default as an instance attribute (the solution you arrived at)

  2. Add the extra parameter to the lambda, so _condition = lambda _self, _i: True

  3. Make the method static: _condition = staticmethod(lambda _i: True)

slothrop
  • 3,218
  • 1
  • 18
  • 11
  • For completeness, this is how I solved the problem: ```class Action: def __init__(self): self._condition = lambda i: True``` – chrset May 27 '23 at 10:48