Repository: kivy/kivy

Issue:
KEP003: Syntax to clear previous kv rules of a property
Many times one creates e.g. a dynamic class with some properties and their rules which is than used repeatedly in many places. E.g. a simple button with a label and a icon.

Although most of the class instances conform to the rules defined in the dynamic class, sometimes, you want to overwrite the rules for a specific property just in one or two places. This can be hard to impossible  to do currently.

One example:

```
<SwitchIcon@ToggleButtonBehavior+Widget>:
    disabled: app.running
    color: app.that_color
```

Then later in one instance we want to do:

```
SwitchIcon:
    disabled: not app.running
    color: (1, 1, 1, 1)
```

The `disabled` property will now have two rules, with the `disabled: not app.running` executed _after_ the `disabled: app.running` rule, which is why most people don't realize that the second rule definition doesn't overwrite, but just adds a second rule to the disabled property.

For the `color` property, this just won't work. Yes, this `color` will be set to `(1, 1, 1, 1)`, but as soon as `app.that_color` changes, because `color` is still bound to that rule, `color: (1, 1, 1, 1)` will be overwritten.

A solution is to allow clearing the rules for a specific property with a dash, e.g.

```
SwitchIcon:
    -disabled: not app.running
    -color: (1, 1, 1, 1)
```



Discussion:
Well, sounds good to me in principle, in some ways it's a small thing but I think it's also a fairly small solution and it makes things much neater.

Is there a situation where you would want multiple bindings that will set a property? Should clearing previous bindings just be the normal behavior?

@kived Yes, there are many instances where you would want multiple bindings, so I think it should be enabled by default. Imagine if you want to update a label from multiple sources.


Resolving patch:
diff --git a/kivy/lang.py b/kivy/lang.py
--- a/kivy/lang.py
+++ b/kivy/lang.py
@@ -648,6 +648,43 @@ class MyWidget(Label):
 MyWidget will now have a Color and Rectangle instruction in its canvas
 without any of the instructions inherited from the Label.
 
+Redefining a widget's property style
+------------------------------------
+
+Similar to :ref:`Redefining a widget's style`, sometimes we would like to
+inherit from a widget, keep all its KV defined styles, except for the style
+applied to a specific property. For example, we would
+like to inherit from a :class:`~kivy.uix.button.Button`, but we would also
+like to set our own `state_image`, rather then relying on the
+`background_normal` and `background_down` values. We can achieve this by
+prepending a dash (-) before the `state_image` property name in the .kv style
+definition.
+
+In myapp.py:
+
+.. code-block:: python
+
+    class MyWidget(Button):
+
+        new_background = StringProperty('my_background.png')
+
+and in my.kv:
+
+.. code-block:: kv
+
+    <MyWidget>:
+        -state_image: self.new_background
+
+MyWidget will now have a `state_image` background set only by `new_background`,
+and not by any previous styles that may have set `state_image`.
+
+.. note::
+
+    Although the previous rules are cleared, they are still applied during
+    widget constrction, and are only removed when the new rule with the dash
+    is reached. This means that initially, previous rules could be used to set
+    the property.
+
 Lang Directives
 ---------------
 
@@ -829,7 +866,7 @@ class MyWidget(Label):
 
 # all the widget handlers, used to correctly unbind all the callbacks then the
 # widget is deleted
-_handlers = defaultdict(list)
+_handlers = defaultdict(partial(defaultdict, list))
 
 
 class ProxyApp(object):
@@ -932,9 +969,9 @@ class ParserRuleProperty(object):
     '''
 
     __slots__ = ('ctx', 'line', 'name', 'value', 'co_value',
-                 'watched_keys', 'mode', 'count')
+                 'watched_keys', 'mode', 'count', 'ignore_prev')
 
-    def __init__(self, ctx, line, name, value):
+    def __init__(self, ctx, line, name, value, ignore_prev=False):
         super(ParserRuleProperty, self).__init__()
         #: Associated parser
         self.ctx = ctx
@@ -952,6 +989,8 @@ def __init__(self, ctx, line, name, value):
         self.watched_keys = None
         #: Stats
         self.count = 0
+        #: whether previous rules targeting name should be cleared
+        self.ignore_prev = ignore_prev
 
     def precompile(self):
         name = self.name
@@ -1379,7 +1418,14 @@ def parse_level(self, level, lines, spaces=0):
                 # It's a class, add to the current object as a children
                 current_property = None
                 name = x[0]
+                ignore_prev = name[0] == '-'
+                if ignore_prev:
+                    name = name[1:]
+
                 if ord(name[0]) in Parser.CLASS_RANGE:
+                    if ignore_prev:
+                        raise ParserException(
+                            self, ln, 'clear previous, `-`, not allowed here')
                     _objects, _lines = self.parse_level(
                         level + 1, lines[i:], spaces)
                     current_object.children = _objects
@@ -1404,15 +1450,21 @@ def parse_level(self, level, lines, spaces=0):
                                 'Invalid id, cannot be "self" or "root"')
                         current_object.id = value
                     elif len(value):
-                        rule = ParserRuleProperty(self, ln, name, value)
+                        rule = ParserRuleProperty(
+                            self, ln, name, value, ignore_prev)
                         if name[:3] == 'on_':
                             current_object.handlers.append(rule)
                         else:
+                            ignore_prev = False
                             current_object.properties[name] = rule
                     else:
                         current_property = name
                         current_propobject = None
 
+                    if ignore_prev:  # it wasn't consumed
+                        raise ParserException(
+                            self, ln, 'clear previous, `-`, not allowed here')
+
             # Two more levels?
             elif count == indent + 2 * spaces:
                 if current_property in (
@@ -1586,7 +1638,7 @@ def create_handler(iself, element, key, value, rule, idmap, delayed=False):
     idmap = copy(idmap)
     idmap.update(global_idmap)
     idmap['self'] = iself.proxy_ref
-    handler_append = _handlers[iself.uid].append
+    handler_append = _handlers[iself.uid][key].append
 
     # we need a hash for when delayed, so we don't execute duplicate canvas
     # callbacks from the same handler during a sync op
@@ -1989,6 +2041,10 @@ def _apply_rule(self, widget, rule, rootrule, template_ctx=None):
         if rule.properties:
             rctx['set'].append((widget.proxy_ref,
                                 list(rule.properties.values())))
+            for key, crule in rule.properties.items():
+                # clear previously applied rules if asked
+                if crule.ignore_prev:
+                    Builder.unbind_property(widget, key)
         if rule.handlers:
             rctx['hdl'].append((widget.proxy_ref, rule.handlers))
 
@@ -2088,16 +2144,91 @@ def sync(self):
         _delayed_start = None
 
     def unbind_widget(self, uid):
-        '''(internal) Unbind all the handlers created by the rules of the
+        '''Unbind all the handlers created by the KV rules of the
         widget. The :attr:`kivy.uix.widget.Widget.uid` is passed here
-        instead of the widget itself, because we are using it in the
+        instead of the widget itself, because Builder is using it in the
         widget destructor.
 
+        This effectively clearls all the KV rules associated with this widget.
+        For example::
+
+            >>> w = Builder.load_string(\'''
+            ... Widget:
+            ...     height: self.width / 2. if self.disabled else self.width
+            ...     x: self.y + 50
+            ... \''')
+            >>> w.size
+            [100, 100]
+            >>> w.pos
+            [50, 0]
+            >>> w.width = 500
+            >>> w.size
+            [500, 500]
+            >>> Builder.unbind_widget(w.uid)
+            >>> w.width = 222
+            >>> w.y = 500
+            >>> w.size
+            [222, 500]
+            >>> w.pos
+            [50, 500]
+
         .. versionadded:: 1.7.2
         '''
         if uid not in _handlers:
             return
-        for callbacks in _handlers[uid]:
+        for prop_callbacks in _handlers[uid].values():
+            for callbacks in prop_callbacks:
+                for f, k, fn, bound_uid in callbacks:
+                    if fn is None:  # it's not a kivy prop.
+                        continue
+                    try:
+                        f.unbind_uid(k, bound_uid)
+                    except ReferenceError:
+                        # proxy widget is already gone, that's cool :)
+                        pass
+        del _handlers[uid]
+
+    def unbind_property(self, widget, name):
+        '''Unbind the handlers created by all the rules of the widget that set
+        the name.
+
+        This effectively clears all the rules of widget that take the form::
+
+            name: rule
+
+        For examples::
+
+            >>> w = Builder.load_string(\'''
+            ... Widget:
+            ...     height: self.width / 2. if self.disabled else self.width
+            ...     x: self.y + 50
+            ... \''')
+            >>> w.size
+            [100, 100]
+            >>> w.pos
+            [50, 0]
+            >>> w.width = 500
+            >>> w.size
+            [500, 500]
+            >>> Builder.unbind_property(w, 'height')
+            >>> w.width = 222
+            >>> w.size
+            [222, 500]
+            >>> w.y = 500
+            >>> w.pos
+            [550, 500]
+
+        .. versionadded:: 1.9.1
+        '''
+        uid = widget.uid
+        if uid not in _handlers:
+            return
+
+        prop_handlers = _handlers[uid]
+        if name not in prop_handlers:
+            return
+
+        for callbacks in prop_handlers[name]:
             for f, k, fn, bound_uid in callbacks:
                 if fn is None:  # it's not a kivy prop.
                     continue
@@ -2106,7 +2237,9 @@ def unbind_widget(self, uid):
                 except ReferenceError:
                     # proxy widget is already gone, that's cool :)
                     pass
-        del _handlers[uid]
+        del prop_handlers[name]
+        if not prop_handlers:
+            del _handlers[uid]
 
     def _build_canvas(self, canvas, widget, rule, rootrule):
         global Instruction


Test patch: