Skip to content
Snippets Groups Projects
Commit 9410d638 authored by Daniel Hornung's avatar Daniel Hornung
Browse files

Merge branch 'f-merge-conflict-error' into 'dev'

F merge conflict error

See merge request !81
parents 31bea860 d575fc54
No related branches found
No related tags found
2 merge requests!93Release 0.11.0,!81F merge conflict error
Pipeline #30847 failed
...@@ -9,8 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -9,8 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### ### Added ###
* `apiutils.EntityMergeConflictError` class for unresesolvable merge conflicts
when merging two entities
### Changed ### ### Changed ###
* `apiutils.merge_entities` now raises an `EntityMergeConflictError` in case of
unresolvable merge conflicts.
### Deprecated ### ### Deprecated ###
### Removed ### ### Removed ###
......
...@@ -27,12 +27,13 @@ ...@@ -27,12 +27,13 @@
Some simplified functions for generation of records etc. Some simplified functions for generation of records etc.
""" """
import logging
import sys import sys
import tempfile import tempfile
import warnings import warnings
from collections.abc import Iterable from collections.abc import Iterable
from subprocess import call from subprocess import call
from typing import Optional, Any, Dict, List from typing import Optional, Any, Dict, List
from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER, from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER,
...@@ -40,8 +41,13 @@ from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER, ...@@ -40,8 +41,13 @@ from caosdb.common.datatype import (BOOLEAN, DATETIME, DOUBLE, FILE, INTEGER,
from caosdb.common.models import (Container, Entity, File, Property, Query, from caosdb.common.models import (Container, Entity, File, Property, Query,
Record, RecordType, execute_query, Record, RecordType, execute_query,
get_config, SPECIAL_ATTRIBUTES) get_config, SPECIAL_ATTRIBUTES)
from caosdb.exceptions import CaosDBException
import logging
class EntityMergeConflictError(CaosDBException):
"""An error that is raised in case of an unresolvable conflict when merging
two entities.
"""
def new_record(record_type, name=None, description=None, def new_record(record_type, name=None, description=None,
...@@ -365,14 +371,15 @@ def empty_diff(old_entity: Entity, new_entity: Entity, compare_referenced_record ...@@ -365,14 +371,15 @@ def empty_diff(old_entity: Entity, new_entity: Entity, compare_referenced_record
return True return True
def merge_entities(entity_a: Entity, entity_b: Entity, merge_references_with_empty_diffs=True, force=False): def merge_entities(entity_a: Entity, entity_b: Entity, merge_references_with_empty_diffs=True,
""" force=False):
Merge entity_b into entity_a such that they have the same parents and properties. """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 datatype, unit, value, name and description will only be changed in entity_a
are None for entity_a and set for entity_b. If there is a corresponding value if they are None for entity_a and set for entity_b. If there is a
for entity_a different from None a RuntimeError will be raised informing of an corresponding value for entity_a different from None, an
unresolvable merge conflict. EntityMergeConflictError will be raised to inform about an unresolvable merge
conflict.
The merge operation is done in place. The merge operation is done in place.
...@@ -392,13 +399,18 @@ def merge_entities(entity_a: Entity, entity_b: Entity, merge_references_with_emp ...@@ -392,13 +399,18 @@ def merge_entities(entity_a: Entity, entity_b: Entity, merge_references_with_emp
force : bool, optional force : bool, optional
If True, in case `entity_a` and `entity_b` have the same properties, the If True, in case `entity_a` and `entity_b` have the same properties, the
values of `entity_a` are replaced by those of `entity_b` in the merge. values of `entity_a` are replaced by those of `entity_b` in the merge.
If `False`, a RuntimeError is raised instead. Default is False. If `False`, an EntityMergeConflictError is raised instead. Default is False.
Returns Returns
------- -------
entity_a : Entity entity_a : Entity
The initial entity_a after the in-place merge The initial entity_a after the in-place merge
Raises
------
EntityMergeConflictError
In case of an unresolvable merge conflict.
""" """
logging.warning( logging.warning(
...@@ -433,8 +445,8 @@ def merge_entities(entity_a: Entity, entity_b: Entity, merge_references_with_emp ...@@ -433,8 +445,8 @@ def merge_entities(entity_a: Entity, entity_b: Entity, merge_references_with_emp
setattr(entity_a.get_property(key), attribute, setattr(entity_a.get_property(key), attribute,
diff_r2["properties"][key][attribute]) diff_r2["properties"][key][attribute])
else: else:
raise RuntimeError( raise EntityMergeConflictError(
f"Merge conflict:\nEntity a ({entity_a.id}, {entity_a.name}) " f"Entity a ({entity_a.id}, {entity_a.name}) "
f"has a Property '{key}' with {attribute}=" f"has a Property '{key}' with {attribute}="
f"{diff_r2['properties'][key][attribute]}\n" f"{diff_r2['properties'][key][attribute]}\n"
f"Entity b ({entity_b.id}, {entity_b.name}) " f"Entity b ({entity_b.id}, {entity_b.name}) "
...@@ -463,7 +475,9 @@ def merge_entities(entity_a: Entity, entity_b: Entity, merge_references_with_emp ...@@ -463,7 +475,9 @@ def merge_entities(entity_a: Entity, entity_b: Entity, merge_references_with_emp
# force overwrite # force overwrite
setattr(entity_a, special_attribute, sa_b) setattr(entity_a, special_attribute, sa_b)
else: else:
raise RuntimeError("Merge conflict.") raise EntityMergeConflictError(
f"Conflict in special attribute {special_attribute}:\n"
f"A: {sa_a}\nB: {sa_b}")
return entity_a return entity_a
......
...@@ -30,7 +30,8 @@ import pytest ...@@ -30,7 +30,8 @@ import pytest
import caosdb as db import caosdb as db
import caosdb.apiutils import caosdb.apiutils
from caosdb.apiutils import (apply_to_ids, compare_entities, create_id_query, from caosdb.apiutils import (apply_to_ids, compare_entities, create_id_query,
empty_diff, resolve_reference, merge_entities) empty_diff, EntityMergeConflictError,
resolve_reference, merge_entities)
from caosdb.common.models import SPECIAL_ATTRIBUTES from caosdb.common.models import SPECIAL_ATTRIBUTES
...@@ -307,7 +308,7 @@ def test_merge_bug_conflict(): ...@@ -307,7 +308,7 @@ def test_merge_bug_conflict():
r3 = db.Record() r3 = db.Record()
r3.add_property(name="C", value=4, datatype="INTEGER") r3.add_property(name="C", value=4, datatype="INTEGER")
with pytest.raises(RuntimeError) as excinfo: with pytest.raises(EntityMergeConflictError):
merge_entities(r3, r2) merge_entities(r3, r2)
...@@ -402,15 +403,13 @@ def test_wrong_merge_conflict_reference(): ...@@ -402,15 +403,13 @@ def test_wrong_merge_conflict_reference():
rec_a.add_property(name=title_prop.name, value="Some dataset title") rec_a.add_property(name=title_prop.name, value="Some dataset title")
# this does not compare referenced records, so it will fail # this does not compare referenced records, so it will fail
with pytest.raises(RuntimeError) as re: with pytest.raises(EntityMergeConflictError):
merge_entities(rec_a, rec_b, merge_references_with_empty_diffs=False) merge_entities(rec_a, rec_b, merge_references_with_empty_diffs=False)
assert "Merge conflict" in str(re.value)
# ... as should this, of course # ... as should this, of course
rec_b.get_property(license_rt.name).value.name = "Another license" rec_b.get_property(license_rt.name).value.name = "Another license"
with pytest.raises(RuntimeError) as re: with pytest.raises(EntityMergeConflictError) as re:
merge_entities(rec_a, rec_b) merge_entities(rec_a, rec_b)
assert "Merge conflict" in str(re.value)
def test_empty_diff(): def test_empty_diff():
...@@ -484,9 +483,8 @@ def test_force_merge(): ...@@ -484,9 +483,8 @@ def test_force_merge():
recA = db.Record(name="A") recA = db.Record(name="A")
recB = db.Record(name="B") recB = db.Record(name="B")
with pytest.raises(RuntimeError) as re: with pytest.raises(EntityMergeConflictError):
merge_entities(recA, recB) merge_entities(recA, recB)
assert "Merge conflict" in str(re.value)
merge_entities(recA, recB, force=True) merge_entities(recA, recB, force=True)
assert "B" == recA.name assert "B" == recA.name
...@@ -499,9 +497,11 @@ def test_force_merge(): ...@@ -499,9 +497,11 @@ def test_force_merge():
recB = db.Record() recB = db.Record()
recB.description = "something else" recB.description = "something else"
with pytest.raises(RuntimeError) as re: with pytest.raises(EntityMergeConflictError) as emce:
merge_entities(recA, recB) merge_entities(recA, recB)
assert "Merge conflict" in str(re.value) assert str(emce.value) == """Conflict in special attribute description:
A: something
B: something else"""
merge_entities(recA, recB, force=True) merge_entities(recA, recB, force=True)
assert recA.description == "something else" assert recA.description == "something else"
...@@ -514,9 +514,8 @@ def test_force_merge(): ...@@ -514,9 +514,8 @@ def test_force_merge():
recB = db.Record() recB = db.Record()
recB.add_property(name="propA", value="something else") recB.add_property(name="propA", value="something else")
with pytest.raises(RuntimeError) as re: with pytest.raises(EntityMergeConflictError):
merge_entities(recA, recB) merge_entities(recA, recB)
assert "Merge conflict" in str(re.value)
merge_entities(recA, recB, force=True) merge_entities(recA, recB, force=True)
assert recA.get_property("propA").value == "something else" assert recA.get_property("propA").value == "something else"
...@@ -540,9 +539,8 @@ def test_force_merge(): ...@@ -540,9 +539,8 @@ def test_force_merge():
rtB = db.RecordType() rtB = db.RecordType()
rtB.add_property(name="propA", datatype=db.TEXT) rtB.add_property(name="propA", datatype=db.TEXT)
with pytest.raises(RuntimeError) as re: with pytest.raises(EntityMergeConflictError):
merge_entities(rtA, rtB) merge_entities(rtA, rtB)
assert "Merge conflict" in str(re.value)
merge_entities(rtA, rtB, force=True) merge_entities(rtA, rtB, force=True)
assert rtA.get_property("propA").datatype == db.TEXT assert rtA.get_property("propA").datatype == db.TEXT
...@@ -555,9 +553,8 @@ def test_force_merge(): ...@@ -555,9 +553,8 @@ def test_force_merge():
recB = db.Record() recB = db.Record()
recB.add_property(name="propA", value=5, unit="cm") recB.add_property(name="propA", value=5, unit="cm")
with pytest.raises(RuntimeError) as re: with pytest.raises(EntityMergeConflictError):
merge_entities(recA, recB) merge_entities(recA, recB)
assert "Merge conflict" in str(re.value)
merge_entities(recA, recB, force=True) merge_entities(recA, recB, force=True)
assert recA.get_property("propA").unit == "cm" assert recA.get_property("propA").unit == "cm"
# unchanged # unchanged
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment