diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b62a8f20eadc475ab5a95b30e9a6eb2b2e345d..d77e58424d9932fd8178536c4cf7dda6a697469c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the contained elements by ID and/or name. * Official support for Python 3.13 +* Added arguments to `describe_diff` that allow customizing the labels for the 'old' and the 'new' diffs. * Optional `realm` argument for `linkahead_admin.py set_user_password` which defaults to `None`, i.e., the server's default realm. diff --git a/src/linkahead/apiutils.py b/src/linkahead/apiutils.py index 17bd5af4b223b9d0db84b2124b0393e07ba2f80c..49336aa8db24fba663337185c5c37a346330c4cd 100644 --- a/src/linkahead/apiutils.py +++ b/src/linkahead/apiutils.py @@ -447,28 +447,46 @@ def compare_entities(entity0: Optional[Entity] = None, return diff -def empty_diff(old_entity: Entity, new_entity: Entity, +def empty_diff(entity0: Entity, + entity1: Entity, compare_referenced_records: bool = False, - entity_name_id_equivalency: bool = False) -> bool: + entity_name_id_equivalency: bool = False, + old_entity: Optional[Entity] = None, + new_entity: Optional[Entity] = None, + ) -> bool: """Check whether the `compare_entities` found any differences between - old_entity and new_entity. + entity0 and entity1. Parameters ---------- - old_entity, new_entity : Entity + entity0, entity1 : Entity Entities to be compared compare_referenced_records : bool, optional - Whether to compare referenced records in case of both, `old_entity` and - `new_entity`, have the same reference properties and both have a Record + Whether to compare referenced records in case of both, `entity0` and + `entity1`, have the same reference properties and both have a Record object as value. entity_name_id_equivalency : bool, optional If set to True, the comparison between an entity and an int or str also checks whether the int/str matches the name or id of the entity, so Entity(id=100) == 100 == "100". """ - olddiff, newdiff = compare_entities(old_entity, new_entity, - compare_referenced_records, entity_name_id_equivalency) - for diff in [olddiff, newdiff]: + if entity0 is None and old_entity is None: + raise ValueError("Please provide the first entity as first argument (`entity0`)") + if entity1 is None and new_entity is None: + raise ValueError("Please provide the second entity as second argument (`entity1`)") + if old_entity is not None: + warnings.warn("Please use 'entity0' instead of 'old_entity'.", DeprecationWarning) + if entity0 is not None: + raise ValueError("You cannot use both entity0 and old_entity") + entity0 = old_entity + if new_entity is not None: + warnings.warn("Please use 'entity1' instead of 'new_entity'.", DeprecationWarning) + if entity1 is not None: + raise ValueError("You cannot use both entity1 and new_entity") + entity1 = new_entity + e0diff, e1diff = compare_entities(entity0, entity1, compare_referenced_records, + entity_name_id_equivalency) + for diff in [e0diff, e1diff]: for key in ["parents", "properties"]: if len(diff[key]) > 0: # There is a difference somewhere in the diff @@ -622,55 +640,103 @@ def merge_entities(entity_a: Entity, return entity_a -def describe_diff(olddiff, newdiff, name=None, as_update=True): +def describe_diff(entity0_diff: dict[str, Any], entity1_diff: dict[str, Any], + name: Optional[str] = None, + as_update: Optional[bool] = None, + label_e0: str = "first version", + label_e1: str = "second version", + olddiff: Any = None, + newdiff: Any = None, + ) -> str: """ - This function generates a textual representation of the differences between two entities that have been generated - using compare_entities. + Generate a textual description of the differences between two entities. + These can be generated using :func:`compare_entities` and used within this function like this: + + `describe_diff(*compare_entities(...))` Arguments: ---------- - olddiff: The diff output for the entity marked as "old". - newdiff: The diff output for the entity marked as "new". - Example: - >>> describe_diff(*compare_entities(db.Record().add_property("P"), "value", db.Record())) + entity0_diff: dict[str, Any] + First element of the tuple output of :func:`compare_entities`. + This is referred to as the "first" version. + + entity1_diff: dict[str, Any] + Second element of the tuple output of :func:`compare_entities`. + This is referred to as the "second" version. + + + name: Optional[str] + Default None. Name of the entity that will be shown in the output text. + + as_update: Optional[bool] + Default None. Not used anymore. + + label_e0: str + Can be used to set a custom label for the diff that is associated with the first entity. + + label_e1: str + Can be used to set a custom label for the diff that is associated with the second entity. + + olddiff: Any + Deprecated. Replaced by entity0_diff. + + newdiff: Any + Deprecated. Replaced by entity1_diff. + + Returns: + -------- + A text description of the differences. + """ description = "" - for attr in list(set(list(olddiff.keys()) + list(newdiff.keys()))): + if as_update: + warnings.warn("'as_update' is deprecated. Do not use it.", DeprecationWarning) + if olddiff: + warnings.warn("'olddiff' is deprecated. Use 'entity0_diff' instead.", DeprecationWarning) + entity0_diff = olddiff + if newdiff: + warnings.warn("'newdiff' is deprecated. Use 'entity1_diff' instead.", DeprecationWarning) + entity1_diff = newdiff + + for attr in list(set(list(entity0_diff.keys()) + list(entity1_diff.keys()))): if attr == "parents" or attr == "properties": continue description += "{} differs:\n".format(attr) - description += "old version: {}\n".format( - olddiff[attr] if attr in olddiff else "not set") - description += "new version: {}\n\n".format( - newdiff[attr] if attr in newdiff else "not set") + description += label_e0 + ": {}\n".format( + entity0_diff[attr] if attr in entity0_diff else "not set") + description += label_e1 + ": {}\n\n".format( + entity1_diff[attr] if attr in entity1_diff else "not set") - if len(olddiff["parents"]) > 0: - description += ("Parents that are only in the old version:\n" - + ", ".join(olddiff["parents"]) + "\n") + if len(entity0_diff["parents"]) > 0: + description += ("Parents that are only in the " + label_e0 + ":\n" + + ", ".join(entity0_diff["parents"]) + "\n") - if len(newdiff["parents"]) > 0: - description += ("Parents that are only in the new version:\n" - + ", ".join(olddiff["parents"]) + "\n") + if len(entity1_diff["parents"]) > 0: + description += ("Parents that are only in the " + label_e1 + ":\n" + + ", ".join(entity0_diff["parents"]) + "\n") - for prop in list(set(list(olddiff["properties"].keys()) - + list(newdiff["properties"].keys()))): + for prop in list(set(list(entity0_diff["properties"].keys()) + + list(entity1_diff["properties"].keys()))): description += "property {} differs:\n".format(prop) - if prop not in olddiff["properties"]: - description += "it does not exist in the old version: \n" - elif prop not in newdiff["properties"]: - description += "it does not exist in the new version: \n" + if prop not in entity0_diff["properties"]: + description += "it does not exist in the " + label_e0 + ":\n" + elif prop not in entity1_diff["properties"]: + description += "it does not exist in the " + label_e1 + ":\n" else: - description += "old version: {}\n".format( - olddiff["properties"][prop]) - description += "new version: {}\n\n".format( - newdiff["properties"][prop]) + description += label_e0 + ": {}\n".format( + entity0_diff["properties"][prop]) + description += label_e1 + ": {}\n\n".format( + entity1_diff["properties"][prop]) if description != "": - description = ("## Difference between the old and the new " - "version of {}\n\n".format(name))+description + description = ("## Difference between the " + + label_e0 + + " and the " + + label_e1 + + " of {}\n\n".format(name)) + description return description diff --git a/tox.ini b/tox.ini index 592c660c5bbbf5805a3ecbb3e60c41f597182a55..c63555c27b73224106109c1f675e9525d9e89b74 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,6 @@ max-line-length=100 [pytest] testpaths = unittests xfail_strict = True -addopts = -x -vv --cov=caosdb +addopts = -x -vv --cov=linkahead pythonpath = src diff --git a/unittests/test_apiutils.py b/unittests/test_apiutils.py index 2fb946c518d940bd505622284070a0f5fafdf12f..fdd5adda065a563b15008f1b840539c110921b65 100644 --- a/unittests/test_apiutils.py +++ b/unittests/test_apiutils.py @@ -1,6 +1,7 @@ # # This file is a part of the LinkAhead Project. # +# Copyright (C) 2024 Alexander Schlemmer <a.schlemmer@indiscale.com> # Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> # Copyright (C) 2022 Florian Spreckelsen <f.spreckelsen@indiscale.com> # Copyright (C) 2022 Daniel Hornung <d.hornung@indiscale.com> @@ -32,7 +33,8 @@ import linkahead.apiutils import pytest from linkahead.apiutils import (EntityMergeConflictError, apply_to_ids, compare_entities, create_id_query, empty_diff, - merge_entities, resolve_reference) + merge_entities, resolve_reference, + describe_diff) from linkahead.common.models import SPECIAL_ATTRIBUTES @@ -946,3 +948,46 @@ def test_merge_id_with_resolved_entity(): merge_entities(recA, recB, merge_id_with_resolved_entity=True) assert recA.get_property(rtname).value == [ref_rec, ref_id*2] assert recB.get_property(rtname).value == [ref_id, ref_id*2] + + +def test_describe_diff(): + recA = db.Record() + recA.add_property(name="propA", value=2) + recA.add_property(name="propB", value=2) + recA.add_property(name="propD", value=-273, unit="K") + + recB = db.Record() + recB.add_property(name="propA", value=2) + recB.add_property(name="propB", value=12) + recB.add_property(name="propC", value="cool 17") + recB.add_property(name="propD", value=-273, unit="°C") + + diff = compare_entities(recA, recB) + diffout = describe_diff(*diff) + + assert diffout.startswith("## Difference between the first version and the second version of None") + + # The output of the describe_diff function is currently not ordered (e.g. by name of the property) + # so we cannot just compare a well-defined output string. + + assert "it does not exist in the first version:" in diffout + assert "first version: {'value': 2}" in diffout + assert "second version: {'value': 12}" in diffout + + assert "first version: {'unit': 'K'}" in diffout + assert "second version: {'unit': '°C'}" in diffout + + diffout = describe_diff(*diff, name="Entity") + assert diffout.startswith("## Difference between the first version and the second version of Entity") + + diffout = describe_diff(*diff, label_e0="recA", label_e1="recB") + assert "recA: {'value': 2}" in diffout + assert "recB: {'value': 12}" in diffout + + assert "recA: {'unit': 'K'}" in diffout + assert "recB: {'unit': '°C'}" in diffout + + assert "it does not exist in the recA:" in diffout + + assert "first" not in diffout + assert "second" not in diffout