Repository: codalab/codalab-worksheets

Issue:
TypeError: expected string or bytes-like object
```
Error on request by matt.downey18(0xd72e93b8f2b448fc9318d136e89e555a):

POST /cli/command

Query params:

Form params:

JSON body:
{
"command": "cl run evaluate.py: 0xdcf10e1da32e45ce970bbb07fd4694eb",
"worksheet_uuid": "0x1fc53883bf2f432eb3bc0ae4164ee0dd"
}

Local variables:
bundle_spec=None
worksheet_uuid='0x1fc53883bf2f432eb3bc0ae4164ee0dd'
client=<codalab.client.json_api_client.JsonApiClient object at 0x7f678015a6a0>

Traceback (most recent call last):
File "/opt/codalab-worksheets/codalab/server/rest_server.py", line 106, in wrapper
return callback(*args, **kwargs)
File "/usr/local/bin/bottle.py", line 1732, in wrapper
rv = callback(*a, **ka)
File "/opt/codalab-worksheets/codalab/server/json_api_plugin.py", line 18, in wrapper
result = callback(*args, **kwargs)
File "/opt/codalab-worksheets/codalab/rest/cli.py", line 48, in post_worksheets_command
return general_command(query['worksheet_uuid'], query['command'])
File "/opt/codalab-worksheets/codalab/rest/cli.py", line 149, in general_command
structured_result = cli.do_command(args)
File "/opt/codalab-worksheets/codalab/lib/bundle_cli.py", line 839, in do_command
structured_result = command_fn()
File "/opt/codalab-worksheets/codalab/lib/bundle_cli.py", line 833, in <lambda>
command_fn = lambda: args.function(self, args)
File "/opt/codalab-worksheets/codalab/lib/bundle_cli.py", line 1527, in do_run_command
targets = self.resolve_key_targets(client, worksheet_uuid, args.target_spec)
File "/opt/codalab-worksheets/codalab/lib/bundle_cli.py", line 585, in resolve_key_targets
client, worksheet_uuid, target_spec, allow_remote=False
File "/opt/codalab-worksheets/codalab/lib/bundle_cli.py", line 563, in resolve_target
bundle_uuid = BundleCLI.resolve_bundle_uuid(client, worksheet_uuid, bundle_spec)
File "/opt/codalab-worksheets/codalab/lib/bundle_cli.py", line 597, in resolve_bundle_uuid
if spec_util.UUID_REGEX.match(bundle_spec):
TypeError: expected string or bytes-like object
```

Discussion:

Patch:
diff --git a/codalab/lib/cli_util.py b/codalab/lib/cli_util.py
index 1ea35acc..53409e04 100644
--- a/codalab/lib/cli_util.py
+++ b/codalab/lib/cli_util.py
@@ -62,7 +62,8 @@ def nested_dict_get(obj, *args, **kwargs):
 
 def parse_key_target(spec):
     """
-    Parses a keyed target spec into its key and the rest of the target spec
+    Parses a keyed target spec into its key and the rest of the target spec.
+    Raise UsageError when the value of the spec is empty.
     :param spec: a target spec in the form of
         [[<key>]:][<instance>::][<worksheet_spec>//]<bundle_spec>[/<subpath>]
     where <bundle_spec> is required and the rest are optional.
@@ -71,11 +72,19 @@ def parse_key_target(spec):
         - <key>: (<key> if present,
                     empty string if ':' in spec but no <key>,
                     None otherwise)
-        - <value> (where value is everyhing after a <key>: (or everything if no key specified)
+        - <value> (where value is everything after a <key>: (or everything if no key specified)
     """
-
     match = re.match(TARGET_KEY_REGEX, spec)
-    return match.groups()
+    key, value = match.groups()
+    # This check covers three usage errors:
+    # 1. both key and value are empty, e.g. "cl run : 'echo a'"
+    # 2. key is not empty, value is empty, e.g. "cl run a.txt: 'echo a'"
+    if value == '':
+        raise UsageError(
+            'target_spec (%s) in wrong format. Please provide a valid target_spec in the format of %s.'
+            % (spec, RUN_TARGET_SPEC_FORMAT)
+        )
+    return (key, value)
 
 
 def parse_target_spec(spec):
@@ -132,13 +141,6 @@ def desugar_command(orig_target_spec, command):
                     'key %s exists with multiple values: %s and %s' % (key, key2val[key], val)
                 )
         else:
-            if key is None:
-                raise UsageError(
-                    'target_spec is empty. Please provide a valid target_spec in the format of {}.'.format(
-                        RUN_TARGET_SPEC_FORMAT
-                    )
-                )
-
             key2val[key] = val
             target_spec.append(key + ':' + val)
         return key


Test patch:
diff --git a/tests/lib/cli_util_test.py b/tests/lib/cli_util_test.py
index 3d74c8f0..9ea0a6bb 100644
--- a/tests/lib/cli_util_test.py
+++ b/tests/lib/cli_util_test.py
@@ -59,6 +59,10 @@ class CLIUtilTest(unittest.TestCase):
         for spec, expected_parse in cases:
             self.assertEqual(cli_util.parse_key_target(spec), expected_parse)
 
+        usage_error_cases = [':', 'a:', '']
+        for spec in usage_error_cases:
+            self.assertRaises(UsageError, lambda: cli_util.parse_key_target(spec))
+
     def test_parse_target_spec(self):
         cases = [
             (
@@ -107,3 +111,6 @@ class CLIUtilTest(unittest.TestCase):
         )
         self.assertRaises(UsageError, lambda: cli_util.desugar_command([], 'echo %a:b% %a:c%'))
         self.assertRaises(UsageError, lambda: cli_util.desugar_command([':b'], 'echo %b:c%'))
+        self.assertRaises(UsageError, lambda: cli_util.desugar_command(['b:'], 'echo %b:c%'))
+        self.assertRaises(UsageError, lambda: cli_util.desugar_command([':'], 'echo %b:c%'))
+        self.assertRaises(UsageError, lambda: cli_util.desugar_command([''], 'echo %b:c%'))