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.
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:
Set the default as an instance attribute (the solution you arrived at)
Add the extra parameter to the lambda, so _condition = lambda _self, _i: True
Make the method static: _condition = staticmethod(lambda _i: True)