Repository: DLR-SC/prov-db-connector

Issue:
influence(influencee, influencer, identifier=None, other_attributes=None) causes "TypeError: 'NoneType' object is not callable"
Hello,
i tried to call the prov influence in combination of agent, entity and activity with all possible combination. The result was a TypeError: 'NoneType' object is not callable.

Traceback (most recent call last):
  File "c:\Users\..\.vscode\extensions\ms-python.python-2019.9.34911\pythonFiles\ptvsd_launcher.py", line 43, in <module>
    main(ptvsdArgs)
  File "c:\Users\..\.vscode\extensions\ms-python.python-2019.9.34911\pythonFiles\lib\python\ptvsd\__main__.py", line 432, in main
    run()
  File "c:\Users\..\.vscode\extensions\ms-python.python-2019.9.34911\pythonFiles\lib\python\ptvsd\__main__.py", line 316, in run_file
    runpy.run_path(target, run_name='__main__')
  File "C:\Anaconda3\lib\runpy.py", line 263, in run_path
    pkg_name=pkg_name, script_name=fname)
  File "C:\Anaconda3\lib\runpy.py", line 96, in _run_module_code
    mod_name, mod_spec, pkg_name, script_name)
  File "C:\Anaconda3\lib\runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "c:\dev\..\ProvDBConnector\prov_model_test1.py", line 199, in <module>
    document_id = prov_api.save_document(prov_document)
  File "C:\Anaconda3\lib\site-packages\provdbconnector\prov_db.py", line 162, in save_document
    doc_id = self._save_bundle_internal(prov_document)
  File "C:\Anaconda3\lib\site-packages\provdbconnector\prov_db.py", line 503, in _save_bundle_internal
    self.save_relation(relation, bundle_id)
  File "C:\Anaconda3\lib\site-packages\provdbconnector\prov_db.py", line 563, in save_relation
    self.save_element(prov_element=from_type_cls(prov_relation.bundle, identifier=from_qualified_name), bundle_id=bundle_id)
TypeError: 'NoneType' object is not callable

Python: 3.7.1
prov-db-connector: 0.3.1
prov: 1.5.2 / 1.5.3


Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 1709012..23f1ae4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,7 +3,8 @@ sudo: required
 python:
   - 3.5
   - 3.6
-  - 3.7-dev
+  - 3.7
+  - 3.8
 services:
   - docker
 before_install:
@@ -13,7 +14,6 @@ install:
   - pip install '.[test]'
   - pip install '.[docs]'
 script:
-  - docker ps | grep -q 'provdbconnector_neo4j_1'
   - curl --output /dev/null --silent --head --fail http://localhost:7474
   - coverage run --source=provdbconnector setup.py test
   - make docs-travis
diff --git a/examples/simple_example_influence.py b/examples/simple_example_influence.py
new file mode 100644
index 0000000..7813d67
--- /dev/null
+++ b/examples/simple_example_influence.py
@@ -0,0 +1,28 @@
+from prov.model import ProvDocument
+from provdbconnector import ProvDb
+from provdbconnector.db_adapters.in_memory import SimpleInMemoryAdapter
+
+prov_api = ProvDb(adapter=SimpleInMemoryAdapter, auth_info=None)
+
+# create the prov document
+prov_document = ProvDocument()
+prov_document.add_namespace("ex", "http://example.com")
+
+prov_document.agent("ex:Bob")
+prov_document.activity("ex:Alice")
+
+prov_document.influence("ex:Alice", "ex:Bob")
+
+document_id = prov_api.save_document(prov_document)
+
+print(prov_api.get_document_as_provn(document_id))
+
+# Output:
+#
+# document
+#   prefix ex < http: // example.com >
+#
+#   agent(ex: Bob)
+#   activity(ex: Alice, -, -)
+#   wasInfluencedBy(ex: Alice, ex: Bob)
+# endDocument
diff --git a/provdbconnector/prov_db.py b/provdbconnector/prov_db.py
index 4fb41b2..6568b4f 100644
--- a/provdbconnector/prov_db.py
+++ b/provdbconnector/prov_db.py
@@ -558,7 +558,8 @@ class ProvDb(object):
         to_type_cls = PROV_ATTR_BASE_CLS[to_type]
 
         if from_type_cls is None or to_type_cls is None:
-            log.info("Something went wrong ")
+            raise InvalidArgumentTypeException(
+                "Could not determinate typ for relation from: {}, to: {}, prov_relation was {}, ".format(from_type, to_type, type(prov_relation)))
         #save from and to node
         self.save_element(prov_element=from_type_cls(prov_relation.bundle, identifier=from_qualified_name), bundle_id=bundle_id)
 
diff --git a/provdbconnector/utils/serializer.py b/provdbconnector/utils/serializer.py
index 642e883..472efe8 100644
--- a/provdbconnector/utils/serializer.py
+++ b/provdbconnector/utils/serializer.py
@@ -13,7 +13,8 @@ from prov.constants import PROV_QUALIFIEDNAME, PROV_ATTRIBUTES_ID_MAP, PROV_ATTR
     PROV_ATTR_GENERATION,PROV_ATTR_USAGE,PROV_ATTR_SPECIFIC_ENTITY,PROV_ATTR_GENERAL_ENTITY,PROV_ATTR_ALTERNATE1, \
     PROV_ATTR_ALTERNATE2,PROV_ATTR_BUNDLE,PROV_ATTR_INFLUENCEE,PROV_ATTR_INFLUENCER
 
-from prov.model import Literal, Identifier, QualifiedName, Namespace, parse_xsd_datetime, PROV_REC_CLS, ProvAgent, ProvEntity, ProvActivity
+from prov.model import Literal, Identifier, QualifiedName, Namespace, parse_xsd_datetime, PROV_REC_CLS, ProvAgent, \
+    ProvEntity, ProvActivity, ProvElement
 from provdbconnector.db_adapters.baseadapter import METADATA_KEY_NAMESPACES, METADATA_KEY_PROV_TYPE, \
     METADATA_KEY_TYPE_MAP
 from provdbconnector.exceptions.database import MergeException
@@ -53,15 +54,15 @@ PROV_ATTR_BASE_CLS = {
     PROV_ATTR_RESPONSIBLE: ProvAgent,
     PROV_ATTR_GENERATED_ENTITY: ProvEntity,
     PROV_ATTR_USED_ENTITY: ProvEntity,
-    PROV_ATTR_GENERATION: None,
-    PROV_ATTR_USAGE:None,
+    PROV_ATTR_GENERATION: ProvElement,
+    PROV_ATTR_USAGE: ProvElement,
     PROV_ATTR_SPECIFIC_ENTITY: ProvEntity,
     PROV_ATTR_GENERAL_ENTITY: ProvEntity,
     PROV_ATTR_ALTERNATE1: ProvEntity,
     PROV_ATTR_ALTERNATE2: ProvEntity,
-    PROV_ATTR_BUNDLE:None,
-    PROV_ATTR_INFLUENCEE:None,
-    PROV_ATTR_INFLUENCER:None,
+    PROV_ATTR_BUNDLE:ProvElement,
+    PROV_ATTR_INFLUENCEE: ProvElement,
+    PROV_ATTR_INFLUENCER: ProvElement,
     PROV_ATTR_COLLECTION: ProvEntity
 }
 
@@ -370,9 +371,27 @@ def merge_record(attributes, metadata, other_attributes, other_metadata):
     attributes_merged = attributes.copy()
     attributes_merged.update(other_attributes)
 
-    if metadata[METADATA_KEY_PROV_TYPE] != other_metadata[METADATA_KEY_PROV_TYPE]:
+    metadata_prov_typ = metadata[METADATA_KEY_PROV_TYPE]
+    other_metadata_prov_typ = other_metadata[METADATA_KEY_PROV_TYPE]
+
+    # Determinate non unknown prov type for merge
+    merged_prov_typ = other_metadata_prov_typ
+    is_one_prov_type_unknown = False
+    # Support unknown typ during the merge process, needed for polymorph nodes
+    if other_metadata_prov_typ.localpart == "Unknown":
+        merged_prov_typ = metadata_prov_typ
+        is_one_prov_type_unknown = True
+
+    if metadata_prov_typ.localpart == "Unknown":
+        merged_prov_typ = other_metadata_prov_typ
+        is_one_prov_type_unknown = True
+
+    if merged_prov_typ.localpart == "Unknown":
+        raise MergeException("Prov type can't be unknown metadata: {}, other metadata: {}".format(metadata_prov_typ, other_metadata_prov_typ))
+
+    if metadata_prov_typ != other_metadata_prov_typ and not is_one_prov_type_unknown:
         raise MergeException(
-            "Prov type should be the same but is: {}:{}".format(METADATA_KEY_PROV_TYPE, METADATA_KEY_PROV_TYPE))
+            "Prov type should be the same but is: {}:{}".format(metadata_prov_typ, other_metadata_prov_typ))
 
     for (key, value) in attributes.items():
         if attributes_merged[key] != value:
@@ -388,6 +407,7 @@ def merge_record(attributes, metadata, other_attributes, other_metadata):
 
     merged_metadata = metadata.copy()
     merged_metadata.update(other_metadata)
+    merged_metadata.update({METADATA_KEY_PROV_TYPE: merged_prov_typ})
     merged_metadata.update({METADATA_KEY_NAMESPACES: merged_metadata_namespaces})
     merged_metadata.update({METADATA_KEY_TYPE_MAP: merged_metadata_type_map})
 
diff --git a/setup.py b/setup.py
index 311e68d..fba276c 100644
--- a/setup.py
+++ b/setup.py
@@ -35,6 +35,9 @@ setup(
         'License :: OSI Approved :: Apache Software License',
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5'
+        'Programming Language :: Python :: 3.6'
+        'Programming Language :: Python :: 3.7'
+        'Programming Language :: Python :: 3.8'
     ],
     license="Apache License 2.0",
 


Test patch:
diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py
index f570c9d..396b4d0 100644
--- a/examples/tests/test_examples.py
+++ b/examples/tests/test_examples.py
@@ -35,6 +35,13 @@ class ExamplesTest(unittest.TestCase):
         """
         import examples.simple_example
 
+    def test_simple_example_influence(self):
+        """
+        Test the basic example with influence by relation
+
+        """
+        import examples.simple_example_influence
+
     def test_simple_example_with_neo4j(self):
         """
         Test the basic neo4j example
diff --git a/provdbconnector/tests/examples.py b/provdbconnector/tests/examples.py
index b17330a..5f30f87 100644
--- a/provdbconnector/tests/examples.py
+++ b/provdbconnector/tests/examples.py
@@ -17,6 +17,16 @@ from provdbconnector.db_adapters.baseadapter import METADATA_KEY_NAMESPACES, MET
     METADATA_KEY_TYPE_MAP, METADATA_KEY_IDENTIFIER
 
 
+def prov_db_unknown_prov_typ_example():
+    doc = ProvDocument()
+    doc.add_namespace("ex", "https://example.com")
+    doc.entity(identifier="ex:Entity1")
+    doc.entity(identifier="ex:Entity2")
+    doc.influence(influencee="ex:Entity1", influencer="ex:Entity2")
+    return doc
+
+
+
 def attributes_dict_example():
     """
     Retuns a example dict with some different attributes
diff --git a/provdbconnector/tests/test_prov_db.py b/provdbconnector/tests/test_prov_db.py
index 0919e12..f53bf9e 100644
--- a/provdbconnector/tests/test_prov_db.py
+++ b/provdbconnector/tests/test_prov_db.py
@@ -641,6 +641,18 @@ class ProvDbTests(unittest.TestCase):
         with self.assertRaises(InvalidArgumentTypeException):
             self.provapi._get_metadata_and_attributes_for_record(None)
 
+    def test_save_unknown_prov_typ(self):
+        """
+        Test to prefer non unknown prov type
+        """
+        self.clear_database()
+        doc = examples.prov_db_unknown_prov_typ_example()
+        self.provapi.save_document(doc)
+        doc_with_entities = self.provapi.get_elements(ProvEntity)
+        self.assertEqual(len(doc_with_entities.records), 2)
+        self.assertIsInstance(doc_with_entities.records[0], ProvEntity)
+        self.assertIsInstance(doc_with_entities.records[1], ProvEntity)
+
     def test_get_metadata_and_attributes_for_record(self):
         """
         Test the split into metadata / attributes function