Repository: dwavesystems/dwave-cloud-client

Issue:
Solver filtering failing for some ops with non-existing properties
For example, relational and set operators will fail when applied to a property that a particular solver does not have:
```
$ dwave solvers -s '{"max_anneal_schedule_points__gt": 2}'
Traceback (most recent call last):
  File "/home/radomir/work/dwave-cloud-client/env3/bin/dwave", line 11, in <module>
    load_entry_point('dwave-cloud-client', 'console_scripts', 'dwave')()
  File "/home/radomir/work/dwave-cloud-client/env3/lib/python3.5/site-packages/click/core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "/home/radomir/work/dwave-cloud-client/env3/lib/python3.5/site-packages/click/core.py", line 697, in main
    rv = self.invoke(ctx)
  File "/home/radomir/work/dwave-cloud-client/env3/lib/python3.5/site-packages/click/core.py", line 1066, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/radomir/work/dwave-cloud-client/env3/lib/python3.5/site-packages/click/core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/radomir/work/dwave-cloud-client/env3/lib/python3.5/site-packages/click/core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "/home/radomir/work/dwave-cloud-client/dwave/cloud/cli.py", line 314, in solvers
    solvers = client.get_solvers(**client.default_solver)
  File "/home/radomir/work/dwave-cloud-client/dwave/cloud/client.py", line 820, in get_solvers
    solvers = [s for s in solvers if all(p(s) for p in predicates)]
  File "/home/radomir/work/dwave-cloud-client/dwave/cloud/client.py", line 820, in <listcomp>
    solvers = [s for s in solvers if all(p(s) for p in predicates)]
  File "/home/radomir/work/dwave-cloud-client/dwave/cloud/client.py", line 820, in <genexpr>
    solvers = [s for s in solvers if all(p(s) for p in predicates)]
  File "/home/radomir/work/dwave-cloud-client/dwave/cloud/client.py", line 780, in predicate
    return op(None, val)
TypeError: unorderable types: NoneType() > int()
```

Some operators should accept `None` as a LHS value (`eq`, `available`, `in`), but for some (relational, set) LHS `None` doesn't make too much sense.

Proposal: patch the affected operators/predicates to shortcircuit to `False` if LHS is `None`.

Discussion:

Patch:
diff --git a/dwave/cloud/client.py b/dwave/cloud/client.py
index bf97812..7aa7d1e 100644
--- a/dwave/cloud/client.py
+++ b/dwave/cloud/client.py
@@ -53,7 +53,7 @@ import warnings
 import operator
 import collections
 from itertools import chain
-from functools import partial
+from functools import partial, wraps
 
 from dateutil.parser import parse as parse_datetime
 from plucky import pluck
@@ -734,24 +734,32 @@ class Client(object):
                 return set(tuple(x) for x in iterable)
             return set(iterable)
 
+        def with_valid_lhs(op):
+            @wraps(op)
+            def _wrapper(prop, val):
+                if prop is None:
+                    return False
+                return op(prop, val)
+            return _wrapper
+
         # available filtering operators
         ops = {
-            'lt': operator.lt,
-            'lte': operator.le,
-            'gt': operator.gt,
-            'gte': operator.ge,
+            'lt': with_valid_lhs(operator.lt),
+            'lte': with_valid_lhs(operator.le),
+            'gt': with_valid_lhs(operator.gt),
+            'gte': with_valid_lhs(operator.ge),
             'eq': operator.eq,
             'available': lambda prop, val: prop is not None if val else prop is None,
-            'regex': lambda prop, val: re.match("^{}$".format(val), prop),
+            'regex': with_valid_lhs(lambda prop, val: re.match("^{}$".format(val), prop)),
             # range operations
-            'covers': covers_op,
-            'within': within_op,
+            'covers': with_valid_lhs(covers_op),
+            'within': with_valid_lhs(within_op),
             # membership tests
             'in': lambda prop, val: prop in val,
-            'contains': lambda prop, val: val in prop,
+            'contains': with_valid_lhs(lambda prop, val: val in prop),
             # set tests
-            'issubset': lambda prop, val: _set(prop).issubset(_set(val)),
-            'issuperset': lambda prop, val: _set(prop).issuperset(_set(val)),
+            'issubset': with_valid_lhs(lambda prop, val: _set(prop).issubset(_set(val))),
+            'issuperset': with_valid_lhs(lambda prop, val: _set(prop).issuperset(_set(val))),
         }
 
         # features available as `Solver` attribute/properties


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index aa0e8f8..a62e25e 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -278,7 +278,11 @@ class FeatureBasedSolverSelection(unittest.TestCase):
                 "num_reads_range": [0, 1000],
                 "parameters": {"num_reads": "Number of samples to return."},
                 "vfyc": False,
-                "avg_load": 0.7
+                # the following are only present in this solver
+                "avg_load": 0.7,
+                "some_set": [1, 2],
+                "some_range": [1, 2],
+                "some_string": "x"
             },
             "id": "c4-sw_solver3",
             "description": "A test of software solver"
@@ -364,6 +368,16 @@ class FeatureBasedSolverSelection(unittest.TestCase):
         self.assertSolvers(self.client.get_solvers(num_qubits__lte=7), self.solvers)
         self.assertSolvers(self.client.get_solvers(num_qubits__lt=7), [self.solver1, self.solver2])
 
+        # skip solver if LHS value not defined (None)
+        self.assertSolvers(self.client.get_solvers(avg_load__gt=0), [self.solver3])
+        self.assertSolvers(self.client.get_solvers(avg_load__gte=0), [self.solver3])
+        self.assertSolvers(self.client.get_solvers(avg_load__lt=1), [self.solver3])
+        self.assertSolvers(self.client.get_solvers(avg_load__lte=1), [self.solver3])
+        self.assertSolvers(self.client.get_solvers(avg_load=0.7), [self.solver3])
+        self.assertSolvers(self.client.get_solvers(avg_load__eq=0.7), [self.solver3])
+        self.assertSolvers(self.client.get_solvers(avg_load=None), [self.solver1, self.solver2])
+        self.assertSolvers(self.client.get_solvers(avg_load__eq=None), [self.solver1, self.solver2])
+
     def test_range_ops(self):
         # value within range
         self.assertSolvers(self.client.get_solvers(num_qubits__within=[3, 7]), self.solvers)
@@ -376,6 +390,9 @@ class FeatureBasedSolverSelection(unittest.TestCase):
         self.assertSolvers(self.client.get_solvers(num_reads_range__within=(0, 500)), [self.solver1, self.solver2])
         self.assertSolvers(self.client.get_solvers(num_reads_range__within=(1, 500)), [])
 
+        # invalid LHS
+        self.assertSolvers(self.client.get_solvers(some_range__within=[0, 2]), [self.solver3])
+
         # range covering a value (value included in range)
         self.assertSolvers(self.client.get_solvers(num_reads_range__covers=0), self.solvers)
         self.assertSolvers(self.client.get_solvers(num_reads_range__covers=150), [self.solver2, self.solver3])
@@ -387,6 +404,9 @@ class FeatureBasedSolverSelection(unittest.TestCase):
         self.assertSolvers(self.client.get_solvers(num_reads_range__covers=(10, 90)), self.solvers)
         self.assertSolvers(self.client.get_solvers(num_reads_range__covers=(110, 200)), [self.solver2, self.solver3])
 
+        # invalid LHS
+        self.assertSolvers(self.client.get_solvers(some_range__covers=1.5), [self.solver3])
+
     def test_membership_ops(self):
         # property contains
         self.assertSolvers(self.client.get_solvers(supported_problem_types__contains="qubo"), self.solvers)
@@ -399,6 +419,11 @@ class FeatureBasedSolverSelection(unittest.TestCase):
         self.assertSolvers(self.client.get_solvers(num_qubits__in=[7]), [self.solver3])
         self.assertSolvers(self.client.get_solvers(num_qubits__in=[]), [])
 
+        # invalid LHS
+        self.assertSolvers(self.client.get_solvers(some_set__contains=1), [self.solver3])
+        self.assertSolvers(self.client.get_solvers(avg_load__in=[None]), [self.solver1, self.solver2])
+        self.assertSolvers(self.client.get_solvers(avg_load__in=[None, 0.7]), self.solvers)
+
     def test_set_ops(self):
         # property issubset
         self.assertSolvers(self.client.get_solvers(supported_problem_types__issubset=("qubo", "ising", "other")), self.solvers)
@@ -418,12 +443,19 @@ class FeatureBasedSolverSelection(unittest.TestCase):
         self.assertSolvers(self.client.get_solvers(couplers__issuperset={(0, 1), (0, 2), (2, 3)}), [self.solver2])
         self.assertSolvers(self.client.get_solvers(couplers__issuperset={(0, 1), (0, 2), (2, 3), (0, 5)}), [])
 
+        # invalid LHS
+        self.assertSolvers(self.client.get_solvers(some_set__issubset={0, 1, 2}), [self.solver3])
+        self.assertSolvers(self.client.get_solvers(some_set__issuperset={1}), [self.solver3])
+
     def test_regex(self):
         self.assertSolvers(self.client.get_solvers(num_reads__regex='.*number.*'), [])
         self.assertSolvers(self.client.get_solvers(num_reads__regex='.*Number.*'), self.solvers)
         self.assertSolvers(self.client.get_solvers(num_reads__regex='Number.*'), self.solvers)
         self.assertSolvers(self.client.get_solvers(num_reads__regex='Number'), [])
 
+        # invalid LHS
+        self.assertSolvers(self.client.get_solvers(some_string__regex='x'), [self.solver3])
+
     def test_range_boolean_combo(self):
         self.assertSolvers(self.client.get_solvers(num_qubits=3, vfyc=True), [])
         self.assertSolvers(self.client.get_solvers(num_qubits__gte=3, vfyc=True), [self.solver2])