Repository: biolab/orange3

Issue:
Make is_discrete, is_continuous (also) useful as predicates
In #2706 we assumed that `filter(Variable.is_discrete, domain)` would return discrete variables from `domain`. It doesn't.

`is_discrete` and `is_continuous` are currently instance properties that return `bool` and as such cannot be used as predicates. To be used as predicates, they would need to be class properties resolving to a function. Python's `Descriptors` actually foresaw [this dual functionality](https://docs.python.org/2/reference/datamodel.html#object.__get__). Implementation would be as follows. 

```
class _IndicatorDescriptor:
    def __init__(self, atype):
        self.check_type = lambda instance: isinstance(instance, atype)

    def __get__(self, instance, owner):
        if instance is not None:
            return self.check_type(instance)
        else:
            return self.check_type

Variable.is_discrete = _IndicatorDescriptor(DiscreteVariable)
Variable.is_continuous = _IndicatorDescriptor(ContinuousVariable)
Variable.is_string = _IndicatorDescriptor(StringVariable)
Variable.is_time = _IndicatorDescriptor(TimeVariable)
```

This requires that `DiscreteVariable` and `ContinuousVariable` are defined before `Variable`, which is impossible, so properties are inserted into `Variable` class at the end of the module. (Needless to say, `Variable` could have a metaclass that would ensure that any derived class would push its corresponding property into the base class `Variable`. Needless to hope, @astaric won't like it. :))

This now allows

```
>>> v = ContinuousVariable("x")
>>> v.is_continuous  # current functionality
True
>>> Variable.is_continuous(v)  # needed for filter predicates
True
>>> v.is_discrete
False
>>> Variable.is_discrete(v)
False
```

This doesn't work for `is_primitive`, which is a class method. A similar trick should work there, however.

Please comment before I continue with `is_primitive`, add tests etc.


Discussion:
> Please comment before I continue ...

Please don't.
Why not???

Resolving patch:
diff --git a/Orange/data/variable.py b/Orange/data/variable.py
--- a/Orange/data/variable.py
+++ b/Orange/data/variable.py
@@ -343,28 +343,13 @@ def _clear_all_caches():
             cls._clear_cache()
 
     @classmethod
-    def is_primitive(cls):
+    def is_primitive(cls, var=None):
         """
         `True` if the variable's values are stored as floats.
         Non-primitive variables can appear in the data only as meta attributes.
         """
-        return issubclass(cls, (DiscreteVariable, ContinuousVariable))
-
-    @property
-    def is_discrete(self):
-        return isinstance(self, DiscreteVariable)
-
-    @property
-    def is_continuous(self):
-        return isinstance(self, ContinuousVariable)
-
-    @property
-    def is_string(self):
-        return isinstance(self, StringVariable)
-
-    @property
-    def is_time(self):
-        return isinstance(self, TimeVariable)
+        to_check = cls if var is None else type(var)
+        return issubclass(to_check, (DiscreteVariable, ContinuousVariable))
 
     def repr_val(self, val):
         """
@@ -1019,3 +1004,34 @@ def to_val(self, s):
             return self.parse(s)
         else:
             return super().to_val(s)
+
+
+class _TypeIndicatorDescriptor:
+    """
+    Descriptor that can be used on classes and on instances.
+
+    On an instance, the descriptor is used as a property of a variable,
+    e.g. `var.is_discrete`.
+
+    As a class attribute, the descriptor returns a predicate that can
+    be called as `Variable.is_discrete(var)` or, for a more common use,
+    `filter(Variable.is_discrete, domain)`.
+    """
+    def __init__(self, atype):
+        def __check_type(instance):
+            return isinstance(instance, atype)
+
+        self.check_type = __check_type
+
+    def __get__(self, instance, owner):
+        if instance is not None:
+            return self.check_type(instance)
+        else:
+            return self.check_type
+
+# These properties of Variable require its subclasses, hence they have to
+# inserted into the class later.
+Variable.is_discrete = _TypeIndicatorDescriptor(DiscreteVariable)
+Variable.is_continuous = _TypeIndicatorDescriptor(ContinuousVariable)
+Variable.is_string = _TypeIndicatorDescriptor(StringVariable)
+Variable.is_time = _TypeIndicatorDescriptor(TimeVariable)


Test patch:
diff --git a/Orange/data/tests/test_variable.py b/Orange/data/tests/test_variable.py
--- a/Orange/data/tests/test_variable.py
+++ b/Orange/data/tests/test_variable.py
@@ -107,6 +107,19 @@ def test_properties(self):
         self.assertTrue(a.is_string)
         self.assertFalse(a.is_primitive())
 
+    def test_properties_as_predicates(self):
+        a = ContinuousVariable()
+        self.assertTrue(Variable.is_continuous(a))
+        self.assertFalse(Variable.is_discrete(a))
+        self.assertFalse(Variable.is_string(a))
+        self.assertTrue(Variable.is_primitive(a))
+
+        a = StringVariable()
+        self.assertFalse(Variable.is_continuous(a))
+        self.assertFalse(Variable.is_discrete(a))
+        self.assertTrue(Variable.is_string(a))
+        self.assertFalse(Variable.is_primitive(a))
+
     def test_strange_eq(self):
         a = ContinuousVariable()
         b = ContinuousVariable()