Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • caosdb/src/caosdb-pylib
1 result
Show changes
Commits on Source (20)
......@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ###
- New function in apiutils that copies an Entity.
### Changed ###
### Deprecated ###
......
......@@ -103,7 +103,10 @@ like this, check out the "Authentication" section in the [configuration document
Now would be a good time to continue with the [tutorials](tutorials/index).
## Run Unit Tests
tox
- Run all tests: `tox` or `make unittest`
- Run a specific test file: e.g. `tox -- unittests/test_schema.py`
- Run a specific test function: e.g. `tox -- unittests/test_schema.py::test_config_files`
## Documentation ##
......
......@@ -37,7 +37,9 @@ from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER,
REFERENCE, TEXT, is_reference)
from caosdb.common.models import (Container, Entity, File, Property, Query,
Record, RecordType, execute_query,
get_config)
get_config, SPECIAL_ATTRIBUTES)
import logging
def new_record(record_type, name=None, description=None,
......@@ -565,10 +567,6 @@ def getCommitIn(folder):
return t.readline().strip()
COMPARED = ["name", "role", "datatype", "description", "importance",
"id", "path", "checksum", "size"]
def compare_entities(old_entity: Entity, new_entity: Entity):
"""
Compare two entites.
......@@ -592,7 +590,7 @@ def compare_entities(old_entity: Entity, new_entity: Entity):
if old_entity is new_entity:
return (olddiff, newdiff)
for attr in COMPARED:
for attr in SPECIAL_ATTRIBUTES:
try:
oldattr = old_entity.__getattribute__(attr)
old_entity_attr_exists = True
......@@ -681,10 +679,77 @@ def compare_entities(old_entity: Entity, new_entity: Entity):
return (olddiff, newdiff)
def merge_entities(entity_a: Entity, entity_b: Entity):
"""
Merge entity_b into entity_a such that they have the same parents and properties.
datatype, unit, value, name and description will only be changed in entity_a if they
are None for entity_a and set for entity_b. If there is a corresponding value
for entity_a different from None a RuntimeError will be raised informing of an
unresolvable merge conflict.
The merge operation is done in place.
Returns entity_a.
WARNING: This function is currently experimental and insufficiently tested. Use with care.
"""
logging.warning(
"This function is currently experimental and insufficiently tested. Use with care.")
# Compare both entities:
diff_r1, diff_r2 = compare_entities(entity_a, entity_b)
# Go through the comparison and try to apply changes to entity_a:
for key in diff_r2["parents"]:
entity_a.add_parent(entity_b.get_parent(key))
for key in diff_r2["properties"]:
if key in diff_r1["properties"]:
if ("importance" in diff_r1["properties"][key] and
"importance" in diff_r2["properties"][key]):
if (diff_r1["properties"][key]["importance"] !=
diff_r2["properties"][key]["importance"]):
raise NotImplementedError()
elif ("importance" in diff_r1["properties"][key] or
"importance" in diff_r2["properties"][key]):
raise NotImplementedError()
for attribute in ("datatype", "unit", "value"):
if diff_r1["properties"][key][attribute] is None:
setattr(entity_a.get_property(key), attribute,
diff_r2["properties"][key][attribute])
else:
raise RuntimeError("Merge conflict.")
else:
# TODO: This is a temporary FIX for
# https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/issues/105
entity_a.add_property(id=entity_b.get_property(key).id,
name=entity_b.get_property(key).name,
datatype=entity_b.get_property(key).datatype,
value=entity_b.get_property(key).value,
unit=entity_b.get_property(key).unit,
importance=entity_b.get_importance(key))
# entity_a.add_property(
# entity_b.get_property(key),
# importance=entity_b.get_importance(key))
for special_attribute in ("name", "description"):
sa_a = getattr(entity_a, special_attribute)
sa_b = getattr(entity_b, special_attribute)
if sa_a != sa_b:
if sa_a is None:
setattr(entity_a, special_attribute, sa_b)
else:
raise RuntimeError("Merge conflict.")
return entity_a
def describe_diff(olddiff, newdiff, name=None, as_update=True):
description = ""
for attr in list(set(list(olddiff.keys())+list(newdiff.keys()))):
for attr in list(set(list(olddiff.keys()) + list(newdiff.keys()))):
if attr == "parents" or attr == "properties":
continue
description += "{} differs:\n".format(attr)
......
......@@ -79,6 +79,10 @@ ALL = "ALL"
NONE = "NONE"
SPECIAL_ATTRIBUTES = ["name", "role", "datatype", "description",
"id", "path", "checksum", "size"]
class Entity(object):
"""Entity is a generic CaosDB object.
......@@ -121,6 +125,48 @@ class Entity(object):
self.id = id
self.state = None
def copy(self):
"""
Return a copy of entity.
If deep == True return a deep copy, recursively copying all sub entities.
Standard properties are copied using add_property.
Special attributes, as defined by the global variable SPECIAL_ATTRIBUTES and additionaly
the "value" are copied using setattr.
"""
if self.role == "File":
new = File()
elif self.role == "Property":
new = Property()
elif self.role == "RecordType":
new = RecordType()
elif self.role == "Record":
new = Record()
elif self.role == "Entity":
new = Entity()
else:
raise RuntimeError("Unkonwn role.")
# Copy special attributes:
# TODO: this might rise an exception when copying
# special file attributes like checksum and size.
for attribute in SPECIAL_ATTRIBUTES + ["value"]:
val = getattr(self, attribute)
if val is not None:
setattr(new, attribute, val)
# Copy parents:
for p in self.parents:
new.add_parent(p)
# Copy properties:
for p in self.properties:
new.add_property(p, importance=self.get_importance(p))
return new
@property
def version(self):
if self._version is not None or self._wrapped_entity is None:
......
......@@ -31,10 +31,14 @@ import tempfile
import caosdb as db
import caosdb.apiutils
from caosdb.apiutils import (apply_to_ids, compare_entities, create_id_query,
resolve_reference)
resolve_reference, merge_entities)
from caosdb.common.models import SPECIAL_ATTRIBUTES
from .test_property import testrecord
import pytest
def test_convert_object():
r2 = db.apiutils.convert_to_python_object(testrecord)
......@@ -230,3 +234,92 @@ def test_compare_special_properties():
assert diff_r2[key] == 2
assert len(diff_r1["properties"]) == 0
assert len(diff_r2["properties"]) == 0
def test_copy_entities():
r = db.Record(name="A")
r.add_parent(name="B")
r.add_property(name="C", value=4, importance="OBLIGATORY")
r.add_property(name="D", value=[3, 4, 7], importance="OBLIGATORY")
r.description = "A fancy test record"
c = r.copy()
assert c is not r
assert c.name == "A"
assert c.role == r.role
assert c.parents[0].name == "B"
# parent and property objects are not shared among copy and original:
assert c.parents[0] is not r.parents[0]
for i in [0, 1]:
assert c.properties[i] is not r.properties[i]
for special in SPECIAL_ATTRIBUTES:
assert getattr(c.properties[i], special) == getattr(r.properties[i], special)
assert c.get_importance(c.properties[i]) == r.get_importance(r.properties[i])
def test_merge_entities():
r = db.Record(name="A")
r.add_parent(name="B")
r.add_property(name="C", value=4, importance="OBLIGATORY")
r.add_property(name="D", value=[3, 4, 7], importance="OBLIGATORY")
r.description = "A fancy test record"
r2 = db.Record()
r2.add_property(name="F", value="text")
merge_entities(r2, r)
assert r2.get_parents()[0].name == "B"
assert r2.get_property("C").name == "C"
assert r2.get_property("C").value == 4
assert r2.get_property("D").name == "D"
assert r2.get_property("D").value == [3, 4, 7]
assert r2.get_property("F").name == "F"
assert r2.get_property("F").value == "text"
def test_merge_bug_109():
rt = db.RecordType(name="TestBug")
p = db.Property(name="test_bug_property", datatype=db.LIST(db.INTEGER))
r_b = db.Record(name="TestRecord")
r_b.add_parent(rt)
r_b.add_property(p, value=[18, 19])
r_a = db.Record(name="TestRecord")
r_a.add_parent(rt)
merge_entities(r_a, r_b)
assert r_b.get_property("test_bug_property").value == [18, 19]
assert r_a.get_property("test_bug_property").value == [18, 19]
assert "<Value>18</Value>\n <Value>19</Value>" in str(r_b)
assert "<Value>18</Value>\n <Value>19</Value>\n <Value>18</Value>\n <Value>19</Value>" not in str(r_b)
assert "<Value>18</Value>\n <Value>19</Value>" in str(r_a)
assert "<Value>18</Value>\n <Value>19</Value>\n <Value>18</Value>\n <Value>19</Value>" not in str(r_a)
@pytest.mark.xfail
def test_bug_109():
rt = db.RecordType(name="TestBug")
p = db.Property(name="test_bug_property", datatype=db.LIST(db.INTEGER))
r_b = db.Record(name="TestRecord")
r_b.add_parent(rt)
r_b.add_property(p, value=[18, 19])
r_a = db.Record(name="TestRecord")
r_a.add_parent(rt)
r_a.add_property(r_b.get_property("test_bug_property"))
assert r_b.get_property("test_bug_property").value == [18, 19]
assert r_a.get_property("test_bug_property").value == [18, 19]
assert "<Value>18</Value>\n <Value>19</Value>" in str(r_b)
assert "<Value>18</Value>\n <Value>19</Value>\n <Value>18</Value>\n <Value>19</Value>" not in str(r_b)
assert "<Value>18</Value>\n <Value>19</Value>" in str(r_a)
assert "<Value>18</Value>\n <Value>19</Value>\n <Value>18</Value>\n <Value>19</Value>" not in str(r_a)