Repository: vertexproject/synapse

Issue:
setprop() should take both full props and rel props
```
setprop(foo:bar:baz=10)
- or -
setprop(:baz=10)
```
rather than
```
setprop(baz=10)
```

Discussion:

Patch:
diff --git a/synapse/cores/common.py b/synapse/cores/common.py
index 69634180c..47202a198 100644
--- a/synapse/cores/common.py
+++ b/synapse/cores/common.py
@@ -2208,7 +2208,7 @@ class Cortex(EventBus, DataModel, Runtime, Configable, s_ingest.IngestApi):
             valu = inprops.get(name)
 
             prop = form + ':' + name
-            if not self._okSetProp(prop):
+            if not self.isSetPropOk(prop):
                 inprops.pop(name, None)
                 continue
 
@@ -2328,8 +2328,27 @@ class Cortex(EventBus, DataModel, Runtime, Configable, s_ingest.IngestApi):
 
         return node
 
-    def _okSetProp(self, prop):
-        # check for enforcement and validity of a full prop name
+    def isSetPropOk(self, prop):
+        '''
+        Check for enforcement and validity of a full prop name.
+
+        This can be used to determine if a property name may be set on a node,
+        given the data models currently loaded in a Cortex.
+
+        Args:
+            prop (str): Full property name to check.
+
+        Examples:
+            Check if a value is valid before calling a function.::
+
+                prop = 'foo:bar:baz'
+                if core.isSetPropOk(prop):
+                    doSomething(...)
+
+        Returns:
+            bool: True if the property can be set on the node; False if it cannot be set.
+        '''
+        #
         if not self.enforce:
             return True
 
@@ -2339,10 +2358,17 @@ class Cortex(EventBus, DataModel, Runtime, Configable, s_ingest.IngestApi):
         '''
         Set ( with de-duplication ) the given tufo props.
 
-        Example:
+        Args:
+            tufo ((str, dict)): The tufo to set properties on.
+            **props:  Properties to set on the tufo.
 
-            tufo = core.setTufoProps(tufo, woot='hehe', blah=10)
+        Examples:
+            ::
+
+                tufo = core.setTufoProps(tufo, woot='hehe', blah=10)
 
+        Returns:
+            ((str, dict)): The source tufo, with any updated properties.
         '''
         reqiden(tufo)
         # add tufo form prefix to props
@@ -2398,7 +2424,7 @@ class Cortex(EventBus, DataModel, Runtime, Configable, s_ingest.IngestApi):
         form = tufo[1].get('tufo:form')
         prop = form + ':' + prop
 
-        if not self._okSetProp(prop):
+        if not self.isSetPropOk(prop):
             return tufo
 
         return self._incTufoProp(tufo, prop, incval=incval)
diff --git a/synapse/lib/storm.py b/synapse/lib/storm.py
index 1254253f7..376aee35f 100644
--- a/synapse/lib/storm.py
+++ b/synapse/lib/storm.py
@@ -4,6 +4,7 @@ import re
 import time
 import fnmatch
 import logging
+import collections
 
 import synapse.common as s_common
 import synapse.compat as s_compat
@@ -1121,12 +1122,58 @@ class Runtime(Configable):
         # TODO: use edits here for requested delete
 
     def _stormOperSetProp(self, query, oper):
+        # Coverage of this function is affected by the following issue:
+        # https://bitbucket.org/ned/coveragepy/issues/198/continue-marked-as-not-covered
         args = oper[1].get('args')
         props = dict(oper[1].get('kwlist'))
 
         core = self.getStormCore()
 
-        [core.setTufoProps(node, **props) for node in query.data()]
+        formnodes = collections.defaultdict(list)
+        formprops = collections.defaultdict(dict)
+
+        for node in query.data():
+            formnodes[node[1].get('tufo:form')].append(node)
+
+        forms = tuple(formnodes.keys())
+
+        for prop, valu in props.items():
+
+            if prop.startswith(':'):
+                valid = False
+                _prop = prop[1:]
+                # Check against every lifted form, since we may have a relative prop
+                # Which is valid against
+                for form in forms:
+                    _fprop = form + prop
+                    if core.isSetPropOk(_fprop):
+                        formprops[form][_prop] = valu
+                        valid = True
+                if not valid:
+                    mesg = 'Relative prop is not valid on any lifted forms.'
+                    raise s_common.BadSyntaxError(name=prop, mesg=mesg)
+                continue  # pragma: no cover
+
+            if prop.startswith(forms):
+                valid = False
+                for form in forms:
+                    if prop.startswith(form + ':') and core.isSetPropOk(prop):
+                        _prop = prop[len(form) + 1:]
+                        formprops[form][_prop] = valu
+                        valid = True
+                        break
+                if not valid:
+                    mesg = 'Full prop is not valid on any lifted forms.'
+                    raise s_common.BadSyntaxError(name=prop, mesg=mesg)
+                continue  # pragma: no cover
+
+            mesg = 'setprop operator requires props to start with relative or full prop names.'
+            raise s_common.BadSyntaxError(name=prop, mesg=mesg)
+
+        for form, nodes in formnodes.items():
+            props = formprops.get(form)
+            if props:
+                [core.setTufoProps(node, **props) for node in nodes]
 
     def _iterPropTags(self, props, tags):
         for prop in props:
diff --git a/synapse/lib/syntax.py b/synapse/lib/syntax.py
index b0f8c2f49..e12e62122 100644
--- a/synapse/lib/syntax.py
+++ b/synapse/lib/syntax.py
@@ -611,7 +611,7 @@ def parse(text, off=0):
 
                 valu, off = parse_macro_valu(text, off + 1)
                 if prop[0] == ':':
-                    kwargs = {prop[1:]: valu}
+                    kwargs = {prop: valu}
                     ret.append(oper('setprop', **kwargs))
                     continue
 


Test patch:
diff --git a/synapse/tests/test_lib_storm.py b/synapse/tests/test_lib_storm.py
index e4236c6b4..1cede3be2 100644
--- a/synapse/tests/test_lib_storm.py
+++ b/synapse/tests/test_lib_storm.py
@@ -44,13 +44,62 @@ class StormTest(SynTest):
         with s_cortex.openurl('ram:///') as core:
             core.setConfOpt('enforce', 1)
 
+            # relative key/val syntax, explicitly relative vals
+            node = core.formTufoByProp('inet:netuser', 'vertex.link/pennywise')
+            node = core.formTufoByProp('inet:netuser', 'vertex.link/visi')
+            node = core.eval('inet:netuser=vertex.link/pennywise setprop(:realname="Robert Gray")')[0]
+
+            self.eq(node[1].get('inet:netuser'), 'vertex.link/pennywise')
+            self.eq(node[1].get('inet:netuser:realname'), 'robert gray')
+
+            # Full prop val syntax
+            node = core.eval('inet:netuser=vertex.link/pennywise setprop(inet:netuser:signup="1970-01-01")')[0]
+            self.eq(node[1].get('inet:netuser'), 'vertex.link/pennywise')
+            self.eq(node[1].get('inet:netuser:signup'), 0)
+
+            # Combined syntax using both relative props and full props together
+            cmd = 'inet:netuser=vertex.link/pennywise setprop(:seen:min="2000", :seen:max="2017", ' \
+                  'inet:netuser:email=pennywise@vertex.link, inet:netuser:signup:ipv4="127.0.0.1")'
+            node = core.eval(cmd)[0]
+            self.nn(node[1].get('inet:netuser:seen:min'))
+            self.nn(node[1].get('inet:netuser:seen:max'))
+            self.nn(node[1].get('inet:netuser:signup:ipv4'))
+            self.eq(node[1].get('inet:netuser:email'), 'pennywise@vertex.link')
+
+            # old / bad syntax fails
+            # kwlist key/val syntax is no longer valid in setprop()
             node = core.formTufoByProp('inet:fqdn', 'vertex.link')
-
-            node = core.eval('inet:fqdn=vertex.link setprop(created="2016-05-05",updated="2017/05/05")')[0]
-
-            self.eq(node[1].get('inet:fqdn'), 'vertex.link')
-            self.eq(node[1].get('inet:fqdn:created'), 1462406400000)
-            self.eq(node[1].get('inet:fqdn:updated'), 1493942400000)
+            bad_cmd = 'inet:fqdn=vertex.link setprop(created="2016-05-05",updated="2017/05/05")'
+            self.raises(BadSyntaxError, core.eval, bad_cmd)
+            # a full prop which isn't valid for the node is bad
+            bad_cmd = 'inet:fqdn=vertex.link setprop(inet:fqdn:typocreated="2016-05-05")'
+            self.raises(BadSyntaxError, core.eval, bad_cmd)
+            # a rel prop which isn't valid for the node is bad
+            bad_cmd = 'inet:fqdn=vertex.link setprop(:typocreated="2016-05-05")'
+            self.raises(BadSyntaxError, core.eval, bad_cmd)
+
+            # test  possible form confusion
+            modl = {
+                'types': (
+                    ('foo:bar', {'subof': 'str'}),
+                    ('foo:barbaz', {'subof': 'str'})
+                ),
+                'forms': (
+                    ('foo:bar', {'ptype': 'str'}, [
+                        ('blah', {'ptype': 'str'})
+                    ]),
+                    ('foo:barbaz', {'ptype': 'str'}, [
+                        ('blah', {'ptype': 'str'})
+                    ]),
+                )
+            }
+            core.addDataModel('form_confusion', modl)
+            node = core.formTufoByProp('foo:bar', 'hehe')
+            core.addTufoTag(node, 'confusion')
+            node = core.formTufoByProp('foo:barbaz', 'haha')
+            core.addTufoTag(node, 'confusion')
+            node = core.eval('''#confusion setprop(foo:barbaz:blah=duck) +foo:barbaz''')[0]
+            self.eq(node[1].get('foo:barbaz:blah'), 'duck')
 
     def test_storm_filt_regex(self):