Repository: ResearchObject/ro-crate-py (python)

Issue:
Allow entities to be passed in as `properties` values when initialising an entity
Example demonstrating the issue:
```python
from rocrate.model import ContextEntity, Person
from rocrate.rocrate import ROCrate

crate = ROCrate()

alice = crate.add(
    Person(
        crate,
        "https://orcid.org/0000-0000-0000-0000",
        properties={"name": "Alice Doe", "affiliation": "University of Flatland"},
    )
)
bob = crate.add(
    Person(
        crate,
        "https://orcid.org/0000-0000-0000-0001",
        properties={"name": "Bob Doe", "affiliation": "University of Flatland"},
    )
)

data = crate.add_file(
    "data.csv",
    properties={
        "name": "Data file",
        "encodingFormat": "text/csv",
        "author": [alice, bob],
    },
)

crate.write("out_crate")
```
Running this yields an error:
```
Traceback (most recent call last):
  File "/home/eli/roc/testing/crate.py", line 30, in <module>
    crate.write("crate_out")
  File "/home/eli/roc/testing/.venv-zenodo/lib/python3.12/site-packages/rocrate/rocrate.py", line 453, in write
    writable_entity.write(base_path)
  File "/home/eli/roc/testing/.venv-zenodo/lib/python3.12/site-packages/rocrate/model/metadata.py", line 80, in write
    json.dump(as_jsonld, outfile, indent=4, sort_keys=True)
. . .
TypeError: Object of type Person is not JSON serializable
```

This is because including the author entities in `properties` isn't supported. Instead I would have to use
```python
data = crate.add_file(
    "data.csv",
    properties={
        "name": "Data file",
        "encodingFormat": "text/csv",
    },
)
data["author"] = [alice, bob]
```

In my opinion this is a less intuitive way of doing things, and it's not straightforward for the user to understand why the second method works and the first one doesn't. 

Solution

Behind the scenes we are using a [custom setter](https://github.com/ResearchObject/ro-crate-py/blob/d31f2027b16fb535275c8c8f6c353f58cde64cef/rocrate/model/entity.py#L100) to set individual properties, but [`Entity.__init__()` ](https://github.com/ResearchObject/ro-crate-py/blob/d31f2027b16fb535275c8c8f6c353f58cde64cef/rocrate/model/entity.py#L31) skips activating that function when handling the `properties` argument, the referenced entities don't get connected properly. We can update that `__init__` function so that it triggers `__setitem__`. This way, both methods of setting the author would be equivalent.

Pull request:
Allow other entities as values in Entity's properties arg
Closes #189.

Note that the custom setter is *not* called if the property name starts with `@`, preserving the current behavior in this respect (the main use case is setting the `@type`).


Patch:
diff --git a/rocrate/model/entity.py b/rocrate/model/entity.py
index c18096e..22e69a5 100644
--- a/rocrate/model/entity.py
+++ b/rocrate/model/entity.py
@@ -34,12 +34,13 @@ class Entity(MutableMapping):
             self.__id = self.format_id(identifier)
         else:
             self.__id = f"#{uuid.uuid4()}"
+        self._jsonld = self._empty()
         if properties:
-            empty = self._empty()
-            empty.update(properties)
-            self._jsonld = empty
-        else:
-            self._jsonld = self._empty()
+            for name, value in properties.items():
+                if name.startswith("@"):
+                    self._jsonld[name] = value
+                else:
+                    self[name] = value
 
     @property
     def id(self):


Test patch:
diff --git a/test/test_model.py b/test/test_model.py
index 1935cc8..4465ece 100644
--- a/test/test_model.py
+++ b/test/test_model.py
@@ -513,3 +513,52 @@ def test_immutable_id():
     p = crate.add(Person(crate, "#foo"))
     with pytest.raises(AttributeError):
         p.id = "#bar"
+
+
+def test_entity_in_properties(tmpdir):
+    file_id = "data.csv"
+    file_path = tmpdir / file_id
+    with open(file_path, "wt") as fout:
+        fout.write("DATA\n")
+    crate = ROCrate()
+    alice_id = "https://orcid.org/0000-0000-0000-0000"
+    bob_id = "https://orcid.org/0000-0000-0000-0001"
+    alice = crate.add(Person(crate, alice_id, properties={
+        "name": "Alice Doe",
+        "affiliation": "University of Flatland",
+    }))
+    bob = crate.add(Person(crate, bob_id, properties={
+        "name": "Bob Doe",
+        "affiliation": "University of Flatland",
+    }))
+    data = crate.add_file(file_path, properties={
+        "name": "Data file",
+        "encodingFormat": "text/csv",
+        "author": [alice, bob],
+    })
+    authors = data["author"]
+    assert len(authors) == 2
+    assert authors[0] is alice
+    assert authors[1] is bob
+
+    book = crate.add(ContextEntity(crate, "#44cats", properties={
+        "@type": "Book",
+        "name": "44 Cats",
+        "author": alice,
+    }))
+    assert book.type == "Book"
+    assert book["author"] is alice
+
+    out_path = tmpdir / "ro_crate_out"
+    crate.write(out_path)
+    assert (out_path / file_id).is_file()
+    out_crate = ROCrate(out_path)
+    out_alice = out_crate.get(alice_id)
+    out_bob = out_crate.get(bob_id)
+    out_data = out_crate.get(file_id)
+    out_authors = out_data["author"]
+    assert len(out_authors) == 2
+    assert out_authors[0] is out_alice
+    assert out_authors[1] is out_bob
+    assert out_alice == alice
+    assert out_bob == bob