Repository: glotzerlab/signac

Issue:
Using signac find on the CLI for tuples
<!-- Please replace the text in the individual sections below. -->

### Description

Finding jobs for which a given statepoint parameter, x, is equal to a particular list cannot be as easily done with the `signac find` CLI option as it can with other statepoint parameters. For example, `signac find x '[0, 1, 0]'` is currently interpreted as `'{"x": "[0, 1, 0]"}'` . The user has to explicitly specify `signac find '{"x": [0, 1, 0]}'` to get around this.

### To reproduce

`signac find x '[0, 1, 0]'` will return no jobs, even if x = [0, 1, 0] for some of them

### Error output

No error is thrown. The command is only misinterpreted.

### System configuration

Please complete the following information:

 - Operating System [e.g. macOS]: macOS
 - Version of Python [e.g. 3.7]: 3.9.6
 - Version of signac [e.g. 1.0]: 1.7.0

Or copy & paste the output of: `python -c 'import platform; print(platform.platform()); import sys; print(sys.version); import signac; print(signac.__version__)'`


Comments:

Patch:
diff --git a/signac/contrib/filterparse.py b/signac/contrib/filterparse.py
index 0fe52b75..9868fb8d 100644
--- a/signac/contrib/filterparse.py
+++ b/signac/contrib/filterparse.py
@@ -62,8 +62,8 @@ def _read_index(project, fn_index=None):
         return (json.loads(line) for line in fd)
 
 
-def _is_json(q):
-    """Check if q is JSON.
+def _is_json_like(q):
+    """Check if q is JSON like.
 
     Parameters
     ----------
@@ -76,7 +76,7 @@ def _is_json(q):
         True if q starts with "{" and ends with "}".
 
     """
-    return q.strip().startswith("{") and q.strip().endswith("}")
+    return (q[0] == "{" and q[-1] == "}") or (q[0] == "[" and q[-1] == "]")
 
 
 def _is_regex(q):
@@ -179,19 +179,18 @@ def _parse_single(key, value=None):
     ------
     ValueError
         If filter arguments have an invalid key.
-
     """
-    if value is None or value == "!":
+    if _is_json_like(key):
+        raise ValueError(
+            "Please check your filter arguments. "
+            f"Using a JSON expression as a key is not allowed: '{key}'."
+        )
+    elif value is None or value == "!":
         return key, {"$exists": True}
-    elif _is_json(value):
+    elif _is_json_like(value):
         return key, _parse_json(value)
     elif _is_regex(value):
         return key, {"$regex": value[1:-1]}
-    elif _is_json(key):
-        raise ValueError(
-            "Please check your filter arguments. "
-            "Using a JSON expression as a key is not allowed: '{}'.".format(key)
-        )
     else:
         return key, _cast(value)
 
@@ -238,14 +237,13 @@ def parse_filter_arg(args, file=sys.stderr):
     if args is None or len(args) == 0:
         return None
     elif len(args) == 1:
-        if _is_json(args[0]):
+        if _is_json_like(args[0]):
             return _parse_json(args[0])
         else:
             key, value = _parse_single(args[0])
             return _with_message({key: value}, file)
     else:
         q = dict(parse_simple(args))
-
         return _with_message(q, file)
 
 


Test patch:
diff --git a/tests/test_shell.py b/tests/test_shell.py
index 6ea63bd4..1c5f62de 100644
--- a/tests/test_shell.py
+++ b/tests/test_shell.py
@@ -239,6 +239,7 @@ class TestBasicShell:
         self.call("python -m signac init my_project".split())
         project = signac.Project()
         sps = [{"a": i} for i in range(3)]
+        sps.append({"a": [0, 1, 0]})
         for sp in sps:
             project.open_job(sp).init()
         out = self.call("python -m signac find".split())
@@ -269,8 +270,15 @@ class TestBasicShell:
         assert '{"a": 0}' in out
         assert '{"a": 2}' in out
 
+        job = project.open_job({"a": [0, 1, 0]})
+        msg = [*"python -m signac find a".split(), "[0, 1, 0]", "--sp"]
+        out = self.call(msg).strip()
+        assert out.strip().split(os.linesep) == [str(job.id), str(job.statepoint)]
+
         # Test the doc_filter
         for job in project.find_jobs():
+            if job.statepoint()["a"] == [0, 1, 0]:
+                continue
             job.document["a"] = job.statepoint()["a"]
             job.document["b"] = job.statepoint()["a"] + 1