Repository: ResearchObject/ro-crate-py

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.

Discussion:

Solution patch:
diff --git a/rocrate/model/entity.py b/rocrate/model/entity.py
index c18096ed..22e69a55 100644
--- a/rocrate/model/entity.py
+++ b/rocrate/model/entity.py
@@ -34,12 +34,13 @@ def __init__(self, crate, identifier=None, properties=None):
             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 1935cc8f..4465ecee 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